본문으로 건너뛰기

리팩토링

데이터를 추출하고 크롤러가 완성된 것처럼 보일 수 있지만, 사실 이것은 시작에 불과합니다. 간단한 설명을 위해 오류 처리, 프록시, 로깅, 아키텍처, 테스트, 문서화 등 신뢰할 수 있는 소프트웨어가 갖춰야 할 많은 요소들을 생략했습니다. 다행히도 오류 처리는 대부분 Crawlee가 자체적으로 처리하므로, 특별한 처리가 필요한 경우가 아니라면 걱정하지 않으셔도 됩니다.

자동 봇 차단 방지 기능 안내

아마도 안티 블로킹, 봇 차단 방지, 스텔스 기능에 대해 궁금하실 텐데, 왜 아직 언급하지 않았는지 궁금하실 수 있습니다. 그 이유는 간단합니다: 이러한 기능들은 기본 설정에서 자동으로 적용되어 수동 조정 없이도 원활한 시작이 가능합니다. 다만, 기본 설정이 강력하긴 하지만 모든 상황을 커버하지는 못할 수 있습니다.

자세한 내용은 차단 방지하기, 프록시 관리, 세션 관리 가이드를 참조하세요.

이제 좋은 코딩 관행을 위해 Router를 사용하여 크롤러 코드를 더 잘 구조화하는 방법을 살펴보겠습니다.

라우팅​

다음 코드에서는 몇 가지 변경사항을 적용했습니다:

  • 코드를 여러 파일로 분리했습니다.
  • console.log 대신 더 보기 좋은 컬러 로그를 위해 Crawlee 로거를 사용했습니다.
  • if 문 없이 더 깔끔한 라우팅을 위해 Router를 추가했습니다.

main.mjs 파일에는 크롤러의 기본 구조를 배치했습니다:

src/main.mjs
import { PlaywrightCrawler, log } from 'crawlee';
import { router } from './routes.mjs';

// CRAWLEE_LOG_LEVEL 환경 변수나 설정 옵션으로
// 설정하는 것이 더 좋습니다. 이것은 예시일 뿐입니다 😈
log.setLevel(log.LEVELS.DEBUG);

log.debug('크롤러 설정 중.');
const crawler = new PlaywrightCrawler({
// if 문이 있는 긴 requestHandler 대신
// router 인스턴스를 제공합니다.
requestHandler: router,
});

await crawler.run(['https://warehouse-theme-metal.myshopify.com/collections']);

그리고 별도의 routes.mjs 파일에는:

src/routes.mjs
import { createPlaywrightRouter, Dataset } from 'crawlee';

// createPlaywrightRouter()는 더 나은 인텔리센스와
// 타입을 얻기 위한 헬퍼입니다. Router.create()도 사용 가능합니다.
export const router = createPlaywrightRouter();

// if 문의 request.label === DETAIL 부분을 대체합니다.
router.addHandler('DETAIL', async ({ request, page, log }) => {
log.debug(`데이터 추출 중: ${request.url}`);
const urlPart = request.url.split('/').slice(-1);
const manufacturer = urlPart[0].split('-')[0];

const title = await page.locator('.product-meta h1').textContent();
const sku = await page
.locator('span.product-meta__sku-number')
.textContent();

const priceElement = page
.locator('span.price')
.filter({
hasText: '$',
})
.first();

const currentPriceString = await priceElement.textContent();
const rawPrice = currentPriceString.split('$')[1];
const price = Number(rawPrice.replaceAll(',', ''));

const inStockElement = page
.locator('span.product-form__inventory')
.filter({
hasText: 'In stock',
})
.first();

const inStock = (await inStockElement.count()) > 0;

const results = {
url: request.url,
manufacturer,
title,
sku,
currentPrice: price,
availableInStock: inStock,
};

log.debug(`데이터 저장 중: ${request.url}`);
await Dataset.pushData(results);
});

router.addHandler('CATEGORY', async ({ page, enqueueLinks, request, log }) => {
log.debug(`페이지네이션 인큐 중: ${request.url}`);
// 현재 카테고리 페이지에 있습니다. 이를 통해 페이지네이션을 하면서 모든 제품과
// 발견하는 후속 페이지를 인큐할 수 있습니다

await page.waitForSelector('.product-item > a');
await enqueueLinks({
selector: '.product-item > a',
label: 'DETAIL', // <= 다른 라벨 사용
});

// 이제 "다음" 버튼을 찾아서 다음 결과 페이지를 인큐해야 합니다(있는 경우)
const nextButton = await page.$('a.pagination__next');
if (nextButton) {
await enqueueLinks({
selector: 'a.pagination__next',
label: 'CATEGORY', // <= 같은 라벨 사용
});
}
});

// 시작 URL과 LIST 라벨이 지정된 URL을 처리하는
// 기본 라우트입니다.
router.addDefaultHandler(async ({ request, page, enqueueLinks, log }) => {
log.debug(`페이지에서 카테고리 인큐 중: ${request.url}`);
// 이는 라벨이 없는 시작 페이지에 있다는 의미입니다.
// 이 페이지에서는 모든 카테고리 페이지만 인큐하면 됩니다.

await page.waitForSelector('.collection-block-item');
await enqueueLinks({
selector: '.collection-block-item',
label: 'CATEGORY',
});
});

이러한 변경사항들을 자세히 살펴보겠습니다. 이러한 수정으로 크롤러의 가독성과 관리가 더욱 쉬워질 것입니다.

코드를 여러 파일로 분리하기​

코드를 여러 파일로 분리하고 로직을 분리하는 것은 매우 중요합니다. 한 파일의 코드가 적을수록 한 번에 생각해야 할 코드가 줄어들며, 이는 좋은 방법입니다. 실제로는 라우트도 별도의 파일로 더 분리하는 것이 좋을 것입니다.

Crawlee log 사용하기​

Crawlee의 log 객체에 대해서는 문서에서 자세히 읽어보실 수 있지만, 여기서는 로그 레벨에 대해 강조하고 싶습니다.

Crawlee log는 log.debug, log.info, log.warning 등 여러 로그 레벨을 제공합니다. 이는 로그를 더 읽기 쉽게 만들 뿐만 아니라, log.setLevel() 함수나 CRAWLEE_LOG_LEVEL 환경 변수를 통해 특정 레벨을 선택적으로 끌 수 있습니다. 이를 통해 필요할 때는 디버그 로그를 활용하고, 필요하지 않을 때는 로그를 깔끔하게 유지할 수 있습니다.

Router를 사용한 크롤링 구조화​

처음에는 단순한 if/else 문을 사용하여 페이지 유형에 따라 다른 로직을 선택하는 것이 더 읽기 쉬워 보일 수 있습니다. 하지만 페이지 유형이 두 개 이상이 되고, 각 페이지의 로직이 수십 또는 수백 줄로 늘어나면 이 방식은 복잡해질 수 있습니다.

모든 프로그래밍 언어에서 로직을 읽기 쉽고 이해하기 쉬운 작은 단위로 나누는 것이 좋은 관행입니다. 모든 것이 서로 연결되어 있고 변수를 어디서나 사용할 수 있는 천 줄짜리 requestHandler()를 스크롤하면서 디버깅하는 것은 아름답지 않습니다. 그래서 우리는 라우트를 별도의 파일로 분리하는 것을 선호합니다.

다음 단계​

다음이자 마지막 단계에서는 Crawlee 프로젝트를 클라우드에 배포하는 방법을 살펴보겠습니다. CLI를 사용하여 프로젝트를 시작했다면 이미 Dockerfile이 준비되어 있으며, 다음 섹션에서는 Apify 플랫폼에 쉽게 배포하는 방법을 보여드리겠습니다.