트렌딩 딥다이브 · 2026-07-19 · TrendShift 트렌딩 · KREUZBERG-DEV

kreuzberg-dev/kreuzcrawl 딥다이브
— Rust 코어 하나가 14개 언어를 "찍어내는" 크롤러

kreuzcrawl은 URL을 넣으면 정제된 마크다운·메타데이터·링크를 돌려주는 Rust 웹 크롤링 엔진이다. 단일 페이지 스크레이핑은 물론, 링크를 따라가는 전체 사이트 크롤링, 사이트맵 기반 URL 발견, 브라우저 조작(클릭·타이핑·스크린샷)까지 한 엔진이 담당한다. 진짜 흥미로운 지점은 두 가지다. 첫째, Rust로 짠 코어 하나를 alef라는 자체 코드 생성기가 읽어 Python·Node.js·Go·Java·C#·PHP·Ruby·Elixir·Dart·Swift·Zig·Kotlin·WASM까지 14개 언어 바인딩 코드를 통째로 찍어낸다는 것 — NAPI-RS나 wasm-bindgen 같은 개별 바인딩 도구조차 "손으로 쓰지 않고 생성되는" 코드다. 둘째, 이 저장소는 최근 이름이 통째로 바뀐 이력이 있다 — 원래 이름은 kreuzcrawl(조직명 kreuzberg-dev)이었고, 지금은 crawlberg(조직명 xberg-io)로 개명되어 이 딥다이브가 클론을 시도한 주소는 새 주소로 301 리다이렉트된다.

(조사 시점 기준 클론 주소: kreuzberg-dev/kreuzcrawl → xberg-io/crawlberg로 리다이렉트 · Rust/TypeScript 등 폴리글랏 · MIT 라이선스 · v1.0.6, 2026-07-19 릴리스 · 커밋 f1a30b1 · Rust 소스 183개 파일 약 71,880줄)
목차
  1. 프로젝트 한 줄 요약
  2. 왜 주목받는가 — Firecrawl·Crawl4AI·anycrawl과 뭐가 다른가
  3. 기술 스택 전체 지도
  4. 아키텍처 심화 — 에스컬레이션 사다리와 alef 코드생성 파이프라인
  5. 디렉토리 구조 해부
  6. 학습 포인트 — 여기서 훔쳐올 설계
  7. 하드웨어 / 시스템 요구사항
  8. 직접 해볼 수 있는 실습 과제
  9. 관련 기술 심화 학습 로드맵
  10. 핵심 키워드 사전
  11. 참고 링크

1프로젝트 한 줄 요약

"코어는 하나, 얼굴은 열넷" — Rust 크롤러가 스스로를 모든 언어에 복제한다
한 문장으로

kreuzcrawl(현재 이름 crawlberg) = "웹사이트 → 마크다운" Rust 엔진 + "직접 짜지 않고 찍어내는" 14개 언어 바인딩 공장

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·모든 언어 바인딩에서 완전히 동일한 스키마로 나온다.

용어
단일 소스, 다중 언어 (single-source-of-truth bindings)
보통의 폴리글랏 라이브러리는 언어마다 사람이 바인딩 코드를 따로 유지보수한다(그래서 Python판엔 있는 기능이 Go판엔 없는 일이 흔하다). kreuzcrawl은 bindings.rs라는 좁은 Rust 함수 표면 하나만 사람이 관리하고, alef라는 사내 도구가 이걸 읽어 14개 언어의 바인딩 코드·타입 정의·문서·테스트 앱까지 전부 자동 생성한다. 기능 추가는 Rust 코어 한 곳에서 끝난다.
먼저 짚고 갈 것
이 저장소, 사실 이름이 바뀌었다

git clone https://github.com/kreuzberg-dev/kreuzcrawl.git은 문제없이 성공하지만, 브라우저로 그 주소를 열면 HTTP 301로 github.com/xberg-io/crawlberg로 리다이렉트된다. CHANGELOG.md1.0.0-rc.1 항목에 개명 내역이 그대로 적혀 있다 — "프로젝트 이름을 kreuzcrawl에서 crawlberg로 변경. 크레이트명·모든 언어 패키지·C FFI 심벌 접두사(kcrawl_*cberg_*)·Go 모듈·문서 도메인이 함께 바뀜"과 "kreuzberg 네임스페이스를 xberg로 리브랜딩. npm 스코프 @kreuzberg@xberg-io, Maven groupId dev.kreuzbergio.xberg. 법인명(Kreuzberg, Inc.)은 그대로."

