JavaScript 렌더링
JavaScript 렌더링은 페이지의 구조나 내용을 변경하기 위해 JavaScript를 실행하는 과정입니다. 클라이언트 사이드 렌더링이라고도 하며, 서버 사이드 렌더링과 반대되는 개념입니다. 최근의 웹사이트들은 클라이언트에서 렌더링하거나, 서버에서 렌더링하거나, 혹은 두 가지 방식을 모두 사용하기도 합니다.
Crawlee 웹사이트는 콘텐츠 표시에 JavaScript 렌더링을 사용하지 않기 때문에, 다른 예제를 살펴보겠습니다. Apify Store는 누구나 무료로 사용할 수 있는 스크래퍼와 자동화 도구인 **액터(actor)**들의 라이브러리입니다. 액터 목록을 표시하기 위해 JavaScript 렌더링을 사용하므로, 이를 통해 작동 방식을 설명하겠습니다.
import { CheerioCrawler } from 'crawlee';
const crawler = new CheerioCrawler({
async requestHandler({ $, request }) {
// 액터 카드의 텍스트 내용을 추출
const actorText = $('.ActorStoreItem').text();
console.log(`ACTOR: ${actorText}`);
}
})
await crawler.run(['https://apify.com/store']);
코드를 실행하면 크롤러가 액터 카드의 내용을 출력하지 않는 것을 확인할 수 있습니다.
ACTOR:
이는 Apify Store가 클라이언트 사이드 JavaScript를 사용하여 콘텐츠를 렌더링하기 때문입니다. CheerioCrawler
는 JavaScript를 실행할 수 없어서 텍스트가 HTML에 나타나지 않습니다.
Chrome DevTools를 사용하여 이를 확인할 수 있습니다. https://apify.com/store 에서 페이지를 우클릭하고 페이지 소스 보기를 선택한 후 ActorStoreItem을 검색하면 결과가 없을 것입니다. 하지만 다시 우클릭하고 검사를 선택한 후 같은 ActorStoreItem을 검색하면 많은 결과를 찾을 수 있습니다.
어떻게 이런 일이 가능할까요? 페이지 소스 보기는 JavaScript 실행 전의 원본 HTML을 보여주기 때문입니다. 이것이 CheerioCrawler
가 받는 내용입니다. 반면 검사에서는 JavaScript 실행 후의 현재 HTML을 볼 수 있습니다. 이를 이해하면 CheerioCrawler
가 데이터를 찾지 못하는 것이 당연합니다. 이런 경우에는 헤드리스 브라우저가 필요합니다.
헤드리스 브라우저
.ActorStoreItem
의 내용을 가져오려면 헤드리스 브라우저를 사용해야 합니다. 브라우저를 제어하기 위해 Puppeteer나 Playwright 중 하나를 선택할 수 있습니다. 선택은 간단합니다. 이미 알고 있는 것이 있다면 그것을 사용하세요. 둘 다 알거나 둘 다 모른다면, 대부분의 경우 더 나은 Playwright를 선택하세요.
요소 렌더링 대기
어떤 라이브러리를 선택하든 두 가지 예제 코드를 제공합니다. Playwright가 사용하기에 조금 더 편리하지만, 두 라이브러리 모두 작업을 완수할 수 있습니다. 가장 큰 차이점은 Playwright는 요소가 나타날 때까지 자동으로 기다리는 반면, Puppeteer에서는 명시적으로 대기해야 한다는 것입니다.
- PlaywrightCrawler
- PuppeteerCrawler
import { PlaywrightCrawler } from 'crawlee';
const crawler = new PlaywrightCrawler({
async requestHandler({ page }) {
// page.locator는 CSS 선택자를 사용하여 DOM에서 요소를 참조하지만
// 아직 요소에 접근하지 않습니다.
const actorCard = page.locator('.ActorStoreItem').first();
// locator 메서드 중 하나를 호출하면 Playwright가
// 요소가 렌더링될 때까지 기다리고 요소에 접근합니다.
const actorText = await actorCard.textContent();
console.log(`ACTOR: ${actorText}`);
},
});
await crawler.run(['https://apify.com/store']);
import { PuppeteerCrawler } from 'crawlee';
const crawler = new PuppeteerCrawler({
async requestHandler({ page }) {
// Puppeteer는 Playwright의 자동 대기 기능이 없으므로
// 요소를 명시적으로 기다려야 합니다.
await page.waitForSelector('.ActorStoreItem');
// Puppeteer는 locator.textContent와 같은 헬퍼 메서드가 없으므로
// 페이지 내 JavaScript를 사용하여 값을 수동으로 추출해야 합니다.
const actorText = await page.$eval('.ActorStoreItem', (el) => {
return el.textContent;
});
console.log(`ACTOR: ${actorText}`);
},
});
await crawler.run(['https://apify.com/store']);
코드를 실행하면 첫 번째 액터 카드의 형식이 깨진 내용이 콘솔에 출력됩니다:
ACTOR: Web Scraperapify/web-scraperCrawls arbitrary websites using [...]
농담이 아닙니다
요소를 기다려야 한다는 것을 믿지 않으신다면, 대기를 건너뛰는 다음 코드를 실행해보세요.
- PlaywrightCrawler
- PuppeteerCrawler
import { PlaywrightCrawler } from 'crawlee';
const crawler = new PlaywrightCrawler({
async requestHandler({ page }) {
// 여기서는 선택자를 기다리지 않고 즉시
// 페이지에서 텍스트 콘텐츠를 추출합니다.
const actorText = await page.$eval('.ActorStoreItem', (el) => {
return el.textContent;
});
console.log(`ACTOR: ${actorText}`);
},
});
await crawler.run(['https://apify.com/store']);
import { PuppeteerCrawler } from 'crawlee';
const crawler = new PuppeteerCrawler({
async requestHandler({ page }) {
// 여기서는 선택자를 기다리지 않고 즉시
// 페이지에서 텍스트 콘텐츠를 추출합니다.
const actorText = await page.$eval('.ActorStoreItem', (el) => {
return el.textContent;
});
console.log(`ACTOR: ${actorText}`);
},
});
await crawler.run(['https://apify.com/store']);
두 경우 모두 요청이 몇 번 재시도된 후 다음과 같은 오류와 함께 실패합니다:
ERROR [...] Error: failed to find element matching selector ".ActorStoreItem"
이는 브라우저에서 요소에 접근하려고 할 때 아직 DOM에 렌더링되지 않았기 때문입니다.
이 가이드는 JavaScript 렌더링과 헤드리스 브라우저 사용에 대한 기본 개념만 다룹니다. 더 자세히 알아보려면 Apify Academy의 Puppeteer & Playwright 과정을 참고하세요. 무료이며 오픈소스입니다 ❤️.