kreuzcrawl(조직명 kreuzberg-dev)이었고, 지금은 crawlberg(조직명 xberg-io)로 개명되어 이 딥다이브가 클론을 시도한 주소는 새 주소로 301 리다이렉트된다.
f1a30b1 · Rust 소스 183개 파일 약 71,880줄)
scrape(url) 한 번이면 본문 마크다운, 메타데이터(Open Graph·Twitter Card·Dublin Core·JSON-LD), 링크·이미지·피드 목록이 한 번에 돌아온다. crawl(url)은 같은 파이프라인을 링크 그래프 전체로 확장해 깊이·페이지 수 제한 안에서 사이트를 통째로 훑는다. 자바스크립트가 필요한 페이지나 WAF(방화벽) 차단 페이지는 자동으로 브라우저 렌더링 단계로 승격(escalate)된다.
이 모든 기능은 딱 7개의 공개 함수(create_engine·scrape·crawl·map_urls·interact·batch_scrape·batch_crawl) 위에 얹혀 있고, CLI·MCP 서버·REST 서버·14개 언어 바인딩이 전부 이 좁은 표면을 그대로 재사용한다.
저장소 루트는 crates/(Rust 코어 + 10개의 바인딩 브릿지 크레이트)와 packages/(Rust crate가 아닌 나머지 언어 패키지 소스 11종), 그리고 fixtures/(테스트 픽스처 322개)·test_apps/(언어별 e2e 스모크 앱 16종)·docs-site/(공식 문서)로 구성된 큰 모노레포다. 코어 크레이트 이름은 crawlberg이고, 여기서 만드는 결과물(스크레이프 결과, 크롤 결과, 사이트맵 지도)은 REST API·MCP 서버·CLI·모든 언어 바인딩에서 완전히 동일한 스키마로 나온다.
bindings.rs라는 좁은 Rust 함수 표면 하나만 사람이 관리하고, alef라는 사내 도구가 이걸 읽어 14개 언어의 바인딩 코드·타입 정의·문서·테스트 앱까지 전부 자동 생성한다. 기능 추가는 Rust 코어 한 곳에서 끝난다.git clone https://github.com/kreuzberg-dev/kreuzcrawl.git은 문제없이 성공하지만, 브라우저로 그 주소를 열면 HTTP 301로 github.com/xberg-io/crawlberg로 리다이렉트된다. CHANGELOG.md의 1.0.0-rc.1 항목에 개명 내역이 그대로 적혀 있다 — "프로젝트 이름을 kreuzcrawl에서 crawlberg로 변경. 크레이트명·모든 언어 패키지·C FFI 심벌 접두사(kcrawl_*→cberg_*)·Go 모듈·문서 도메인이 함께 바뀜"과 "kreuzberg 네임스페이스를 xberg로 리브랜딩. npm 스코프 @kreuzberg→@xberg-io, Maven groupId dev.kreuzberg→io.xberg. 법인명(Kreuzberg, Inc.)은 그대로."
왜 바꿨을까 짐작할 단서도 있다: 이 프로젝트의 저작자(Na'aman Hirschfeld)는 같은 이름의 문서 추출 라이브러리 "Kreuzberg"(PyPI, 저자 이메일도 @kreuzberg.dev)도 만들고 있고, 그 프로젝트 역시 같은 xberg-io CDN 배너·같은 다국어 배지 구성을 쓴다. 즉 "문서추출 Kreuzberg"와 "크롤러 Kreuzcrawl(구 Kreuzberg 계열)"이라는 두 자매 프로젝트 이름이 겹쳐서 크롤러 쪽을 Crawlberg로 새로 지은 것으로 보인다. TrendShift의 트렌딩 스냅샷이 이 개명 전 이름을 아직 들고 있는 셈이다. 이하 본문에서는 실제로 클론되는 소스 기준으로 crawlberg라는 크레이트/함수명을 그대로 쓰되, 저장소를 가리킬 땐 원래 지시받은 이름 kreuzcrawl도 병기한다.
"웹사이트를 LLM이 먹기 좋은 마크다운으로 바꿔주는" 크롤러/스크레이퍼는 이미 붐비는 시장이다. Firecrawl(SaaS 우선, Node/TS 코어), Crawl4AI(Python 전용, Playwright 기반 OSS), anycrawl(Node/TS, Firecrawl류 셀프호스트 대안) 모두 비슷한 약속 — "URL을 던지면 깨끗한 마크다운이 나온다" — 을 한다. kreuzcrawl이 이 무리에서 눈에 띄는 지점은 결과물이 아니라 "어떤 언어로도 같은 엔진을 네이티브로 박아 쓸 수 있다"는 배포 전략이다.
| 축 | Firecrawl / Crawl4AI / anycrawl | kreuzcrawl(crawlberg) |
|---|---|---|
| 코어 구현 언어 | Node/TS(Firecrawl·anycrawl) 또는 Python(Crawl4AI) — 언어 하나에 종속 | Rust 단일 코어. 14개 언어는 전부 이 코어를 네이티브로 임베드 |
| 다른 언어 지원 방식 | 대부분 REST API 클라이언트(HTTP 래퍼)이거나 아예 미지원(Crawl4AI는 Python 전용) | FFI(NAPI-RS·PyO3·JNI·ext-php-rs·Panama FFM 등)로 진짜 네이티브 확장 — 별도 서버 프로세스 없이 같은 프로세스 안에서 실행 |
| 브라우저 렌더링 | Playwright/Puppeteer로 실제 브라우저 프로세스를 항상 띄움 | 1차는 자체 제작 경량 브라우저(V8+html5ever), 필요할 때만 진짜 헤드리스 Chrome으로 승격 |
| 안티봇 대응 | 대개 매니지드 서비스 내부 로직(비공개) — 셀프호스트판은 상대적으로 단순 | 오픈소스 TOML 지문 DB(76종) + 5단계 에스컬레이션 전략을 설정으로 직접 제어 |
| REST API | 각자 자체 스펙(Firecrawl 스펙이 사실상 업계 참조점처럼 쓰임) | Firecrawl v1 API와 호환되는 서버를 자체 내장 — 기존 Firecrawl SDK가 가리키는 주소만 바꿔도 셀프호스트 대체 가능 |
| MCP 서버 | 커뮤니티 unofficial 래퍼가 따로 존재하는 경우가 많음 | 코어에 rmcp 기반 MCP 서버 내장(stdio + streamable-HTTP) |
Crawl4AI가 "Python 개발자를 위한 잘 만든 스크레이퍼"라면, kreuzcrawl은 "크롤러 부품 공장"이다. 전자는 완성차 한 대를 아주 잘 만들지만 그 차는 Python 도로에서만 달릴 수 있다. 후자는 엔진(Rust 코어) 설계도 하나를 두고, 그 설계도를 읽어 각 나라(언어) 규격에 맞는 엔진을 자동으로 찍어내는 공작기계(alef)를 갖고 있다. Go 개발자든 Swift 개발자든 "수입산 부품을 API로 원격 호출"하는 게 아니라 자국 규격 엔진을 그대로 차체에 얹는다.
① Firecrawl API 호환이라는 실용적 한 수. docs-site의 아키텍처 문서는 api feature를 "Firecrawl v1-compatible REST server"라고 명시한다. 이미 Firecrawl 클라이언트 SDK·LangChain/LlamaIndex 커넥터를 쓰고 있는 팀이라면, 서버 주소만 자체 호스팅한 kreuzcrawl로 바꿔도 대부분 그대로 동작한다는 뜻 — 생태계에 무임승차하는 영리한 선택이다.
② 안티봇 대응을 "사다리"로 설계. 무조건 무거운 브라우저부터 던지지 않는다. 순정 HTTP → WAF 지문 판정 → (설정에 따라) 우회 벤더 API 또는 자체 경량 브라우저 → 그래도 안 되면 진짜 Chrome, 순서로 비용이 싼 수단부터 에스컬레이션한다. 대부분의 스크래핑 도구가 "JS 필요하면 무조건 Playwright"인 것과 대비된다.
③ MCP·REST·CLI·바인딩이 코드 한 줄도 안 겹치게 재사용. 네 가지 접근 방식이 전부 bindings.rs의 같은 7개 함수를 호출한다. 기능 패리티 문제(API에만 있고 CLI엔 없는 옵션 같은 것)가 구조적으로 생기기 어렵다.
④ MIT 라이선스, 그리고 오픈코어 비즈니스. 코어·모든 바인딩·CLI·API·MCP가 전부 MIT다. README는 프록시 풀, 튜닝된 봇 우회, 인증 세션, 스케줄링, 과금 같은 "매니지드 확장 기능"은 별도의 xberg-enterprise 저장소에 있다고 명시한다 — 코어는 완전히 열어 두고 운영 편의 기능으로 수익화하는 전형적인 오픈코어 모델이다.
LICENSE 파일과 워크스페이스 Cargo.toml의 license = "MIT"를 직접 확인한 결과 실제로는 MIT다. Elastic License 2.0 같은 소스공개 라이선스는 "소스는 볼 수 있지만 특정 상업적 사용(예: 매니지드 서비스로 재판매)은 금지"하는 제약이 있는 반면, MIT는 그런 제약이 전혀 없는 완전한 오픈소스 라이선스다 — 상업적 이용·수정·재배포가 모두 자유롭다. README/캐시된 페이지보다 소스 파일을 직접 열어봐야 하는 이유를 보여주는 사례다.워크스페이스 루트 Cargo.toml은 edition = "2024", rust-version = "1.91", license = "MIT"로 선언돼 있다. 코어 크레이트 crawlberg는 feature flag로 기능을 잘게 쪼개 놓아, 필요한 것만 골라 컴파일할 수 있다.
crates/crawlberg)| 레이어 | 선택 | 비고 |
|---|---|---|
| 비동기 런타임 | Tokio(full) | 모든 크레이트가 공유. wasm32 타겟은 조건부로 제외 |
| HTTP 클라이언트 | reqwest 0.13(cookies·gzip·brotli) | 전역 리다이렉트 정책은 Policy::none() — 리다이렉트는 엔진이 직접 제어(SSRF 재검증 위해) |
| 서비스 미들웨어 | tower 0.5 + tower-http 0.7 | 레이트리밋 → 캐시 → UA 로테이션 → fetch 순 레이어링 |
| HTML 파싱 | tl(astral-tl 포크) | 제로카피에 가까운 고속 HTML5 파서. html5ever·lol_html은 코어에서 쓰지 않는다(아래 4장 참고) |
| 마크다운 변환 | html-to-markdown-rs | Markdown/Plain/Djot 3가지 출력 포맷, 표·인용·"fit content" 프루닝 포함 |
| 실브라우저 폴백 | chromiumoxide(CDP) | 진짜 헤드리스 Chrome을 원격 제어. 로컬 실행 또는 --browser-endpoint로 외부 CDP WS 접속 |
| 자체 제작 브라우저 | crawlberg-browser(별도 크레이트) | Chrome 설치 없이 V8+DOM만으로 JS 렌더링(아래 4장에서 자세히) |
| 스텔스 HTTP | wreq/wreq-util | Chrome 145 TLS(JA3) 지문 위장 — reqwest 포크 |
| WAF 탐지 | 자체 TOML 지문 DB + aho-corasick | 76개 룰(Cloudflare 등), arc-swap으로 핫리로드 |
| 3rd-party 우회 | crawlberg-bypass + saphyr(YAML) | Bright Data·ScrapingBee·Zyte 어댑터를 YAML로 선언 |
| MCP 서버 | rmcp(server·macros·stdio·streamable-http) | #[tool_router]/#[tool] 매크로로 선언적 정의 |
| REST API | axum 0.8 + utoipa(OpenAPI) | Firecrawl v1 호환 스펙 |
| 관측성 | OpenTelemetry(+OTLP gRPC) + tracing | xberg-enterprise와 스팬/메트릭 스키마 공유(주석에 명시) |
| AI 리서치 에이전트 | liter-llm + minijinja | ai feature. plan → crawl → synthesize 자율 루프 |
| 아카이빙 | 자체 WARC 1.1 writer | 표준 규격 그대로 구현(외부 크레이트 없이 직접 작성) |
README가 밝히는 지원 언어는 정확히 Rust, Python, Node.js, Ruby, Go, Java, Kotlin(Android), C#, PHP, Elixir, Dart, Swift, Zig, WebAssembly = 14개다. 각 언어가 코어를 "부르는" 방식은 언어 특성에 맞춰 전부 다르다.
| 언어 | 배포처 | 연결 메커니즘 |
|---|---|---|
| Python | PyPI crawlberg | PyO3 + maturin, abi3-py310(3.10+ 단일 wheel) |
| Node.js | npm @xberg-io/crawlberg | NAPI-RS(napi v3, cdylib) |
| WebAssembly | npm @xberg-io/crawlberg-wasm | wasm-bindgen |
| PHP | Composer xberg-io/crawlberg | ext-php-rs(진짜 Zend 확장으로 컴파일) |
| C/C++ | 공유 라이브러리 + cbindgen 헤더 | C FFI(cdylib+staticlib+rlib) |
| Ruby | RubyGems crawlberg | Magnus + rb-sys |
| Elixir | Hex crawlberg | Rustler NIF |
| Go | Go 모듈 github.com/xberg-io/crawlberg/packages/go | cgo로 C FFI 호출 |
| Java | Maven Central io.xberg.crawlberg:crawlberg | Java 25 Panama FFM(최신 Foreign Function & Memory API) |
| Kotlin(Android) | Maven Central io.xberg.crawlberg.android | Android AAR + JNI(.so) — Panama 대신 JNI를 쓰는 이유는 7장 참고 |
| C# | NuGet Crawlberg | .NET 10 P/Invoke |
| Dart | pub.dev crawlberg | Dart FFI |
| Swift | Swift Package Manager | C FFI 경유 |
| Zig | zig fetch --save | C FFI 경유(Zig의 강력한 C 상호운용 활용) |
npm에 올라가는 네이티브 바이너리는 @xberg-io/crawlberg-{darwin-arm64, darwin-x64, linux-arm64-gnu, linux-arm64-musl, linux-x64-gnu, linux-x64-musl, win32-arm64-msvc, win32-x64-msvc} 8종 — glibc/musl, x64/arm64, 3대 OS를 모두 사전 빌드해 optionalDependencies로 배포한다(Node 엔진 요구 버전은 >=22).
루트의 alef.toml(46KB)이 alef의 설정 파일이다. 언어별 DTO 스타일(Python은 dataclass, Java는 record, Go는 struct...), 패키지 매니저(Python은 uv, Node는 pnpm), CLI/MCP 문서 소스 경로, 심지어 .claude/skills·.codex/skills·.agents/skills·.github/skills에 뿌릴 에이전트용 스킬 문서 템플릿까지 이 한 파일이 정의한다. crawlberg-node/src/lib.rs·crawlberg-wasm/src/lib.rs 첫 줄은 전부 // This file is auto-generated by alef. DO NOT EDIT.다.
Docker — rust:1.91-bookworm로 빌드하고 debian:bookworm-slim에 스트립된 바이너리만 올리는 멀티스테이지 구성(docker/Dockerfile), 그 외 alpine·musl-build·musl-ffi·musl-nif용 Dockerfile이 따로 4개 더 있다. Task(go-task)가 16개 언어별 태스크파일(.task/languages/*.yml)과 버전동기화·문서생성·테스트앱·벤치마크 태스크를 오케스트레이션한다. cargo-machete(미사용 의존성 탐지)·cargo-deny(deny.toml)·워크스페이스 단위 clippy 린트로 품질을 관리하고, ai-rulez가 에이전트 설명 파일들을 동기화한다.
이 저장소의 설계를 이해하는 데는 두 다이어그램이면 충분하다. 하나는 런타임 흐름(URL 하나가 들어와서 결과가 나오기까지), 다른 하나는 빌드타임 흐름(Rust 소스 하나가 14개 언어 패키지로 불어나기까지)이다.
핵심은 EscalationStrategy 열거형이다 — None·BrowserOnly·BypassFirst·BypassOnly·BypassThenBrowser 5가지 값으로 "막히면 무엇부터 시도할지" 순서를 설정으로 고른다. 브라우저 백엔드도 BrowserMode::Auto/Always/Never/Stealth와 BrowserBackend::Native/Chromiumoxide 두 축으로 나뉜다. 즉 "JS 필요하면 무조건 Playwright 켠다"는 식이 아니라, 비용이 싼 수단부터 순서대로 시도하도록 촘촘하게 설계돼 있다.
engine/crawl_loop.rs의 content_density()는 HTML 문자열을 한 글자씩 훑으며 <...> 태그 바깥 글자 수만 센다. 전체 DOM을 파싱하지 않고 5줄짜리 태그 스트리핑만으로 "텍스트 밀도"를 근사한다 — SPA 껍데기(밀도 0.0~0.05)와 정상 콘텐츠 페이지(밀도 0.3+)를 구분하는 데는 이 정도면 충분하다는 실용적 트레이드오프. 정확도보다 속도가 중요한 1차 판정에 딱 맞는 선택이다.
이 파이프라인의 재미있는 지점은 alef가 만드는 게 저수준 FFI 글루 코드에서 그치지 않는다는 것이다. 예컨대 Node 바인딩의 JsBytes 래퍼 타입은 NAPI v3의 기본 Vec<u8> 역직렬화가 Array<number>만 받아들이는 한계를 피해 Buffer·Uint8Array·Array를 모두 받도록 만든 커스텀 타입인데, 이것도 alef가 생성한 코드 안에 들어 있다. WASM 바인딩의 WasmExtractionMeta 같은 #[wasm_bindgen] 래퍼 구조체도 마찬가지다 — 사람은 Rust 타입 하나만 설계하면, 각 언어의 관용적인 표현(idiomatic API)까지 alef가 대신 고민해준다.
CrawlEngine은 내부적으로 Arc<dyn Frontier>, Arc<dyn CrawlStrategy> 같은 트레이트 객체를 쓰는데, 이런 동적 디스패치 타입은 FFI 경계를 넘길 수 없다. 그래서 bindings.rs는 기본 구현체만으로 엔진을 구성하는 "설정 전용" 생성 경로(create_engine)와, 그 결과를 감싸는 불투명 핸들 CrawlEngineHandle을 따로 둔다. 언어 바인딩은 이 핸들만 주고받을 뿐 내부 트레이트 객체는 절대 보지 못한다 — "복잡한 내부는 Rust 안에 가두고, 바깥에는 단순한 손잡이만 내준다"는 FFI 설계의 정석이다.api::create_router(engine)는 axum 라우터 하나에 /v1/scrape·/v1/crawl(+상태조회/취소)·/v1/map·/v1/batch/scrape·/v1/download·/health·/version·/openapi.json을 REST로 노출하면서, 동시에 /mcp 경로에 crate::mcp::streamable_http_service(mcp_config)를 nest_service로 얹는다. 소스 주석이 재미있는 이유를 설명한다 — "MCP는 타임아웃/압축 미들웨어 바깥에 둔다. 오래 유지되는 SSE 세션이 그 미들웨어들 아래서 깨지기 때문". REST에는 요청 타임아웃(5분)·바디 크기 제한(10MB)·gzip 압축을 걸어도, 실시간 스트리밍이 필요한 MCP 세션에는 그 레이어를 우회시킨 것이다.
기본값은 사설/루프백/링크로컬/메타데이터/멀티캐스트/IPv6 유니크로컬 대역을 전부 거부(127.0.0.0/8, 10.0.0.0/8, 169.254.0.0/16 등)한다. 여기에 그치지 않고 ① DNS 리바인딩 방지(호스트명이 공인 IP와 사설 IP를 동시에 반환하면 거부), ② 30x 리다이렉트를 매 홉마다 재검증(첫 URL만 검사하고 끝내지 않음), ③ 1.0.4~1.0.5에서 추가된 헤드리스 브라우저 하위 요청까지 검증 — CDP의 Fetch 도메인을 가로채기(interception) 모드로 켜서, Chrome이 내부적으로 따라가는 리다이렉트·서브리소스 요청 하나하나까지 SSRF 정책을 통과해야 진행되도록 만들었다. "브라우저 폴백은 SSRF 사각지대"라는 흔한 취약점을 정면으로 틀어막은 사례다.
docs-site/src/content/docs/concepts/content-extraction.md는 "모든 HTML 파싱은 scraper::Html::parse_document로 수행된다"고 적어 두었지만, 실제 crates/crawlberg/src/html/*.rs·scrape.rs·map.rs를 열어보면 전부 tl(astral-tl) 크레이트를 쓴다. scraper 크레이트는 어디에도 의존성으로 등록돼 있지 않다. 공식 문서조차 실제 구현을 완전히 따라잡지 못한 셈 — README든 공식 docs든, 근거는 결국 클론한 소스에 둬야 한다는 이 프로젝트 작업 원칙을 스스로 증명해 준 사례다.
① src/lib.rs(feature gate로 전체 기능 지도 파악) → ② src/bindings.rs + traits.rs(공개 함수 7개와 트레이트 7개 — 이 저장소의 "헌법") → ③ engine/crawl_loop.rs(리다이렉트·에스컬레이션이 실제로 일어나는 곳) → ④ waf/mod.rs + net/ssrf.rs(안티봇·보안 설계) → ⑤ mcp/server.rs + api/router.rs(에이전트/HTTP 노출) → ⑥ crawlberg-browser/src/lib.rs(자체 브라우저에 관심 있다면) → ⑦ alef.toml(바인딩 생성 메커니즘이 궁금하다면, 다만 46KB라 각오할 것). 이 순서면 저장소의 핵심 설계를 대부분 손에 넣을 수 있다.
CrawlEngine은 이 7개 트레이트의 Arc<dyn Trait> 조합일 뿐이다. 기본 구현(인메모리 프론티어, BFS/DFS/BestFirst/Adaptive 전략, no-op 캐시 등)이 준비돼 있지만, 분산 큐가 필요하면 Frontier만, 커스텀 저장소가 필요하면 CrawlStore만 갈아끼우면 된다. "확장 포인트를 트레이트로 뚫어 두고 기본 구현은 따로 제공한다"는 Rust 라이브러리 설계의 교과서적인 예다.
bindings.rs + types.rs라는 좁은 표면만 관리하면 alef가 NAPI-RS·PyO3·wasm-bindgen·JNI·ext-php-rs·cbindgen 코드를 전부 찍어낸다. 14개 언어를 손으로 유지보수한다고 상상해 보면 이 설계가 왜 필요한지 바로 이해된다 — 기능 하나를 추가할 때마다 14곳을 고치는 대신 1곳만 고치면 나머지는 alef generate 한 번으로 끝난다. 사내 도구를 만들 정도로 이 문제에 진심이었다는 뜻이기도 하다.
EscalationStrategy 열거형 5가지(None/BrowserOnly/BypassFirst/BypassOnly/BypassThenBrowser)로 "막히면 무엇부터 시도할지" 순서를 데이터로 표현했다. 무조건 풀브라우저를 띄우는 대신, HTTP → WAF 판정 → (선택) 우회 벤더 API 또는 자체 경량 브라우저 → 최후에만 진짜 Chrome. 비용·속도·성공률 사이의 트레이드오프를 코드가 아니라 설정값 하나로 조정할 수 있게 만든 설계는 다른 도메인(재시도 정책, 캐시 계층, 알림 채널 폴백)에도 그대로 옮겨 쓸 수 있다.
보통 크롤러는 "HTTP냐 진짜 브라우저(Playwright/Selenium)냐" 둘 중 하나만 고른다. kreuzcrawl은 deno_core(V8)로 자바스크립트를 실행하고 html5ever+cssparser+selectors로 DOM/CSS를 흉내 낸 자체 제작 경량 브라우저(Obscura 포크)를 세 번째 선택지로 넣었다. Chrome 바이너리 설치가 필요 없고 훨씬 가볍지만, 스크린샷이 진짜 컴포지터 캡처가 아니라 "액션 이후 HTML을 재구성한 PNG 스냅샷"이라 픽셀 퍼펙트는 아니라는 한계도 문서에 정직하게 적혀 있다. 무엇을 얻고 무엇을 포기했는지가 명확한 좋은 트레이드오프 사례다.
WAF 탐지 규칙 76개가 Rust 코드가 아니라 rules/waf_fingerprints.toml에 산다. TomlClassifier는 이 규칙들을 ArcSwap에 담아, 파일 변경을 감지하면(500ms 디바운스로 에디터의 tmpfile-rename 저장 패턴까지 고려) 재배포 없이 룰만 갈아끼운다. "탐지 로직을 재컴파일 없이 바꾸고 싶다"는 요구를 만났을 때 참고할 만한 가장 단순한 해법이다.
독자적인 REST 스펙을 새로 설계하는 대신, 이미 생태계가 형성된 Firecrawl v1 API와 호환되는 서버를 내장했다. 이렇게 하면 사용자가 클라이언트 코드를 거의 바꾸지 않고도 자체 호스팅으로 전환할 수 있다 — API를 "발명"하기보다 "이미 검증된 계약을 재사용"하는 것도 어엿한 설계 전략이라는 걸 보여준다.
alef.toml의 [workspace.docs.skills] 섹션은 CLI(crawlberg-cli/src/main.rs)와 MCP 서버(mcp/server.rs)를 소스로 읽어 .claude/skills·.codex/skills·.agents/skills·.github/skills에 동일한 내용의 스킬 문서를 자동 합성한다. 사람이 "이 CLI는 이런 커맨드가 있다"는 문서를 손으로 쓰다가 실제 코드와 어긋나는(documentation drift) 흔한 문제를, 문서 생성 자체를 빌드 파이프라인에 편입시켜 원천 차단한 것이다.
순수 HTTP 스크레이핑만 쓴다면 일반 노트북으로 충분하다. 무거워지는 지점은 딱 하나 — 헤드리스 브라우저 폴백을 chromiumoxide 백엔드로 켰을 때 실제 Chrome 프로세스가 뜨는 경우다.
| 항목 | 요구/권장 |
|---|---|
| Rust(코어 빌드) | 1.91 이상, edition 2024. rust-toolchain.toml로 버전 고정 |
| Node.js(바인딩) | engines.node >= 22 — NAPI-RS v3 요구사항 |
| Python(바인딩) | 3.10 이상. abi3-py310 안정 ABI라 wheel 하나가 3.10+ 전부 커버 |
| Java(바인딩) | Java 25(Panama FFM 사용) — 데스크톱/서버용. Android는 별도로 JNI(.so) 사용 |
| .NET(바인딩) | .NET 10(P/Invoke) |
| 사전 빌드 플랫폼 | Linux x86_64/aarch64(glibc+musl), macOS(arm64+x64), Windows(x64+arm64) — npm 8종 네이티브 바이너리 기준 |
| 브라우저 폴백 · Native | Chrome 설치 불필요. 자체 V8(deno_core) 내장 — 가장 가벼운 선택지 |
| 브라우저 폴백 · Chromiumoxide | 로컬에 Chrome/Chromium 필요, 또는 --browser-endpoint ws://...로 원격 CDP 서비스에 접속 |
| GPU | 불필요 |
| 컨테이너 | ghcr.io/xberg-io/crawlberg(bookworm-slim 런타임) + alpine/musl 변형 4종 |
| 메모리 주의 | v1.0.6에서 map()/map_urls()의 대형 사이트맵 메모리 상한 버그 수정(#33) — 그 이전 버전은 대형 sitemap-index 사이트에서 OOM 위험이 있었다 |
REST API 서버(serve_api)나 MCP 서버는 상태를 갖지 않는 짧은 요청-응답 위주라 특별한 스펙이 필요 없지만, 동시 크롤 세션과 브라우저 풀(BrowserPool) 크기를 늘릴수록 메모리·파일 디스크립터 사용량이 선형으로 늘어난다는 점은 실제 운영 시 감안해야 한다.
Rust 툴체인이 있다면 cargo install --path crates/crawlberg-cli(또는 Docker 이미지)로 crawlberg 바이너리를 준비한다. crawlberg scrape https://example.com --format markdown과 --format json의 출력을 비교해보고, crawlberg crawl <seed> -d 2 -n 20으로 링크를 따라가는 크롤을 실행해 depth·max_pages가 실제로 어떻게 결과를 제한하는지 관찰한다.
pip install crawlberg와 npm i @xberg-io/crawlberg로 각각 최소 스크립트를 짜서 같은 URL을 스크레이프한다. 두 언어의 JSON 응답 필드가 정말 동일한 스키마인지 직접 대조해보라 — "코어는 하나"라는 이 저장소의 핵심 주장을 스스로 검증하는 과제다.
crawlberg mcp(stdio 트랜스포트, mcp feature)로 서버를 띄우고 Claude Code/Claude Desktop의 MCP 설정에 등록한다. 에이전트가 scrape·crawl·map 툴을 실제로 호출하게 만들어보고, mcp-http feature로 켠 HTTP 트랜스포트와 stdio 트랜스포트의 차이(세션 유지 방식)도 비교해본다.
BrowserMode::Auto와 telemetry-init feature를 켠 뒤, WAF가 걸린 테스트 사이트를 스크레이프하며 트레이싱 로그에서 "HTTP 시도 → WAF 지문 판정 → 에스컬레이션" 단계가 실제로 순서대로 찍히는지 추적한다. rules/waf_fingerprints.toml에 자신만의 지문 규칙을 하나 추가하고, TomlClassifier::watch()의 핫리로드가 재시작 없이 동작하는지 확인해본다.
crawlberg 크레이트를 직접 의존성으로 추가해 examples/custom_builder.rs를 출발점으로, 도메인별 우선순위를 매기는 나만의 CrawlStrategy나 Redis 기반 분산 큐로 동작하는 Frontier를 구현하고 CrawlEngineBuilder에 주입해본다. 트레이트 경계를 실제로 넘나들어봐야 "완전 교체형 엔진"이 어떤 자유도를 주는지 체감할 수 있다.
Tokio의 비동기 런타임, async-trait로 트레이트에 비동기 메서드를 얹는 법, Arc<dyn Trait>로 다형성을 표현하는 패턴을 traits.rs·engine/를 교재 삼아 익힌다. BFS/DFS/BestFirst/Adaptive 4가지 CrawlStrategy 구현을 비교하며 "전략 패턴을 트레이트로 표현하기"에 익숙해지는 것이 목표.
TLS 지문(JA3/JA4)이 왜 브라우저를 흉내내는 데 중요한지, CDP(Chrome DevTools Protocol)가 어떻게 브라우저를 원격 제어하는지, 헤드리스 브라우저 스텔스 기법이 무엇을 감추려 하는지를 wreq·chromiumoxide 공식 문서와 crawlberg-bypass/configs/*.yaml를 함께 읽으며 정리한다.
NAPI-RS(Node)·PyO3(Python)·wasm-bindgen(WASM)·cbindgen(C 헤더)·JNI(Java/Android)를 각각 공식 튜토리얼로 한 번씩 손으로 만들어본 뒤, alef.toml을 참고서 삼아 "이 도구들을 자동화하는 메타 도구"를 어떻게 설계할지 스케치해본다. 이 저장소 자체가 alef 없이는 유지 불가능한 규모라는 걸 체감하는 것이 핵심.
WARC 1.1 스펙 문서를 읽고 warc.rs의 레코드 작성 코드와 대조해본다. MCP 프로토콜(rmcp 크레이트, #[tool_router] 매크로)과 OpenAPI(utoipa)로 하나의 엔진을 에이전트용/HTTP용으로 동시에 노출하는 법을 정리하고, Firecrawl의 v1 API 스펙과 실제로 비교해본다.
bindings.rs+types.rs)를 읽어 14개 언어의 바인딩 코드·문서·테스트 앱을 자동 생성하는 xberg-io의 사내 코드생성기. 생성 파일은 전부 "DO NOT EDIT" 헤더가 붙는다.crawlberg-bypass 크레이트가 구현체를 제공.rules/waf_fingerprints.toml에 76개가 Aho-Corasick 자동자로 컴파일돼 있고 ArcSwap으로 핫리로드된다.deno_core(V8)로 JS를 실행하고 html5ever+selectors로 DOM/CSS를 흉내낸 자체 제작 경량 브라우저(Obscura 포크). Chrome 설치 없이 JS 렌더링이 필요할 때 1차로 쓴다.scraper로 잘못 적혀 있지만 소스 기준 진실은 tl이다.warc.rs가 외부 크레이트 없이 warcinfo/response 레코드를 직접 작성한다.#[tool_router]/#[tool] 매크로로 AI 에이전트에 scrape/crawl/map 툴을 선언적으로 노출한다.api feature가 제공하는 REST 서버의 정체성. 독자 스펙 대신 이미 널리 쓰이는 Firecrawl v1 API 계약을 그대로 구현해 생태계 호환성을 확보했다.