Docfind — Rust와 WebAssembly로 만든 서버 없는 브라우저 검색 엔진
검색창에 키워드를 입력했을 때 페이지가 새로 로드되거나, 외부 API 서버로 요청이 날아가는 것을 본 적 있을 것이다. 대부분의 문서 사이트가 그렇게 동작한다. Algolia 같은 SaaS에 돈을 내거나, 자체 서버를 운영하거나, 느린 리다이렉트를 감수하거나. VS Code 문서 팀은 이 세 가지를 모두 거부하고 네 번째 길을 택했다. Rust로 인덱서를 짜고, WebAssembly로 브라우저에 올린 것이다. 결과는 2.7MB 바이너리, 쿼리당 0.4ms.
기존 접근 방식의 문제
문서 사이트 검색을 구현하는 방법은 대략 세 가지로 나뉜다.
| 방식 | 장점 | 단점 |
|---|---|---|
| Algolia / Typesense | 빠르고 기능 풍부 | 서버 비용, API 키 관리, 외부 의존성 |
| Lunr.js (클라이언트) | 서버 불필요 | 인덱스 파일 10MB+, 초기 로드 느림 |
| 서버 사이드 리다이렉트 | 구현 단순 | 검색마다 페이지 이동, UX 저하 |
| Docfind (WASM) | 서버 없음, 빠름, 소형 | 초기 WASM 로드 비용 |
Lunr.js의 10MB 인덱스 문제가 결정적이었다. 문서 사이트 특성상 검색은 자주 쓰이는 기능인데, 매번 10MB를 내려받는 건 모바일 사용자에게 가혹하다. Docfind는 이 간극을 정확히 파고들었다.
Docfind가 뭔가
Docfind는 WebAssembly 모듈 하나로 브라우저에서 완전히 실행되는 검색 엔진이다. 서버 요청 없이 0.4ms 만에 쿼리를 처리하고, 3,700개 문서 기준 Brotli 압축 후 2.7MB다.
Microsoft VS Code 문서 팀이 만들었으며, 오픈소스로 공개되어 있다. 핵심은 단순하다. 검색 인덱스를 WASM 바이너리 안에 직접 구워 넣는다. 런타임에 외부에서 인덱스를 내려받는 게 아니라, WASM 모듈 자체가 인덱스를 들고 있는 방식이다.
[💡 잠깐! 이 용어는?] WebAssembly(WASM): 브라우저에서 실행되는 저수준 바이너리 포맷이다. JavaScript보다 빠른 실행 속도를 제공하며, Rust, C, C++ 등의 언어로 작성한 코드를 브라우저에서 돌릴 수 있다. 샌드박스 환경에서 실행되므로 보안 격리도 보장된다.
인덱싱 파이프라인
문서를 검색 가능한 상태로 만드는 과정은 네 단계를 거친다.
1단계 — RAKE 키워드 추출
원시 JSON 문서를 받아 RAKE(Rapid Automatic Keyword Extraction) 알고리즘으로 키워드를 뽑는다. RAKE는 단어 간 공동 출현 빈도를 분석해 핵심 구문을 추출하는 알고리즘이다. 비유하면 책 한 권의 색인 페이지를 자동으로 만드는 것과 같다. 모든 단어를 나열하는 게 아니라, "이 단어가 이 문서에서 중요하다"는 판단을 알고리즘이 내린다.
2단계 — FST 구성
추출된 키워드로 **FST(Finite State Transducer)**를 구성한다. FST는 여러 문자열을 극도로 압축된 형태로 저장하는 자료구조다.
[💡 잠깐! 이 용어는?] FST(Finite State Transducer): 유한 상태 기계의 일종으로, 입력 문자열을 출력값(예: 문서 ID)으로 매핑하는 자료구조다. 공통 접두어를 공유하는 수천 개의 키워드를 하나의 압축된 그래프로 표현할 수 있어, HashMap보다 메모리 사용량이 현저히 낮다.
3단계 — FSST 압축
문서 본문 문자열은 FSST(Fast Static Symbol Table) 알고리즘으로 압축한다. 반복되는 부분 문자열을 심볼 테이블로 치환하는 방식으로, 문서 사이트의 특성상 용어와 구문이 반복될수록 압축률이 높아진다.
4단계 — 바이너리 패킹
FST와 FSST로 압축된 데이터를 하나의 바이너리로 패킹하여 WASM 모듈에 내장한다.
pub struct Index {
fst: Vec<u8>,
document_strings: FsstStrVec,
keyword_to_documents: Vec<(u32, f32)>,
}WASM 임베딩의 묘수
가장 흥미로운 설계 결정은 WASM 모듈에 인덱스를 내장하는 방식이다. 평범하게 생각하면 인덱스가 바뀔 때마다 Rust 코드를 재컴파일해야 한다. Docfind는 이 문제를 영리하게 우회했다.
정적 WASM 템플릿 + CLI 패치 방식이다.
- WASM 바이너리를 한 번 컴파일할 때, 전역 변수 자리에
0xdead_beef같은 플레이스홀더를 심어둔다 - 문서 인덱싱 CLI가 생성된 인덱스 데이터를 WASM 바이너리에서 플레이스홀더를 찾아 직접 덮어쓴다
- 재컴파일 없음. CLI가 바이너리를 수술하는 것이다
비유하면 출판된 책에서 특정 페이지 번호만 스티커로 가리고 새 숫자로 교체하는 것과 같다. 책 전체를 다시 인쇄할 필요가 없다.
docfind-cli build --docs ./docs --output search.wasm이 설계 덕분에 문서가 업데이트될 때마다 Rust 컴파일 체인을 돌릴 필요가 없다. CI에서 CLI 한 줄이면 충분하다.
검색 실행 — 쿼리가 들어왔을 때
브라우저에서 키워드를 입력하면 다음 과정이 0.4ms 안에 끝난다.
레벤슈타인 오토마톤으로 오타 허용
FST 위에서 Levenshtein 오토마톤을 실행한다. 레벤슈타인 거리 1~2 이내의 오타를 자동으로 허용한다. "docuemnt"를 입력해도 "document"를 찾아낸다.
[💡 잠깐! 이 용어는?] Levenshtein 오토마톤: 주어진 문자열과 편집 거리가 k 이하인 모든 문자열을 인식하는 유한 오토마톤이다. FST와 결합하면 전체 단어 목록을 다 훑지 않고도 오타를 허용하는 검색을 효율적으로 수행할 수 있다.
접두어 매칭 + 점수 결합
같은 FST에서 접두어 매칭도 함께 수행한다. "rust"를 입력하면 "rust", "rustc", "rustfmt" 등이 모두 후보가 된다. 각 후보에 대해 RAKE가 계산한 점수와 접두어 매칭 가중치를 결합하여 최종 순위를 낸다.
온디맨드 압축 해제
결과 문서의 스니펫은 FSST로 압축된 상태다. 검색 결과로 표시할 문서만 선택적으로 압축을 해제한다. 전체 문서 스트링을 한꺼번에 메모리에 올리지 않아도 된다.
성능 수치
| 지표 | 수치 |
|---|---|
| 대상 문서 수 | ~3,700개 (VS Code 문서 기준) |
| WASM 파일 크기 (Brotli 압축) | 2.7MB |
| 쿼리당 응답 시간 | 0.4ms |
| 스트레스 테스트 규모 | 50,000개 문서 |
| 서버 비용 | $0 |
| API 키 | 없음 |
포인트: 검색 성능의 병목은 알고리즘 복잡도보다 네트워크 왕복 지연인 경우가 많다. Docfind는 그 왕복을 원천 차단한다.
어디에 적용할 수 있나
Docfind의 설계가 잘 맞는 상황:
- 정적 사이트 문서 — Gatsby, Next.js Static Export, Astro 등으로 빌드한 문서 사이트
- 서버리스 배포 — Cloudflare Pages, Vercel, GitHub Pages처럼 백엔드 없이 정적 파일만 서빙하는 환경
- 비용 제약이 있는 오픈소스 프로젝트 — Algolia 무료 플랜 한도를 초과한 경우
- 오프라인 지원 필요 — PWA로 오프라인 검색이 필요한 경우, WASM 모듈 자체가 캐싱 단위가 된다
반대로 콘텐츠가 실시간으로 빈번하게 바뀌는 사이트라면 빌드마다 WASM을 재생성해야 한다. 수백만 개 문서를 다루는 엔터프라이즈 규모라면 단일 WASM 모듈 크기가 감당하기 어려운 수준이 될 수 있다.
정리
- Docfind는 Rust + WebAssembly로 구현한 순수 클라이언트 사이드 검색 엔진이다
- FST로 키워드 인덱스를 압축하고, FSST로 문서 문자열을 압축하여 Lunr.js 대비 73% 작은 인덱스를 만든다
- WASM 템플릿에
0xdead_beef플레이스홀더를 심고 CLI가 패치하는 방식으로 재컴파일 없이 인덱스를 교체한다 - Levenshtein 오토마톤으로 오타 허용 검색을 FST 순회와 동시에 처리한다
- 3,700개 문서 기준 2.7MB, 0.4ms. 서버 비용과 API 키는 없다
- 정적 사이트, 오프라인 지원, 비용 제약 환경에서 Algolia와 Lunr.js의 실질적 대안이다
참고:
- Docfind 소개 글: https://code.visualstudio.com/blogs/2026/01/15/docfind
관심 있을 만한 포스트
V8 WasmGC 투기적 최적화 — 가상 메서드를 인라인으로 만드는 법
V8이 WasmGC의 가상 메서드 디스패치에 투기적 인라이닝을 도입해 Dart와 Java 앱에서 최대 8% 성능을 끌어낸 방법.
VS Code 팀의 AI 에이전트 병렬화 — 월간 릴리스를 주간으로 만든 워크플로우
VS Code 팀이 월간 릴리스에서 주간 릴리스로 전환한 비결. 에이전트 세션 병렬화, 자동화 파이프라인, 품질 게이트 설계 전반을 공개했다.
VS Code 1.110 — 에이전트가 생각하고, 브라우저를 열고, 터미널을 본다
2026년 2월 VS Code 1.110이 AI 에이전트 경험을 한 단계 끌어올린 핵심 기능 6가지를 분석한다.
Long-Distance NES — VS Code Copilot이 커서 너머까지 코드를 고치는 방법
VS Code Copilot의 Next Edit Suggestions가 파일 전체로 확장되면서, 멀리 떨어진 코드도 자동으로 제안하는 기술적 배경을 분석한다.
Rolldown의 코드 스플리팅 — 비트셋 한 줄로 모듈의 소속을 결정하는 법
Vite의 차세대 번들러 Rolldown이 비트셋 기반 알고리즘으로 코드 스플리팅을 수행하는 원리를 분석한다.
VS Code 1.109 — 에디터 하나에서 Claude, Codex, Copilot을 동시에 돌리는 시대
VS Code 1.109가 도입한 멀티 에이전트 개발 환경의 3가지 실행 모드와 MCP Apps 지원을 분석한다.
뱀의 탈피에서 배운 서버 재시작 — Rust로 커넥션 제로 로스를 구현하는 ecdysis
Cloudflare가 5년간 프로덕션에서 검증한 Rust 무중단 재시작 라이브러리 ecdysis를 오픈소스로 공개했다.
Next.js 블로그 만들기 — 정적 블로그에 검색 기능 추가
빌드 타임 검색 인덱스 생성과 클라이언트 사이드 필터링으로 정적 블로그에 검색 기능을 구현하기. Cmd+K 단축키, 오버레이 UI까지.