# 브라우저로 자료 모으기 — Crawlee + 스텔스

> **이 장에서 배우는 것**
> Crawlee의 `PuppeteerCrawler`에 `puppeteer-extra-plugin-stealth`를 붙이는 법.
> 그리고 **붙이기 전에 확인할 것**과, 붙인 뒤에 반드시 겪게 되는 것.

## 먼저 — 브라우저가 정말 필요한가

[사례 — 콘텐츠 공장](/guide/ht-case-factory)의 수집 단계는 `terminal` 명령 하나입니다.
대부분의 수집은 거기서 끝납니다. 위에서부터 확인하세요.

| 상대가 제공하는 것 | 쓸 것 |
|---|---|
| 공식 API | API |
| RSS · 사이트맵 · JSON 엔드포인트 | 그것 |
| 서버가 이미 렌더한 HTML | `fetch` + HTML 파서 |
| 브라우저에서 스크립트가 돌아야 나오는 화면 | 그때 비로소 Crawlee |

브라우저는 **가장 비싼 선택지**입니다. Chrome 한 탭이 수백 MB를 쓰고, 페이지당
시간도 초 단위로 늘어나며, 무엇보다 **가장 자주 깨집니다**. 상대가 마크업을
조금만 바꿔도 멈춥니다.

```mermaid
graph TD
  A["자료가 필요하다"] --> B{"공식 API가 있나"}
  B -->|있다| C["API — 여기서 끝"]
  B -->|없다| D{"HTML에 이미 들어 있나"}
  D -->|있다| E["fetch + 파서"]
  D -->|없다| F["브라우저 — Crawlee"]
```

## 그 다음 — 해도 되는 일인지

이 확인이 기술보다 **먼저**입니다. 아래를 어기면 차단이 문제가 아니라 계약과
법률 문제가 됩니다. 스텔스 플러그인은 그 어느 것도 해결해 주지 않습니다.

- **robots.txt와 이용약관.** 자동 수집을 금지한 곳이면 기술적으로 가능한지와
  무관하게 하지 않습니다.
- **로그인·결제 장벽 뒤.** 계정 약관에 걸립니다. "내 계정으로 내 데이터를
  받는 것"과 "장벽을 우회하는 것"은 다릅니다.
- **개인정보.** 공개돼 있다고 수집해도 되는 것은 아닙니다. 수집 근거가 필요합니다.
- **상대 서버 부담.** 동시 요청과 간격을 제한하세요. Crawlee의
  `maxConcurrency`와 `maxRequestsPerMinute`가 그 자리입니다.

**가장 편한 경우는 내 사이트, 내 데이터, 또는 명시적으로 허가받은 곳입니다.**
자기 서비스의 렌더링을 점검하거나 내 상품 정보를 모으는 용도라면 위 대부분이
애초에 문제가 되지 않습니다. 그 밖의 대상이라면 먼저 허락을 구하는 편이
빠릅니다.

## 배선

설치:

```
npm install crawlee puppeteer puppeteer-extra puppeteer-extra-plugin-stealth
```

핵심은 `launchContext.launcher`입니다. Crawlee에게 **기본 puppeteer 대신
puppeteer-extra를 써서 브라우저를 띄우라**고 알려 주는 자리입니다.

```js
import { PuppeteerCrawler } from "crawlee";
import puppeteerExtra from "puppeteer-extra";
import stealthPlugin from "puppeteer-extra-plugin-stealth";

// 크롤러를 만들기 **전에** 등록해야 합니다.
puppeteerExtra.use(stealthPlugin());

const crawler = new PuppeteerCrawler({
  launchContext: {
    launcher: puppeteerExtra,
    launchOptions: { headless: true },
  },
  maxConcurrency: 2,
  maxRequestsPerMinute: 30,
  async requestHandler({ page, request, log }) {
    const title = await page.title();
    log.info(`${title} — ${request.url}`);
  },
});

await crawler.run(["https://example.com"]);
```

배선은 여기까지입니다. 어려운 것은 이 앞뒤입니다.

