Skip to content

Instantly share code, notes, and snippets.

@listly-io
Created August 6, 2026 13:22
Show Gist options
  • Select an option

  • Save listly-io/25c96a75da8bafa1aae81fa8d21963d2 to your computer and use it in GitHub Desktop.

Select an option

Save listly-io/25c96a75da8bafa1aae81fa8d21963d2 to your computer and use it in GitHub Desktop.
musinsa_review_2026-08-06
// test page: https://www.musinsa.com/review/goods/3098202?gf=A
// ============================================================
// ▶ 사용자 설정 영역 (필요에 따라 수정하세요)
// ============================================================
const MAX_SCROLL_COUNT = 100; // 최대 스크롤 횟수 (많을수록 더 많은 리뷰 수집)
const STOP_AFTER_EMPTY = 5; // 새 리뷰가 안 나오는 스크롤이 연속 N회면 종료
const WAIT_AFTER_SCROLL = 1500; // 스크롤 후 로딩 대기 시간 (ms)
const WAIT_BEFORE_SCROLL = 300; // 수집 후 스크롤 전 대기 시간 (ms)
const REVIEW_LIST_SELECTOR =
'.review-list-item__Container-sc-5kl1h0-0'; // 개별 리뷰 항목 셀렉터
const REVIEW_ID_ATTR = 'data-content-id'; // 리뷰 고유 ID 속성 (중복 제거용)
// ※ 이 사이트(무신사)는 무한 스크롤 + DOM 재활용 방식이라
// 페이지네이션 버튼이 없습니다. 스크롤로 리뷰를 불러오며,
// 화면 밖 리뷰는 DOM에서 제거되므로 REVIEW_ID_ATTR 로 중복을 걸러냅니다.
const CONTAINER_ID = '__review_collector'; // 수집 컨테이너 ID
// ============================================================
// ▶ 내부 로직 (수정 불필요)
// ============================================================
(async () => {
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
// 수집 컨테이너 생성
let container = document.getElementById(CONTAINER_ID);
if (!container) {
container = document.createElement('div');
container.id = CONTAINER_ID;
Object.assign(container.style, {
position: 'fixed',
right: '200px',
bottom: '200px',
zIndex: '999999',
background: 'rgba(0, 0, 0, 0.85)',
color: '#0f0',
fontSize: '18px',
fontWeight: 'bold',
fontFamily: 'monospace',
padding: '16px 24px',
borderRadius: '12px',
border: '2px solid #0f0',
boxShadow: '0 4px 20px rgba(0,255,0,.3)',
cursor: 'default',
userSelect: 'none',
overflow: 'hidden',
});
document.body.appendChild(container);
}
// 수집 저장소 (숨김 영역) - 컨테이너 내부에 display:none 인 래퍼
let store = container.querySelector('#__review_store');
if (!store) {
store = document.createElement('div');
store.id = '__review_store';
store.style.display = 'none';
container.appendChild(store);
}
// 카운트 표시 라벨
let label = container.querySelector('#__review_label');
if (!label) {
label = document.createElement('div');
label.id = '__review_label';
container.appendChild(label);
}
const updateLabel = () => {
const count = store.children.length;
label.textContent = `수집된 리뷰: ${count}개`;
};
// 이미 수집한 리뷰 ID 집합 (중복 제거용)
const seen = new Set(
[...store.children].map((c) => c.getAttribute(REVIEW_ID_ATTR))
);
// 현재 화면에 있는 리뷰 수집 (중복은 건너뜀)
// 반환값 = 이번에 새로 추가된 리뷰 수
const collectCurrentView = () => {
const reviews = document.querySelectorAll(
`${REVIEW_LIST_SELECTOR}:not(#${CONTAINER_ID} *)`
);
let added = 0;
reviews.forEach((el) => {
// 리뷰 고유 ID (없으면 앞부분 텍스트로 대체)
const id = el.getAttribute(REVIEW_ID_ATTR) || el.innerText.slice(0, 50);
if (seen.has(id)) return;
seen.add(id);
const clone = el.cloneNode(true);
store.appendChild(clone);
added++;
});
console.log(`[Scraper] ${added}개 리뷰 수집 (누적 ${store.children.length}개)`);
updateLabel();
return added;
};
// 다음 리뷰 불러오기 (스크롤 → 대기)
// ※ 이 사이트는 무한 스크롤이므로 버튼 클릭 대신 화면을 아래로 내립니다.
const goNextScroll = async () => {
await sleep(WAIT_BEFORE_SCROLL);
window.scrollBy(0, window.innerHeight * 0.8);
await sleep(WAIT_AFTER_SCROLL);
};
// 메인
console.log(`[Scraper] 수집 시작 — 최대 ${MAX_SCROLL_COUNT}회 스크롤`);
let emptyStreak = 0;
for (let step = 1; step <= MAX_SCROLL_COUNT; step++) {
// 현재 화면 수집
const added = collectCurrentView();
console.log(`[Scraper] ${step}/${MAX_SCROLL_COUNT} 스크롤 수집 완료`);
// 새 리뷰가 없으면 연속 카운트, 있으면 초기화
if (added === 0) {
emptyStreak++;
} else {
emptyStreak = 0;
}
// 연속으로 새 리뷰가 안 나오면 끝에 도달한 것으로 보고 종료
if (emptyStreak >= STOP_AFTER_EMPTY) {
console.log('[Scraper] 더 이상 새 리뷰 없음 — 수집 종료');
break;
}
// 다음 리뷰 불러오기 위해 스크롤
await goNextScroll();
}
collectCurrentView();
console.log(`[Scraper] 수집 완료! 총 ${store.children.length}개 리뷰`);
updateLabel();
// ▶ 나중에 수집된 HTML을 꺼내려면 콘솔에서 확인해주세요.
// document.getElementById('__review_store').innerHTML
})();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment