SPA 구조로 된 웹사이트에서 데이터를 크롤링할 때 흔히 마주치는 문제:
바로 무한스크롤(infinite scroll)!!
예를 들어 쿠팡 상품 리뷰 페이지를 보면, 처음에 10개 정도의 리뷰만 보이고, 스크롤을 아래로 내릴 때마다 추가로 로딩된다.
이 구조에서는 단순히 page.content()로는 전체 리뷰를 긁을 수 없다.
Puppeteer로 직접 "스크롤"을 해줘야 한다.

const browser = await puppeteer.launch({ headless: true });
const page = await browser.newPage();
await page.goto(url, {
waitUntil: 'networkidle2' // 네트워크 요청이 거의 끝났을 때까지 기다림
});
waitUntil: 'networkidle2'는 페이지 내 JS 비동기 요청들이 마무리되었음을 기다리는 전략이다.await page.waitForSelector('.sdp-review__article__list'); // 리뷰 리스트 DOM이 뜰 때까지 기다림
await page.waitForTimeout(1000); // 또는 UI가 안정화되도록 약간 대기
let previousHeight = await page.evaluate('document.body.scrollHeight');
while (true) {
await page.evaluate('window.scrollTo(0, document.body.scrollHeight)');
await page.waitForTimeout(1000);
const newHeight = await page.evaluate('document.body.scrollHeight');
if (newHeight === previousHeight) break;
previousHeight = newHeight;
}
scrollHeight를 계속 비교하는 것.scrollHeight 비교로는 부족할 수 있고, 리뷰 DOM이 일정 개수 이상 쌓였는지로 판단하기도 한다.let maxRetries = 10;
let retryCount = 0;
while (retryCount < maxRetries) {
const beforeCount = await page.$$eval('.sdp-review__article__list > li', els => els.length);
await page.evaluate('window.scrollTo(0, document.body.scrollHeight)');
await page.waitForTimeout(1500);
const afterCount = await page.$$eval('.sdp-review__article__list > li', els => els.length);
if (afterCount === beforeCount) {
retryCount++;
} else {
retryCount = 0;
}
}
✅ 이런 구조는 리뷰가 1500개에 못 미치는 경우에도 안정적으로 종료할 수 있게 도와준다.
await page.screenshot({ path: 'loaded.png', fullPage: true });
IntersectionObserver를 쓰기도 한다.scrollTo() 방식이 훨씬 확실하고 실전적이다.무한스크롤을 처리하는 건 단순한 반복문 이상의 논리다.
로딩 타이밍, 렌더링 완료, 종료 조건 등 다양한 요소를 고려해야 한다.
Puppeteer를 통해 리뷰 데이터를 수집하면서 느낀 점은
“실제 브라우저가 하는 일”을 하나하나 따라 하는 게 결국 크롤링의 본질이라는 것이다.