## 함정 — Crawlee도 이미 같은 일을 하고 있습니다

**이 장에서 가장 중요한 문단입니다.**

Crawlee는 자체 **브라우저 지문 생성기**를 갖고 있고, `PuppeteerCrawler`와
`PlaywrightCrawler`에서 **기본으로 켜져 있습니다**. 예전에 있던 `stealth` 옵션이
이것으로 대체됐습니다. 즉 아무 설정도 안 한 상태에서 이미 지문이 만들어져
주입되고 있습니다.

여기에 스텔스 플러그인을 얹으면 **두 도구가 같은 값을 각자 덮어씁니다.**
`navigator.webdriver`, User-Agent, 언어, 화면 크기 같은 것들입니다. 운이 나쁘면
서로 어긋난 조합이 나와서, **아무것도 안 썼을 때보다 더 눈에 띄는** 상태가
됩니다. "스텔스를 붙였는데 오히려 더 막힌다"의 흔한 정체가 이것입니다.

한쪽만 쓰세요.

```js
const crawler = new PuppeteerCrawler({
  // 스텔스 플러그인을 쓰기로 했다면 Crawlee 지문은 끕니다.
  browserPoolOptions: { useFingerprints: false },
  launchContext: { launcher: puppeteerExtra },
  // ...
});
```

**그 전에 스텔스가 정말 필요한지 확인하세요.** Crawlee 기본 지문만으로 되는
경우가 많고, 그러면 의존성이 두 개 줄고 아래의 유지비도 사라집니다. 순서는
"아무것도 안 붙이고 시도 → 안 되면 한쪽만 붙이기"입니다.

## 유지비 — 이건 한 번 만들고 끝나지 않습니다

| 무엇이 | 어떻게 |
|---|---|
| 스텔스 플러그인 | puppeteer 새 버전을 **뒤따라갑니다**. 올리자마자 깨지는 조합이 생깁니다 |
| 탐지 쪽 | 계속 바뀝니다. 오늘 되는 것이 다음 달에 안 됩니다 |
| 상대 마크업 | 예고 없이 바뀝니다 |

그래서 **성공을 가정한 파이프라인을 만들면 안 됩니다.** 조용히 빈 결과를
내려보내는 것이 가장 나쁩니다 — 뒷단계가 그 빈 값으로 계속 돌아갑니다.

[예약으로 파이프라인 만들기](/guide/ht-pipelines)에서 본 원칙이 여기서도
그대로입니다.

```
[완료 조건]
- 수집 건수가 0이면 결과 파일을 쓰지 말고 실패로 알릴 것
- 평소 건수의 절반 미만이면 경고와 함께 남길 것
```

## HyperTeams에서 돌리기

수집 자체는 판단이 없는 작업이라 `task`가 아니라 **`terminal`**이 맞습니다.
정해진 명령이 정해진 형식으로 저장하고 끝납니다.

```
매일 05:30  ·  kind: terminal
node collect.mjs
```

- 스크립트와 결과물은 [작업 폴더](/guide/ht-working-directory) 안에 둡니다.
  브라우저가 뜨고 파일을 쓰는 작업이라 활동 범위를 좁혀 두는 편이 안전합니다.
- 결과 해석과 가공은 다음 단계 `task`에 맡깁니다. 수집과 판단을 한 칸에 섞으면
  실패했을 때 어느 쪽이 문제인지 알 수 없습니다.
- 헤드리스 브라우저는 메모리를 많이 씁니다. `maxConcurrency`를 낮게 두고
  시작하세요.

## 정리

1. API → 정적 HTML → 브라우저. **순서대로** 확인하고 마지막에만 Crawlee.
2. 해도 되는 일인지가 기술보다 먼저. 스텔스는 그 문제를 풀어 주지 않습니다.
3. 배선은 `launchContext.launcher`.
4. Crawlee 지문과 스텔스를 **겹치지 마세요**. 겹치면 더 눈에 띕니다.
5. 깨질 것을 전제로 만들고, 0건일 때 반드시 알리게 하세요.