왜 바꿨을까 짐작할 단서도 있다: 이 프로젝트의 저작자(Na'aman Hirschfeld)는 같은 이름의 문서 추출 라이브러리 "Kreuzberg"(PyPI, 저자 이메일도 @kreuzberg.dev)도 만들고 있고, 그 프로젝트 역시 같은 xberg-io CDN 배너·같은 다국어 배지 구성을 쓴다. 즉 "문서추출 Kreuzberg"와 "크롤러 Kreuzcrawl(구 Kreuzberg 계열)"이라는 두 자매 프로젝트 이름이 겹쳐서 크롤러 쪽을 Crawlberg로 새로 지은 것으로 보인다. TrendShift의 트렌딩 스냅샷이 이 개명 전 이름을 아직 들고 있는 셈이다. 이하 본문에서는 실제로 클론되는 소스 기준으로 crawlberg라는 크레이트/함수명을 그대로 쓰되, 저장소를 가리킬 땐 원래 지시받은 이름 kreuzcrawl도 병기한다.

2왜 주목받는가 — Firecrawl·Crawl4AI·anycrawl과 뭐가 다른가

"크롤러"가 아니라 "크롤러를 만드는 공장 설비"를 판다

"웹사이트를 LLM이 먹기 좋은 마크다운으로 바꿔주는" 크롤러/스크레이퍼는 이미 붐비는 시장이다. Firecrawl(SaaS 우선, Node/TS 코어), Crawl4AI(Python 전용, Playwright 기반 OSS), anycrawl(Node/TS, Firecrawl류 셀프호스트 대안) 모두 비슷한 약속 — "URL을 던지면 깨끗한 마크다운이 나온다" — 을 한다. kreuzcrawl이 이 무리에서 눈에 띄는 지점은 결과물이 아니라 "어떤 언어로도 같은 엔진을 네이티브로 박아 쓸 수 있다"는 배포 전략이다.

기존 도구와의 결정적 차이

Firecrawl / Crawl4AI / anycrawlkreuzcrawl(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로 원격 호출"하는 게 아니라 자국 규격 엔진을 그대로 차체에 얹는다.

주목 포인트 4가지

① 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 저장소에 있다고 명시한다 — 코어는 완전히 열어 두고 운영 편의 기능으로 수익화하는 전형적인 오픈코어 모델이다.

용어
MIT 라이선스 (참고: 처음 지시받은 정보와 다름)
이 딥다이브를 의뢰받을 때는 "Elastic License 2.0로 보인다"는 전제가 있었지만, 클론한 소스의 LICENSE 파일과 워크스페이스 Cargo.tomllicense = "MIT"를 직접 확인한 결과 실제로는 MIT다. Elastic License 2.0 같은 소스공개 라이선스는 "소스는 볼 수 있지만 특정 상업적 사용(예: 매니지드 서비스로 재판매)은 금지"하는 제약이 있는 반면, MIT는 그런 제약이 전혀 없는 완전한 오픈소스 라이선스다 — 상업적 이용·수정·재배포가 모두 자유롭다. README/캐시된 페이지보다 소스 파일을 직접 열어봐야 하는 이유를 보여주는 사례다.

3기술 스택 전체 지도

Rust 코어 하나 + 언어별 바인딩 크레이트 10개 + 비-Rust 패키지 11개 + 코드생성기

워크스페이스 루트 Cargo.tomledition = "2024", rust-version = "1.91", license = "MIT"로 선언돼 있다. 코어 크레이트 crawlberg는 feature flag로 기능을 잘게 쪼개 놓아, 필요한 것만 골라 컴파일할 수 있다.

① 백엔드 — Rust 코어(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-rsMarkdown/Plain/Djot 3가지 출력 포맷, 표·인용·"fit content" 프루닝 포함
실브라우저 폴백chromiumoxide(CDP)진짜 헤드리스 Chrome을 원격 제어. 로컬 실행 또는 --browser-endpoint로 외부 CDP WS 접속
자체 제작 브라우저crawlberg-browser(별도 크레이트)Chrome 설치 없이 V8+DOM만으로 JS 렌더링(아래 4장에서 자세히)
스텔스 HTTPwreq/wreq-utilChrome 145 TLS(JA3) 지문 위장 — reqwest 포크
WAF 탐지자체 TOML 지문 DB + aho-corasick76개 룰(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 APIaxum 0.8 + utoipa(OpenAPI)Firecrawl v1 호환 스펙
관측성OpenTelemetry(+OTLP gRPC) + tracingxberg-enterprise와 스팬/메트릭 스키마 공유(주석에 명시)
AI 리서치 에이전트liter-llm + minijinjaai feature. plan → crawl → synthesize 자율 루프
아카이빙자체 WARC 1.1 writer표준 규격 그대로 구현(외부 크레이트 없이 직접 작성)

② 바인딩 레이어 — 14개 언어, 하나의 코어

README가 밝히는 지원 언어는 정확히 Rust, Python, Node.js, Ruby, Go, Java, Kotlin(Android), C#, PHP, Elixir, Dart, Swift, Zig, WebAssembly = 14개다. 각 언어가 코어를 "부르는" 방식은 언어 특성에 맞춰 전부 다르다.

언어배포처연결 메커니즘
PythonPyPI crawlbergPyO3 + maturin, abi3-py310(3.10+ 단일 wheel)
Node.jsnpm @xberg-io/crawlbergNAPI-RS(napi v3, cdylib)
WebAssemblynpm @xberg-io/crawlberg-wasmwasm-bindgen
PHPComposer xberg-io/crawlbergext-php-rs(진짜 Zend 확장으로 컴파일)
C/C++공유 라이브러리 + cbindgen 헤더C FFI(cdylib+staticlib+rlib)
RubyRubyGems crawlbergMagnus + rb-sys
ElixirHex crawlbergRustler NIF
GoGo 모듈 github.com/xberg-io/crawlberg/packages/gocgo로 C FFI 호출
JavaMaven Central io.xberg.crawlberg:crawlbergJava 25 Panama FFM(최신 Foreign Function & Memory API)
Kotlin(Android)Maven Central io.xberg.crawlberg.androidAndroid AAR + JNI(.so) — Panama 대신 JNI를 쓰는 이유는 7장 참고
C#NuGet Crawlberg.NET 10 P/Invoke
Dartpub.dev crawlbergDart FFI
SwiftSwift Package ManagerC FFI 경유
Zigzig fetch --saveC 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

루트의 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.다.

④ 인프라 · 품질 툴체인

Dockerrust: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가 에이전트 설명 파일들을 동기화한다.

4아키텍처 심화 — 에스컬레이션 사다리와 alef 코드생성 파이프라인

"어떻게 막힌 페이지를 뚫는가"와 "어떻게 코드 하나로 14개 언어가 되는가"

이 저장소의 설계를 이해하는 데는 두 다이어그램이면 충분하다. 하나는 런타임 흐름(URL 하나가 들어와서 결과가 나오기까지), 다른 하나는 빌드타임 흐름(Rust 소스 하나가 14개 언어 패키지로 불어나기까지)이다.

(A) 런타임 — 요청은 "에스컬레이션 사다리"를 오른다

scrape(url) / crawl(url) / map_urls(url) / interact(url, actions) │ CrawlEngine (트레이트 조합으로 구성) Frontier · RateLimiter · CrawlStrategy · ContentFilter CrawlCache · CrawlStore · EventEmitter ← 전부 트레이트, 교체 가능 │ Tower 서비스 스택 (레이트리밋 → 캐시 → UA 로테이션 → fetch) │ ① 순정 HTTP로 우선 시도 (reqwest) │ WAF 지문 판정 (TOML + Aho-Corasick, 76개 룰) │ ┌─────────────────┼──────────────────────────┐ │ 정상 응답 │ 차단 감지 / 콘텐츠 밀도 낮음(SPA 의심) │ ▼ ▼ tl로 HTML 파싱 ② EscalationStrategy 에 따라 승격 │ ├─ BypassFirst / BypassOnly: │ │ crawlberg-bypass → Bright Data / ScrapingBee / Zyte │ ├─ BrowserOnly / BypassThenBrowser: │ │ ┌─ BrowserBackend::Native → crawlberg-browser(V8+html5ever, 경량) │ │ └─ BrowserBackend::Chromiumoxide → 진짜 헤드리스 Chrome(CDP) │ └─ None: 승격하지 않고 실패 그대로 반환 ▼ │ html-to-markdown-rs 변환 ◀─────┘ (성공하면 다시 파싱 파이프라인으로 합류) │ citations 추출 + fit_content 프루닝 │ ScrapeResult / CrawlResult / MapResult (+ warc 피처면 WARC 레코드도 기록)

핵심은 EscalationStrategy 열거형이다 — None·BrowserOnly·BypassFirst·BypassOnly·BypassThenBrowser 5가지 값으로 "막히면 무엇부터 시도할지" 순서를 설정으로 고른다. 브라우저 백엔드도 BrowserMode::Auto/Always/Never/StealthBrowserBackend::Native/Chromiumoxide 두 축으로 나뉜다. 즉 "JS 필요하면 무조건 Playwright 켠다"는 식이 아니라, 비용이 싼 수단부터 순서대로 시도하도록 촘촘하게 설계돼 있다.

영리한 디테일
SPA 감지에 DOM 파서를 안 쓴다

engine/crawl_loop.rscontent_density()는 HTML 문자열을 한 글자씩 훑으며 <...> 태그 바깥 글자 수만 센다. 전체 DOM을 파싱하지 않고 5줄짜리 태그 스트리핑만으로 "텍스트 밀도"를 근사한다 — SPA 껍데기(밀도 0.0~0.05)와 정상 콘텐츠 페이지(밀도 0.3+)를 구분하는 데는 이 정도면 충분하다는 실용적 트레이드오프. 정확도보다 속도가 중요한 1차 판정에 딱 맞는 선택이다.

(B) 빌드타임 — alef가 Rust를 14개 언어로 번역한다

crates/crawlberg/src/bindings.rs + types.rs ← 사람이 손으로 관리하는 유일한 "진실의 원천" #[cfg_attr(alef, alef(skip))] 로 Rust 전용 API만 표시해 제외 │ alef.toml (46KB) 언어별 DTO 스타일 · 패키지명 · 문서 출력 경로 · 동기화 대상 파일 정의 │ $ alef generate (이 저장소는 alef 0.34.4 사용) │ ┌───────────┬───────────┬────────────┬────────────┬─────────────┬───────────┐ ▼ ▼ ▼ ▼ ▼ ▼ ▼ crawlberg- crawlberg- crawlberg- crawlberg- packages/java packages/go .claude/ node/ wasm/ php/ ffi/ (Panama FFM) (cgo) skills/*.md lib.rs lib.rs lib.rs cbindgen (CLI --help, (NAPI-RS) (wasm- (ext-php- 헤더 자동 MCP 툴 정의로부터) bindgen) rs) 생성 "// This file is auto-generated by alef. DO NOT EDIT." ← 모든 생성 파일 공통 헤더

이 파이프라인의 재미있는 지점은 alef가 만드는 게 저수준 FFI 글루 코드에서 그치지 않는다는 것이다. 예컨대 Node 바인딩의 JsBytes 래퍼 타입은 NAPI v3의 기본 Vec<u8> 역직렬화가 Array<number>만 받아들이는 한계를 피해 Buffer·Uint8Array·Array를 모두 받도록 만든 커스텀 타입인데, 이것도 alef가 생성한 코드 안에 들어 있다. WASM 바인딩의 WasmExtractionMeta 같은 #[wasm_bindgen] 래퍼 구조체도 마찬가지다 — 사람은 Rust 타입 하나만 설계하면, 각 언어의 관용적인 표현(idiomatic API)까지 alef가 대신 고민해준다.

용어
CrawlEngineHandle (불투명 핸들 패턴)
코어의 CrawlEngine은 내부적으로 Arc<dyn Frontier>, Arc<dyn CrawlStrategy> 같은 트레이트 객체를 쓰는데, 이런 동적 디스패치 타입은 FFI 경계를 넘길 수 없다. 그래서 bindings.rs는 기본 구현체만으로 엔진을 구성하는 "설정 전용" 생성 경로(create_engine)와, 그 결과를 감싸는 불투명 핸들 CrawlEngineHandle을 따로 둔다. 언어 바인딩은 이 핸들만 주고받을 뿐 내부 트레이트 객체는 절대 보지 못한다 — "복잡한 내부는 Rust 안에 가두고, 바깥에는 단순한 손잡이만 내준다"는 FFI 설계의 정석이다.

MCP·REST 서버 — 같은 엔진을 공유하는 두 개의 얼굴

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 세션에는 그 레이어를 우회시킨 것이다.

SSRF 방어 — 계층을 하나씩 쌓는다

기본값은 사설/루프백/링크로컬/메타데이터/멀티캐스트/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)도 100% 맞진 않다

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든, 근거는 결국 클론한 소스에 둬야 한다는 이 프로젝트 작업 원칙을 스스로 증명해 준 사례다.

5디렉토리 구조 해부

183개 Rust 파일, 71,880줄 — 어디부터 열어봐야 하나
kreuzcrawl/ (= xberg-io/crawlberg, 2026-06-26 이후 이름) │ Cargo.toml ★ 워크스페이스 정의. license=MIT, edition=2024 │ alef.toml ★★ 14개 언어 바인딩 생성 설정(46KB) — 2번째로 열 파일 │ CHANGELOG.md 개명 히스토리가 그대로 적혀 있는 저장소의 "일기장" │ ├─ crates/ │ ├─ crawlberg/ ★★★ 코어. 이 저장소의 90% │ │ └─ src/ │ │ ├─ lib.rs 공개 API 진입점 — feature gate 목록부터 훑기 │ │ ├─ bindings.rs ★ alef가 읽는 "진실의 원천" 함수 시그니처 │ │ ├─ traits.rs Frontier/RateLimiter/CrawlStrategy 등 7개 트레이트 │ │ ├─ engine/ CrawlEngine 본체 + crawl_loop(내부 오케스트레이션) │ │ ├─ defaults/ 트레이트 기본 구현(BFS/DFS/BestFirst/Adaptive 전략 등) │ │ ├─ html/ 메타데이터·링크·이미지·JSON-LD·피드 추출(tl 기반) │ │ ├─ waf/ TOML 지문 분류기 + rules/waf_fingerprints.toml(76종) │ │ ├─ net/ssrf.rs SSRF 방어 정책 │ │ ├─ browser.rs chromiumoxide(CDP) 폴백 │ │ ├─ native_browser.rs crawlberg-browser 크레이트로의 어댑터 │ │ ├─ mcp/ rmcp 기반 MCP 서버 │ │ ├─ api/ axum REST 서버(Firecrawl v1 호환) │ │ ├─ research/ AI 리서치 에이전트(plan-crawl-synthesize) │ │ └─ warc.rs WARC 1.1 writer │ │ │ ├─ crawlberg-browser/ ★★ 자체 제작 "가짜 헤드리스 브라우저" │ │ └─ src/ │ │ ├─ js/ (runtime·ops·module_loader·v8_flags) deno_core로 V8 실행 │ │ ├─ dom/ (tree·tree_sink·selector·serialize) html5ever 기반 DOM │ │ ├─ net/ (client·wreq_client·interceptor·cookies·ssrf) │ │ └─ NOTICE Obscura(Apache-2.0) 포크임을 명시 │ │ │ ├─ crawlberg-bypass/ 3rd-party 우회 벤더 어댑터 │ │ ├─ src/ (provider·loader·config·extract) │ │ └─ configs/ (bright_data · scrapingbee · zyte).yaml │ │ │ ├─ crawlberg-cli/ clap 기반 CLI (scrape/crawl/map/serve/mcp 등 11개 서브커맨드) │ ├─ crawlberg-node/ crawlberg-wasm/ crawlberg-php/ crawlberg-py/ │ ├─ crawlberg-ffi/ crawlberg-jni/ crawlberg-bindings-common/ │ │ └─ src/lib.rs "auto-generated by alef. DO NOT EDIT." 로 시작 │ │ ├─ packages/ Rust crate가 아닌 나머지 언어 패키지 소스 │ go/ java/ kotlin-android/ csharp/ ruby/ elixir/ dart/ swift/ zig/ python/ │ ├─ .claude/skills/ · .codex/skills/ · .agents/skills/ · .github/skills/ │ (api·cli·mcp) — alef docs 로 CLI --help/MCP 툴 정의에서 자동 생성되는 에이전트용 설명서 │ ├─ rules/waf_fingerprints.toml WAF 지문 76개(TOML) ├─ fixtures/ 322개 테스트 픽스처(HTML/PDF/robots/사이트맵 등) ├─ test_apps/ 16개 언어별 e2e 스모크 테스트 앱 ├─ docs-site/ Astro 기반 공식 문서(docs.crawlberg.xberg.io) └─ e2e/ 언어별 종단 테스트 스위트
읽는 순서 추천

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라 각오할 것). 이 순서면 저장소의 핵심 설계를 대부분 손에 넣을 수 있다.

6학습 포인트 — 여기서 훔쳐올 설계

크롤러에 관심 없어도 배울 값어치가 있는 것들
패턴 1 · 트레이트 기반 완전 교체형 엔진

Frontier·RateLimiter·Strategy·Cache·Store·Emitter·Filter — 7개의 교체 포인트

CrawlEngine은 이 7개 트레이트의 Arc<dyn Trait> 조합일 뿐이다. 기본 구현(인메모리 프론티어, BFS/DFS/BestFirst/Adaptive 전략, no-op 캐시 등)이 준비돼 있지만, 분산 큐가 필요하면 Frontier만, 커스텀 저장소가 필요하면 CrawlStore만 갈아끼우면 된다. "확장 포인트를 트레이트로 뚫어 두고 기본 구현은 따로 제공한다"는 Rust 라이브러리 설계의 교과서적인 예다.

패턴 2 · 진실은 하나, 출력은 열넷

바인딩 코드를 손으로 쓰지 않고 생성한다

bindings.rs + types.rs라는 좁은 표면만 관리하면 alef가 NAPI-RS·PyO3·wasm-bindgen·JNI·ext-php-rs·cbindgen 코드를 전부 찍어낸다. 14개 언어를 손으로 유지보수한다고 상상해 보면 이 설계가 왜 필요한지 바로 이해된다 — 기능 하나를 추가할 때마다 14곳을 고치는 대신 1곳만 고치면 나머지는 alef generate 한 번으로 끝난다. 사내 도구를 만들 정도로 이 문제에 진심이었다는 뜻이기도 하다.

패턴 3 · 안티봇 대응을 사다리로 설계한다

비용이 싼 수단부터, 필요한 만큼만 승격

EscalationStrategy 열거형 5가지(None/BrowserOnly/BypassFirst/BypassOnly/BypassThenBrowser)로 "막히면 무엇부터 시도할지" 순서를 데이터로 표현했다. 무조건 풀브라우저를 띄우는 대신, HTTP → WAF 판정 → (선택) 우회 벤더 API 또는 자체 경량 브라우저 → 최후에만 진짜 Chrome. 비용·속도·성공률 사이의 트레이드오프를 코드가 아니라 설정값 하나로 조정할 수 있게 만든 설계는 다른 도메인(재시도 정책, 캐시 계층, 알림 채널 폴백)에도 그대로 옮겨 쓸 수 있다.

패턴 4 · "가짜 브라우저"라는 세 번째 선택지

Playwright도, 순정 HTTP도 아닌 중간 지점

보통 크롤러는 "HTTP냐 진짜 브라우저(Playwright/Selenium)냐" 둘 중 하나만 고른다. kreuzcrawl은 deno_core(V8)로 자바스크립트를 실행하고 html5ever+cssparser+selectors로 DOM/CSS를 흉내 낸 자체 제작 경량 브라우저(Obscura 포크)를 세 번째 선택지로 넣었다. Chrome 바이너리 설치가 필요 없고 훨씬 가볍지만, 스크린샷이 진짜 컴포지터 캡처가 아니라 "액션 이후 HTML을 재구성한 PNG 스냅샷"이라 픽셀 퍼펙트는 아니라는 한계도 문서에 정직하게 적혀 있다. 무엇을 얻고 무엇을 포기했는지가 명확한 좋은 트레이드오프 사례다.

패턴 5 · 탐지 규칙을 코드가 아니라 데이터로

TOML 지문 DB + 핫리로드

WAF 탐지 규칙 76개가 Rust 코드가 아니라 rules/waf_fingerprints.toml에 산다. TomlClassifier는 이 규칙들을 ArcSwap에 담아, 파일 변경을 감지하면(500ms 디바운스로 에디터의 tmpfile-rename 저장 패턴까지 고려) 재배포 없이 룰만 갈아끼운다. "탐지 로직을 재컴파일 없이 바꾸고 싶다"는 요구를 만났을 때 참고할 만한 가장 단순한 해법이다.

패턴 6 · 경쟁자의 API 계약을 그대로 흉내낸다

Firecrawl v1 호환이라는 실용적 선택

독자적인 REST 스펙을 새로 설계하는 대신, 이미 생태계가 형성된 Firecrawl v1 API와 호환되는 서버를 내장했다. 이렇게 하면 사용자가 클라이언트 코드를 거의 바꾸지 않고도 자체 호스팅으로 전환할 수 있다 — API를 "발명"하기보다 "이미 검증된 계약을 재사용"하는 것도 어엿한 설계 전략이라는 걸 보여준다.

패턴 7 · 에이전트용 문서도 소스에서 생성한다

CLI --help·MCP 툴 정의 → Claude/Codex 스킬 문서

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) 흔한 문제를, 문서 생성 자체를 빌드 파이프라인에 편입시켜 원천 차단한 것이다.

7하드웨어 / 시스템 요구사항

코어는 가볍다 — 무거워지는 건 브라우저 폴백을 켤 때뿐

순수 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종 네이티브 바이너리 기준
브라우저 폴백 · NativeChrome 설치 불필요. 자체 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) 크기를 늘릴수록 메모리·파일 디스크립터 사용량이 선형으로 늘어난다는 점은 실제 운영 시 감안해야 한다.

8직접 해볼 수 있는 실습 과제

난이도별로, 손을 움직이며 배우는 순서

과제 1. CLI로 첫 스크레이프/크롤 난이도 ★☆☆

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가 실제로 어떻게 결과를 제한하는지 관찰한다.

과제 2. 두 언어로 같은 결과 재현하기 난이도 ★★☆

pip install crawlbergnpm i @xberg-io/crawlberg로 각각 최소 스크립트를 짜서 같은 URL을 스크레이프한다. 두 언어의 JSON 응답 필드가 정말 동일한 스키마인지 직접 대조해보라 — "코어는 하나"라는 이 저장소의 핵심 주장을 스스로 검증하는 과제다.

과제 3. MCP 서버를 Claude Code에 연결 난이도 ★★☆

crawlberg mcp(stdio 트랜스포트, mcp feature)로 서버를 띄우고 Claude Code/Claude Desktop의 MCP 설정에 등록한다. 에이전트가 scrape·crawl·map 툴을 실제로 호출하게 만들어보고, mcp-http feature로 켠 HTTP 트랜스포트와 stdio 트랜스포트의 차이(세션 유지 방식)도 비교해본다.

과제 4. 에스컬레이션 사다리 관찰하기 난이도 ★★★

BrowserMode::Autotelemetry-init feature를 켠 뒤, WAF가 걸린 테스트 사이트를 스크레이프하며 트레이싱 로그에서 "HTTP 시도 → WAF 지문 판정 → 에스컬레이션" 단계가 실제로 순서대로 찍히는지 추적한다. rules/waf_fingerprints.toml에 자신만의 지문 규칙을 하나 추가하고, TomlClassifier::watch()의 핫리로드가 재시작 없이 동작하는지 확인해본다.

과제 5. 나만의 CrawlStrategy/Frontier 구현 난이도 ★★★★

crawlberg 크레이트를 직접 의존성으로 추가해 examples/custom_builder.rs를 출발점으로, 도메인별 우선순위를 매기는 나만의 CrawlStrategy나 Redis 기반 분산 큐로 동작하는 Frontier를 구현하고 CrawlEngineBuilder에 주입해본다. 트레이트 경계를 실제로 넘나들어봐야 "완전 교체형 엔진"이 어떤 자유도를 주는지 체감할 수 있다.

9관련 기술 심화 학습 로드맵

이 저장소를 발판 삼아 넓혀갈 4주 코스

1주차 — Rust 비동기와 트레이트 기반 플러그인 아키텍처

Tokio의 비동기 런타임, async-trait로 트레이트에 비동기 메서드를 얹는 법, Arc<dyn Trait>로 다형성을 표현하는 패턴을 traits.rs·engine/를 교재 삼아 익힌다. BFS/DFS/BestFirst/Adaptive 4가지 CrawlStrategy 구현을 비교하며 "전략 패턴을 트레이트로 표현하기"에 익숙해지는 것이 목표.

2주차 — 안티봇/WAF 우회 생태계

TLS 지문(JA3/JA4)이 왜 브라우저를 흉내내는 데 중요한지, CDP(Chrome DevTools Protocol)가 어떻게 브라우저를 원격 제어하는지, 헤드리스 브라우저 스텔스 기법이 무엇을 감추려 하는지를 wreq·chromiumoxide 공식 문서와 crawlberg-bypass/configs/*.yaml를 함께 읽으며 정리한다.

3주차 — FFI와 폴리글랏 바인딩

NAPI-RS(Node)·PyO3(Python)·wasm-bindgen(WASM)·cbindgen(C 헤더)·JNI(Java/Android)를 각각 공식 튜토리얼로 한 번씩 손으로 만들어본 뒤, alef.toml을 참고서 삼아 "이 도구들을 자동화하는 메타 도구"를 어떻게 설계할지 스케치해본다. 이 저장소 자체가 alef 없이는 유지 불가능한 규모라는 걸 체감하는 것이 핵심.

4주차 — 웹 아카이빙과 에이전트 도구화

WARC 1.1 스펙 문서를 읽고 warc.rs의 레코드 작성 코드와 대조해본다. MCP 프로토콜(rmcp 크레이트, #[tool_router] 매크로)과 OpenAPI(utoipa)로 하나의 엔진을 에이전트용/HTTP용으로 동시에 노출하는 법을 정리하고, Firecrawl의 v1 API 스펙과 실제로 비교해본다.

10핵심 키워드 사전

이 문서에 나온 용어를 한 줄로
alef
Rust 코어(bindings.rs+types.rs)를 읽어 14개 언어의 바인딩 코드·문서·테스트 앱을 자동 생성하는 xberg-io의 사내 코드생성기. 생성 파일은 전부 "DO NOT EDIT" 헤더가 붙는다.
에스컬레이션 전략 (EscalationStrategy)
막힌 요청을 무엇으로 재시도할지 정하는 5가지 값(None/BrowserOnly/BypassFirst/BypassOnly/BypassThenBrowser). 비용이 싼 수단부터 순서대로 시도하게 만드는 설정.
BypassProvider
Bright Data·ScrapingBee·Zyte 같은 3rd-party 우회 API를 YAML로 선언해 붙이는 트레이트. crawlberg-bypass 크레이트가 구현체를 제공.
WAF 지문(fingerprint)
응답 헤더/본문에서 방화벽·봇차단 서비스를 식별하는 패턴. rules/waf_fingerprints.toml에 76개가 Aho-Corasick 자동자로 컴파일돼 있고 ArcSwap으로 핫리로드된다.
Native 브라우저 백엔드
deno_core(V8)로 JS를 실행하고 html5ever+selectors로 DOM/CSS를 흉내낸 자체 제작 경량 브라우저(Obscura 포크). Chrome 설치 없이 JS 렌더링이 필요할 때 1차로 쓴다.
chromiumoxide
CDP(Chrome DevTools Protocol)로 진짜 헤드리스 Chrome을 원격 제어하는 Rust 크레이트. Native 백엔드로도 안 되는 페이지의 최후 수단.
wreq / 스텔스
reqwest 포크로, Chrome의 TLS(JA3) 지문을 그대로 흉내내 "이 요청이 봇인지" 판별하는 서버를 속이는 스텔스 HTTP 클라이언트.
tl (astral-tl)
코어가 실제로 쓰는 고속 HTML5 파서(Astral 포크). 문서에는 종종 scraper로 잘못 적혀 있지만 소스 기준 진실은 tl이다.
html-to-markdown-rs
HTML→Markdown 변환 엔진. Markdown/Plain/Djot 3가지 출력 포맷과 표·인용·"fit content"(LLM용 프루닝) 추출을 지원.
Djot
CommonMark/Pandoc 저자 John MacFarlane이 설계한 경량 마크업 언어. html-to-markdown-rs가 Markdown 대신 선택 가능한 출력 포맷 중 하나.
SSRF 방어
사설/루프백/링크로컬/클라우드 메타데이터 IP로 향하는 요청을 기본 차단. 리다이렉트 매 홉 재검증 + 헤드리스 브라우저 하위 요청까지 CDP Fetch 가로채기로 검증하는 계층 방어.
WARC 1.1
국제 웹 아카이빙 표준 규격. warc.rs가 외부 크레이트 없이 warcinfo/response 레코드를 직접 작성한다.
rmcp / MCP
Model Context Protocol의 Rust SDK. #[tool_router]/#[tool] 매크로로 AI 에이전트에 scrape/crawl/map 툴을 선언적으로 노출한다.
Firecrawl v1 호환 API
api feature가 제공하는 REST 서버의 정체성. 독자 스펙 대신 이미 널리 쓰이는 Firecrawl v1 API 계약을 그대로 구현해 생태계 호환성을 확보했다.
Kreuzcrawl → Crawlberg 개명
2026-06-26(v1.0.0-rc.1)에 저장소·패키지명·FFI 심벌 접두사가 전부 바뀐 사건. 저작자의 다른 프로젝트("Kreuzberg" 문서추출 라이브러리)와 이름이 겹쳐서로 추정된다. 법인명(Kreuzberg, Inc.)은 그대로 유지.

11참고 링크

원본부터 배경 지식까지

프로젝트

핵심 소스(먼저 읽을 파일)

기반 기술