@anycrawl/scrape 등 버전 1.0.0-beta.26 · 확인 시점 GitHub 3.3k stars · 소스는 2026-07-18 커밋 f2e0bc8 기준 클론해 직접 분석)
POST /v1/scrape에 URL 하나를 보내면 몇 초 안에 제목·마크다운·메타데이터·스크린샷 등을 JSON으로 돌려받는다. 내부적으로는 요청이 잡(Job)으로 변환돼 Redis 큐에 들어가고, 별도 프로세스(워커)가 이를 꺼내 cheerio(정적 HTML)·Playwright·Puppeteer(둘 다 실제 브라우저) 중 하나로 실행한 뒤, 완료되면 API가 그 결과를 폴링해 응답한다.
같은 파이프라인 위에 /v1/crawl(사이트 전체 순회), /v1/search(구글 등 SERP 수집), /v1/map(사이트맵 기반 URL 목록화), 그리고 json_options를 통한 LLM 구조화 추출까지 쌓여 있다.
README만 보면 "스크래핑 라이브러리"처럼 읽히지만, 실제 저장소 구조는 완전한 SaaS 백엔드의 모양을 하고 있다. API 키 발급·크레딧 차감(billingLedger), 웹훅 구독/재전송, cron 기반 예약 작업(scheduledTasks), 그리고 웹페이지 변경 감지(모니터)까지 DB 스키마 레벨에 1급 시민으로 존재한다. 즉 anycrawl.dev가 파는 호스팅 서비스와 자가 호스팅용 오픈소스가 같은 코드베이스다 — Docker Compose 한 줄로 로컬에 그대로 띄울 수 있다.
formats: ["markdown", "json", "screenshot", ...] 옵션 하나로 처리한다.2026년 "LLM을 위한 웹 크롤러" 카테고리는 이미 붐비는 시장이다 — 호스팅 중심의 Firecrawl, 파이썬 진영의 Crawl4AI, 그리고 AnyCrawl이 나란히 언급된다. AnyCrawl의 GitHub 소개 문구조차 "Google/Bing/Baidu 등에서 구조화된 SERP를 추출한다"고 홍보하지만, 실제 오픈소스로 공개된 packages/search를 열어보면 직접 구현된 건 Google(cheerio로 google.com/search HTML을 직접 파싱)과 SearXNG(자체 호스팅 메타서치, 운영자가 붙여둔 엔진에 따라 Bing·DuckDuckGo 등을 간접 커버) 클라이언트뿐이고, Bing·Baidu 자체 파서는 없다 — 대신 ACEngine.ts라는 호스팅형 멀티 엔진 백엔드로 위임하는 얇은 HTTP 클라이언트가 있다. README를 그대로 믿지 않고 소스로 확인해야 하는 대표적인 사례다.
| 축 | Firecrawl | Crawl4AI | AnyCrawl |
|---|---|---|---|
| 언어/런타임 | Node/TS (호스팅 중심) | Python (라이브러리) | Node/TS, pnpm+Turborepo 모노레포 |
| 실행 구조 | API 서버 + 워커 | 단일 프로세스 async 라이브러리 | API(Express) + 엔진별 분산 큐/워커(BullMQ+Redis) |
| 엔진 선택 | 내부 브라우저 풀 | Playwright 중심 | cheerio·Playwright·Puppeteer 3종 + engine:"auto" 자동판별 |
| 안티봇 | 호스팅 쪽 블랙박스 | 기본 스텔스 플러그인 | Cloudflare Turnstile CDP 솔버 + 2captcha 연동이 소스로 공개 |
| 부가 기능 | Extract·Search(호스팅) | 주로 라이브러리 단위 | 예약 작업·웹훅·변경 감지 모니터·크레딧 원장이 DB 스키마로 내장 |
| 자가 호스팅 | 오픈소스 버전 별도 관리 | pip install 하나로 시작 | Docker Compose 한 줄, SQLite 기본값이라 DB 서버 불필요 |
Crawl4AI가 "혼자 다 하는 팔방미인 프리랜서"라면, AnyCrawl은 "부서가 나뉜 회사"다. 프리랜서는 한 프로세스 안에서 요청부터 렌더링까지 순서대로 처리하지만, 회사는 접수팀(API)·현장팀(엔진 워커)·경리팀(크레딧/DB)·감시팀(모니터)이 따로 있고 우편함(Redis 큐)으로 업무를 주고받는다. 느리게 보일 수 있지만, 현장팀 중 한 명이 쓰러져도(브라우저 크래시) 회사 전체가 멈추지 않는다.
① 엔진 3종 + 자동판별. engine: "auto"로 보내면 서버가 먼저 가벼운 HTTP GET으로 HTML을 5초 안에 받아 "JS가 꼭 필요한가"를 채점한 뒤 cheerio/Playwright 중 하나로 라우팅한다(4장에서 코드로 확인). README에는 나오지 않는, 소스에만 있는 기능이다.
② 진짜로 동작하는 안티봇 대응. Cloudflare Turnstile을 CDP로 감지하고 2captcha로 풀며, 성공한 cf_clearance 쿠키를 캐싱해 재사용한다. 프록시도 "일반(base)"과 "스텔스(stealth)" 두 단계로 나눠 자동/수동 승급시킨다.
③ "스크래퍼"를 넘어선 제품 범위. 예약 작업(cron)·웹훅 구독·모니터(주기적으로 재방문해 변경분만 diff)·사용자 커스텀 스크립트를 위한 샌드박스까지 — Firecrawl이 최근 붙이기 시작한 기능들을 처음부터 DB 스키마 레벨에서 준비해 뒀다.
④ MCP 서버 지원. anycrawl-mcp라는 별도 패키지(npx -y anycrawl-mcp)와 호스팅 엔드포인트(mcp.anycrawl.dev/{API_KEY}/mcp)로 Claude Desktop·Cursor·VS Code에서 바로 크롤링 도구를 쓸 수 있다 — 단, 이 MCP 서버 자체의 소스코드는 이 모노레포 안에 없고 문서(apps/docs)만 존재한다는 점은 분석 중 확인한 사실이다.
저장소는 pnpm + Turborepo 모노레포다. 앱은 2개(apps/api, apps/docs), 패키지는 9개(packages/*)로 나뉘어 있고, TypeScript 소스만 221개 파일·테스트를 제외하고도 약 5.6만 줄에 달한다.
| 레이어 | 선택 | 비고 |
|---|---|---|
| 런타임 | Node.js ≥20 + TypeScript(ESM) | type: "module", tsx로 개발 서버 구동 |
| 웹 프레임워크 | Express 5 | 미들웨어: Auth · CheckCredits · DeductCredits · Log |
| 검증 | Zod | 요청 스키마 → 타입 → (docs에서) OpenAPI까지 한 소스로 생성 |
| 라우트 | scrape·crawl·search·map·scheduled-tasks·webhooks·monitors | 8개 컨트롤러, 총 3,300여 줄 |
| 레이어 | 선택 | 역할 |
|---|---|---|
| 크롤링 프레임워크 | Crawlee 3.x (Apify) | CheerioCrawler/PlaywrightCrawler/PuppeteerCrawler, 요청 큐, 세션 풀 |
| 브라우저 런타임 | CloakBrowser 0.3.30 | Playwright/Puppeteer 실행을 감싸는 안티디텍션 브라우저 레이어 (Node ≥20 필요) |
| 정적 파서 | cheerio + html-to-text | JS 없이 HTML을 그대로 파싱 — 가장 빠른 엔진 |
| 작업 큐 | BullMQ + Redis(ioredis) | 엔진별·작업유형별로 큐를 쪼갬(scrape-cheerio 등) |
| DB 클라이언트 | Drizzle ORM + better-sqlite3 / pg | 잡·크레딧·웹훅·모니터 등 16개 테이블 |
| 안티봇 | CDP Turnstile 솔버 + 2captcha SDK | Cloudflare Turnstile 자동 해결 |
| 스케줄링 | node-cron + cron-parser | 예약 작업의 반복 실행 계산 |
| 기타 | @apify/utilities · minimatch · nodemailer | URL 패턴 매칭, 알림 메일 등 |
Vercel AI SDK(ai v5) 위에 generateObject·streamObject로 구조화 추출을 구현했다. @ai-sdk/openai·@ai-sdk/openai-compatible·@openrouter/ai-sdk-provider를 함께 등록해 두어, OpenAI뿐 아니라 OpenAI 호환 엔드포인트(Atlas Cloud의 DeepSeek 등)·OpenRouter 모델을 DEFAULT_LLM_MODEL=atlascloud/deepseek-v3 같은 환경변수 한 줄로 바꿔 낄 수 있다. 토큰 계산에는 tiktoken을 쓰고, 긴 페이지는 TextChunker로 분할한다.
Drizzle ORM으로 SQLite(자가 호스팅 기본값, 서버 설치 불필요)와 PostgreSQL(docker-compose.pg.yml, 스케일용) 스키마를 나란히 관리한다. 테이블은 jobs·jobResults·billingLedger·templates·scheduledTasks·taskExecutions·webhookSubscriptions·webhookDeliveries·pageCache·mapCache·monitors·monitorSnapshots·monitorChanges 등 16개.
| 도구 | 용도 |
|---|---|
| Docker (엔진별 이미지 3종) | Dockerfile.cheerio/.playwright/.puppeteer — 브라우저가 필요없는 cheerio 이미지는 훨씬 가볍다 |
| 루트 Dockerfile("All-in-One") | redis-server + supervisord로 API·워커·Redis를 한 컨테이너에서 실행 (빠른 체험용) |
| Turborepo + pnpm workspace | 모노레포 빌드 캐시·태스크 그래프 |
| Jest + ts-jest | 패키지별 단위 테스트(__tests__/ 다수) |
| k6 | scripts/k6.js — 부하 테스트 스크립트 내장 |
| Fumadocs + zod-openapi | Zod 스키마에서 OpenAPI 문서를 자동 생성해 docs 사이트에 반영 |
BaseEngine은 이 위에 얹힌 한 겹의 커스텀 레이어다.AnyCrawl의 설계를 이해하려면 요청 하나의 여정을 따라가 보는 게 가장 빠르다. POST /v1/scrape가 들어오면 무슨 일이 벌어지는가?
EngineQueueManager는 cheerio·playwright·puppeteer 각각에 대해 독립된 BullMQ 큐 + 워커를 만든다. scrape-*와 crawl-*도 따로 나뉜다. 이렇게 쪼개는 이유는 배포 유연성 때문이다 — docker-compose.yml을 보면 scrape-cheerio·scrape-playwright·scrape-puppeteer가 각각 별도 컨테이너(각자 다른 Dockerfile.cheerio/.playwright/.puppeteer)로 뜬다. 브라우저가 필요없는 cheerio 컨테이너는 이미지가 훨씬 가볍고, 무거운 Playwright/Puppeteer 워커만 따로 스케일아웃할 수 있다.
// managers/Worker.ts — BullMQ Worker는 그냥 이름+핸들러+concurrency
new Worker(name, async (job) => { return await jobHandler(job); }, {
connection: Utils.getInstance().getRedisConnection(),
concurrency: 50,
});
engines/EngineFactory.ts는 BaseEngineFactory 추상 클래스 하나로 3개 엔진 팩토리(cheerio/playwright/puppeteer)의 공통 로직(기본 옵션 병합, 프록시 설정 주입, launchContext 병합)을 통일한다. 각 구체 팩토리는 "어느 모듈에서 어느 클래스를 동적 import할지"만 다르다.
export class EngineFactoryRegistry {
private static factories = new Map();
static {
this.register('cheerio', new CheerioEngineFactory());
this.register('playwright', new PlaywrightEngineFactory());
this.register('puppeteer', new PuppeteerEngineFactory());
}
static async createEngine(engineType, queue, options) {
const factory = this.factories.get(engineType);
if (!factory) throw new Error(`No factory registered for engine type: ${engineType}`);
return factory.createEngine(queue, options);
}
}
새 엔진(예: 자체 HTTP/2 클라이언트)을 추가하려면 BaseEngine을 상속하고 팩토리를 하나 더 등록하면 끝이다 — 클라이언트 코드(컨트롤러·워커)는 엔진 타입 문자열만 알면 된다.
utils/autoEngine.ts는 README에는 없는, 소스를 읽어야만 보이는 기능이다. 가벼운 HTTP GET으로 HTML을 받아 다음과 같은 점수를 매긴다.
// 강한 신호(+3): 비어있는 루트 div, 본문 텍스트가 100자 미만인데 HTML은 2000자 초과,
// noscript 안에 의미있는 폴백 콘텐츠가 있음
if (/id=["'](?:root|app|__next|__nuxt)["'][^>]*>\s*<\/div>/i.test(rawHtml)) score += 3;
// 중간 신호(+1~2): __NEXT_DATA__ · __NUXT__ · data-reactroot 같은 프레임워크 흔적,
// script 태그가 10개 넘는데 보이는 텍스트는 500자 미만
// 상쇄 신호(-1~-3): 본문 텍스트가 이미 1000자 넘게 존재하면 오히려 JS가 필요없다고 판단
const engine = analysis.jsRequired ? "playwright" : "cheerio";
cache.set(domain, { engine }); // 도메인 단위로 캐싱 — 같은 사이트는 다음부터 재판별 안 함
즉 "이 페이지가 React/Vue SPA 껍데기인가, 이미 서버에서 렌더링된 페이지인가"를 휴리스틱 점수로 판정해 불필요한 브라우저 실행을 피한다. 실무에서 그대로 가져다 쓸 수 있는 실용적인 알고리즘이다.
ChallengePlugin 인터페이스(onPreNavigation 훅 하나)를 구현한 CloudflareChallengeHandler가 실제로 하는 일:
stealth일 때만 동작(일반 모드는 그냥 통과)cf_clearance 쿠키가 있으면 페이지 컨텍스트에 먼저 주입해 재도전 자체를 생략CDPTurnstileSolver가 CDP로 challenge 파라미터를 추출해 2captcha API에 풀이를 요청createCommonHandlers의 retrySolvedChallengeWithReload는 챌린지를 풀었다는 상태(challengeState.solved)만 믿지 않는다. 풀이 직후 응답이 여전히 403이면 페이지를 한 번 더 reload해 최종 상태 코드를 재확인한다. "챌린지 통과"와 "실제 콘텐츠 로드 성공"이 항상 같지 않다는 걸 실전에서 겪고 넣은 방어 코드로 보인다.
resolveProxyModeWithFallback은 요청의 proxy 값에 따라 "시도할 프록시 풀의 순서"를 배열의 배열로 반환한다.
if (proxyValue === 'auto') {
// 1티어: 저렴한 base 프록시 먼저 시도, 실패하면 2티어: stealth로 자동 승급
if (baseProxyUrls.length > 0) {
return stealthProxyUrls.length > 0
? [baseProxyUrls, stealthProxyUrls]
: [baseProxyUrls];
}
}
stealth로 명시하면 반대로 스텔스 프록시를 먼저 쓰고 실패 시 base로 내려간다. 여기에 도메인 단위 캐시(ProxyCacheManager)를 얹어, 한 번 stealth가 필요하다고 판정된 도메인은 이후 요청에서 바로 stealth로 시작한다 — 매번 재시도-실패-승급을 반복하지 않는다.
브라우저 엔진(Playwright/Puppeteer)은 페이지마다 CDP 세션을 열어 Network.setBlockedURLs로 광고 도메인을 차단하고, Fetch.enable로 이미지·미디어·폰트 요청을 가로챈다. 스크린샷 포맷이 필요한 요청이면 이미지는 통과시키고 미디어만 막는 식으로 요청 목적에 따라 차단 규칙을 동적으로 바꾼다. 이 모든 처리가 페이지가 실제로 리소스를 내려받기 전에 CDP 레벨에서 끝나므로, "다운로드 후 DOM에서 숨기기"보다 훨씬 적은 대역폭을 쓴다(BandwidthTracker가 실제 절감량을 잡별로 기록한다).
BaseEngine은 상속 트리를 깊게 파지 않고 DataExtractor·JobManager 같은 전담 클래스를 필드로 들고 위임한다ChallengePlugin) 설계string[][])로 프록시 승급을 표현 — 상태머신 대신 "시도할 순서 목록"으로 단순화처음 여는 사람에게: ① packages/scrape/src/engines/README.md(저장소 저자가 직접 쓴 설계 노트) → ② engines/EngineFactory.ts + Base.ts의 createCommonHandlers(요청 하나의 표준 처리 흐름) → ③ apps/api/src/controllers/v1/ScrapeController.ts(API가 큐에 뭘 어떻게 넣는지) → ④ managers/Proxy.ts와 challenges/cloudflare/(안티봇/프록시의 실제 구현) → ⑤ packages/db/src/db/schemas/SQLite.ts(제품이 실제로 뭘 기록하는지 한눈에). 이 5곳만 봐도 저장소의 핵심 로직 대부분이 손에 잡힌다.
cheerio·Playwright·Puppeteer는 API가 완전히 다른 라이브러리인데, BaseEngine이라는 공통 인터페이스(init()·run()·stop())로 통일하고 EngineFactoryRegistry가 문자열 키 하나로 알맞은 구현체를 골라준다. "실행 전략을 런타임에 문자열로 선택"해야 하는 모든 시스템(결제 PG사 여러 개, 알림 채널 여러 개 등)에 그대로 적용 가능한 패턴이다.
"브라우저를 켤지 말지"를 판단하려고 실제로 브라우저를 켜보지 않는다. 대신 5초짜리 가벼운 HTTP 요청 하나로 HTML을 받아 __NEXT_DATA__ 같은 프레임워크 흔적, 텍스트/스크립트 비율 같은 신호에 가중치를 매겨 점수화한다. "비싼 리소스를 쓰기 전에 값싼 신호로 필터링"하는 감각은 캐시 설계·오토스케일링 어디에나 응용된다.
Cloudflare 대응 로직을 onPreNavigation 훅 하나짜리 인터페이스로 뽑아뒀기 때문에, 다른 챌린지(예: hCaptcha)가 생겨도 새 플러그인만 추가하면 된다. CDP 세션으로 캡차 파라미터를 추출해 외부 솔버(2captcha)에 위임하고, 성공 쿠키를 캐싱해 재사용하는 흐름은 "브라우저 자동화 + 외부 API 위임"을 결합하는 실전 사례다.
ScrapeController는 캐시가 적중해도 실제로 크롤링한 것처럼 createJob·insertJobResult·completedJob을 호출하고 scrape.created→started→completed 웹훅을 그대로 발송한다. 캐시 경로와 정상 경로가 "이후 처리(과금·웹훅·통계)" 관점에서 동일한 모양이 되도록 맞춘 것 — 캐싱을 나중에 끼워 넣을 때 흔히 놓치는 부분을 미리 설계해 둔 사례다.
template-client의 사용자 커스텀 스크립트는 3단계로 방어된다 — ① 실행 전 정규식으로 require·eval·process·__proto__ 등 위험 패턴을 사전 차단(DANGEROUS_PATTERNS), ② Node.js 내장 vm 모듈로 격리된 컨텍스트에서 실행, ③ 호스트가 제공하는 API도 allowedPageMethods 화이트리스트로 제한. "사용자가 로직을 넣게 해주되 시스템은 안전하게" 만드는 3중 방어 조합을 그대로 참고할 수 있다.
packages/libs/src/types/*Schema.ts의 Zod 스키마가 (1) API 요청 검증, (2) TypeScript 타입, (3) apps/docs의 OpenAPI 문서 생성까지 세 가지 역할을 동시에 한다. 스키마와 문서가 따로 놀아 어긋나는 흔한 문제를 "진실의 원천을 하나로" 만들어 원천 차단한 설계다.
cheerio 엔진만 쓸 거라면 노트북 한 대로 충분하다. Playwright/Puppeteer 엔진을 실제로 돌리려면 Chromium 프로세스가 페이지마다(또는 컨텍스트마다) 뜨는 만큼 메모리 여유가 필요하다.
| 항목 | 요구/권장 |
|---|---|
| Node.js | 20 이상 필수(engines.node >=20) — CloakBrowser가 Node 20+를 요구 |
| 패키지 매니저 | pnpm 10.12.4(packageManager 고정), Turborepo로 태스크 오케스트레이션 |
| Redis | 필수. BullMQ 큐가 전부 Redis 위에서 동작 — 없으면 아예 기동 불가 |
| 데이터베이스 | SQLite(기본값, 파일 하나로 자가 호스팅 부담 없음) 또는 PostgreSQL(docker-compose.pg.yml, 스케일용) |
| 브라우저 바이너리 | Playwright/Puppeteer 엔진용 Chromium — Docker 이미지 빌드 시 cloakbrowser install·playwright install chromium으로 사전 설치 |
| 동시성 | 엔진 워커 기본 concurrency: 50 — 리소스가 적은 환경에선 ANYCRAWL_MAX_CONCURRENCY로 낮추길 권장 |
| 선택 사항 | S3 호환 스토리지(페이지 캐시·스크린샷), 2captcha API 키(Turnstile 자동 해결), 프록시 URL(base/stealth) |
| OS | Docker 이미지는 node:20-bookworm-slim(Linux) 기반. 로컬 개발은 macOS/Linux/Windows 모두 지원(플랫폼별 Chromium 실행 인자 분기 있음) |
| 포트 | API 기본 8080, Redis 기본 6379 |
배포 형태는 두 가지다. ① All-in-One 단일 컨테이너(루트 Dockerfile) — supervisord가 Redis·API·워커를 한 컨테이너 안에서 관리해 빠르게 체험하기 좋다. ② docker-compose 다중 컨테이너 — API/Redis/엔진별 워커가 각자 컨테이너로 분리돼 실제 운영 스케일링에 맞다.
docker compose up으로 redis·api·scrape-cheerio·scrape-playwright·scrape-puppeteer를 띄운다. 같은 URL(예: JS로 렌더링되는 SPA 사이트)에 engine을 cheerio/playwright/puppeteer로 바꿔가며 /v1/scrape를 호출해, 응답 시간과 markdown 내용의 완성도가 어떻게 다른지 직접 비교한다.
engine: "auto"로 정적 사이트와 SPA 사이트를 각각 요청해보고, 워커 로그에 찍히는 [AutoEngine] <domain> -> <engine> (score=... reasons=...) 메시지를 확인한다. utils/autoEngine.ts의 점수 조건 하나를 직접 바꿔보고(예: 임계값 3 → 5) 판정이 어떻게 달라지는지 실험한다.
ai.config.example.json을 참고해 OpenAI 호환 프로바이더(로컬 Ollama나 OpenRouter도 가능)를 연결한 뒤, json_options.schema에 원하는 JSON 스키마를 넣어 /v1/scrape를 호출한다. packages/ai/src/agents/LLMExtract.ts의 normalizeSchema가 스키마를 어떻게 변형하는지 로그로 확인해본다.
core/EngineConfigurator.ts의 resourceBlockingHook에 특정 트래커/애널리틱스 도메인을 추가로 차단하는 규칙을 넣고, BandwidthTracker 로그로 대역폭 절감량이 실제로 줄어드는지 비교 측정한다.
packages/template-client/src/constants/security.ts의 DANGEROUS_PATTERNS와 sandbox/index.ts를 읽고, preNav 단계에서 쓸 수 있는 안전한 헬퍼 함수(예: 랜덤 대기 시간 계산기)를 하나 추가해본다. 추가한 API가 화이트리스트를 우회해 위험한 동작을 하지 않는지 스스로 점검하며, "안전한 확장 지점을 설계한다"는 감각을 익힌다.
Crawlee 공식 문서로 CheerioCrawler/PlaywrightCrawler/PuppeteerCrawler의 요청 큐·세션 풀·재시도 모델을 익히고, packages/scrape/src/engines/Base.ts의 createCommonHandlers를 줄 단위로 읽는다. "여러 엔진을 하나의 인터페이스로 감싸는 법"이 이번 주 목표.
BullMQ 공식 문서로 Producer/Consumer 분리, 재시도·백오프, repeatable job(cron) 개념을 익히고, managers/Queue.ts·managers/Scheduler.ts를 대조하며 읽는다. Redis 자료구조(Sorted Set 기반 지연 큐)가 왜 큐 라이브러리에 적합한지도 함께 살펴본다.
Cloudflare Turnstile의 동작 원리, CDP(Chrome DevTools Protocol)의 Network/Fetch 도메인, 2captcha 같은 캡차 해결 서비스의 API 흐름을 익힌다. challenges/와 managers/Proxy.ts를 함께 읽으며 "언제 어떤 방어가 필요한가"를 판단하는 감각을 기른다.
Vercel AI SDK의 generateObject/streamObject, JSON Schema ↔ Zod 변환, 비전 모델 기반 OCR을 packages/ai/src/agents/로 실습한다. 토큰 계산(tiktoken)과 긴 문서 청킹 전략(TextChunker)도 함께 본다.
Node.js vm 모듈의 격리 한계(진짜 보안 경계가 아니라는 점 포함)를 학습하고, template-client의 정적 검사+런타임 화이트리스트 조합을 분석한다. 마지막으로 Zod → OpenAPI 자동 생성(zod-openapi, fumadocs-openapi) 흐름을 apps/docs에서 확인하며 스키마 우선 API 설계를 정리한다.
HTMLTransformer.ts가 Firecrawl과 유사한 규칙 목록으로 구현.vm 모듈 + 정규식 사전검사 + 메서드 화이트리스트로 안전하게 실행하는 구조.monitorChanges)으로 구현.anycrawl-mcp)로 배포되며 이 모노레포에는 문서만 있다.generateObject 구조화 추출