Pagefind 완전 설명 — Rust + WASM 정적 검색 엔진

한 줄 정의

정적 사이트(Static Site)를 위한 서버리스 전문 검색 엔진. Rust로 작성되고 WebAssembly로 브라우저에서 실행된다.


1. 탄생 배경

정적 사이트(GitHub Pages, Netlify 등)는 서버가 없다. 서버가 없으니 검색 쿼리를 처리할 백엔드도 없다. 기존 해결책들의 문제:

기존 방법문제점
Algolia외부 서비스 의존, 유료, 데이터 외부 유출
lunr.js브라우저에 전체 인덱스 로드 → 글 수 늘면 수백 MB
Fuse.js전체 데이터 메모리 로드, 대규모 불가
Elasticsearch서버 필요, 정적 호스팅 불가

Pagefind는 이 모든 문제를 해결하기 위해 2022년 CloudCannon이 만들었다.


2. 핵심 작동 원리

2단계로 작동한다: 빌드 타임 + 런타임

빌드 타임 (인덱싱)

Zola/Jekyll 등으로 사이트를 빌드한 뒤 Pagefind CLI를 실행하면:

npx pagefind --site public
  1. public/ 안의 모든 .html 파일을 읽는다
  2. 각 페이지의 텍스트를 추출, 역색인(Inverted Index) 을 만든다
  3. 역색인을 알파벳/음절 단위 청크 파일로 분할해 저장한다
public/pagefind/
  ├── pagefind.js          (~30KB, WASM 로더)
  ├── pagefind.wasm        (Rust 컴파일 검색 엔진)
  ├── index_a.pf_index     ("a"로 시작하는 단어들의 역색인)
  ├── index_r.pf_index     ("rust", "rayon" 등)
  ├── index_n.pf_index     ("network", "node" 등)
  └── data_xxxxx.pf_meta   (문서 제목, URL, 스니펫)

런타임 (검색)

사용자가 "rust" 를 입력하면:

① pagefind.js 초기화 (첫 focus 시 1회만, ~30KB)
       ↓
② "r"로 시작하는 청크 파일 1개만 fetch (index_r.pf_index, ~10KB)
       ↓
③ WASM 바이너리에서 역색인 조회 (네이티브 속도)
       ↓
④ "rust" 포함 문서 ID 목록 + 관련도 점수 반환
       ↓
⑤ 상위 5개 문서의 메타데이터(제목, URL, 스니펫) fetch
       ↓
⑥ JavaScript가 결과를 DOM에 렌더링

이 과정에서 브라우저 메모리에 올라온 데이터: ~50KB — 글이 10개든 10만 개든 동일.


3. 왜 메모리가 항상 일정한가

lunr.js : 메모리 = O(N)   (글 수에 선형 비례)
Pagefind: 메모리 = O(1)   (검색 1회당 청크 1~2개 고정)

도서관 비유:

  • lunr.js: 도서관 모든 책의 색인을 통째로 복사해서 가방에 넣고 검색한다
  • Pagefind: 색인 카드를 ㄱ/ㄴ/ㄷ... 서랍으로 나눠두고, 해당 서랍 하나만 꺼낸다

역색인 구조:

빌드 타임 생성:
  "rust"    → [문서 B (8%), 문서 D (20%)]
  "base64"  → [문서 A (15%)]
  "network" → [문서 A (5%), 문서 C (12%)]

→ index_r.pf_index 에는 "r"로 시작하는 단어 역색인만 저장 (~10KB)

런타임:
  "rust" 입력
  → index_r 1개 fetch (~10KB)
  → WASM 조회
  → 결과 반환 (총 메모리: ~50KB)

4. 왜 Rust + WASM인가

항목JavaScriptRust → WASM
실행 방식JIT 컴파일 (매번 최적화)AOT 컴파일 (미리 최적화됨)
GC있음 (Pause 발생 가능)없음 (일정한 응답 속도)
역색인 파싱느림C 수준 속도
바이너리 크기wasm-opt으로 최소화
메모리 모델GC가 관리선형 메모리, 수동 관리
검색 벤치마크기준 1.0x2~10x 빠름

Pagefind의 .pf_index 파일은 JSON이 아닌 바이너리 포맷으로, WASM이 이를 파싱·조회하는 작업을 JS 대비 2~10배 빠르게 처리한다.

Rust를 선택한 이유:

이유설명
Zero-cost abstractions고수준 추상화가 런타임 오버헤드 없음
메모리 안전성GC 없이도 컴파일 타임에 메모리 오류 차단
wasm-pack 생태계Rust → WASM 변환 도구체인이 가장 성숙
병렬 인덱싱rayon 으로 빌드 타임 병렬 처리

5. 클라이언트 사이드 메모리 비교

검색 도구글 1,000개글 10,000개글 100,000개
lunr.js~5MB~50MB ❌수백 MB → 크래시 ❌
Fuse.js~3MB~30MB ❌수백 MB → 크래시 ❌
Pagefind (WASM)~50KB~50KB~50KB

6. 실제 성능 수치

규모전체 인덱스 크기검색당 로드첫 검색 응답
1,000글~50KB~5KB<100ms
10,000글~325KB~20KB~100ms
100,000글~4~8MB~50KB~300ms
도구첫 검색 (콜드)재검색 (캐시)메모리
lunr.js200~500ms~10ms~50MB
Fuse.js300~800ms~20ms~30MB
Pagefind50~150ms<5ms~1MB

7. 주요 특징

특징설명
한국어 지원--force-language ko 옵션
검색어 하이라이트결과에 매칭 단어 자동 강조
다국어언어별 인덱스 자동 분리
필터링특정 섹션/태그로 검색 범위 제한 가능
커스텀 메타data-pagefind-meta 속성으로 커스텀 데이터 인덱싱
페이지 제외data-pagefind-ignore 속성으로 특정 요소 제외
CDN 친화적청크 파일이 작아 CDN 캐싱 최적
서버 불필요순수 정적 파일만으로 동작

8. HTML 연동 방법

<!-- 검색 입력창 -->
<input type="text" id="pagefind-search" placeholder="Search...">
<div id="pagefind-dropdown"></div>

<script>
var pagefind = null;

// Focus 시 lazy load — 한 번만 실행
async function initPagefind() {
  if (pagefind) return;
  try {
    pagefind = await import('/pagefind/pagefind.js');
  } catch(e) {
    console.log('Pagefind not available');
  }
}

// 검색 실행
async function search(query) {
  if (!query || !pagefind) return;
  const result = await pagefind.search(query);
  const data = await Promise.all(
    result.results.slice(0, 5).map(r => r.data())
  );
  renderResults(data);
}

function renderResults(results) {
  const drop = document.getElementById('pagefind-dropdown');
  drop.innerHTML = results.map(r =>
    `<a href="${r.url}">${r.meta.title}</a>`
  ).join('');
}

document.getElementById('pagefind-search')
  .addEventListener('focus', initPagefind);

var timer;
document.getElementById('pagefind-search')
  .addEventListener('input', function() {
    clearTimeout(timer);
    timer = setTimeout(() => search(this.value.trim()), 200);
  });
</script>

특정 요소 인덱싱 제어

<!-- 이 요소만 인덱싱 (나머지 제외) -->
<article data-pagefind-body>
  ...본문...
</article>

<!-- 이 요소는 인덱싱에서 제외 -->
<nav data-pagefind-ignore>...</nav>

<!-- 커스텀 메타데이터 추가 -->
<span data-pagefind-meta="author">홍길동</span>

9. 한계

한계설명
오프라인 불가청크 파일을 네트워크에서 fetch해야 함
실시간 인덱싱 불가글 추가 시 반드시 재빌드 + 재배포 필요
로컬 개발 검색 불가zola/jekyll serve 환경에서는 인덱스가 없음
시맨틱 검색 불가키워드 매칭 기반 (AI 벡터 검색 아님)

10. 사용 적합성

상황Pagefind 적합도
GitHub Pages / Netlify 정적 사이트✅ 최적
기술 블로그 / 문서 사이트✅ 최적
대용량 콘텐츠 (10만 글+)✅ 가능
실시간 데이터 검색❌ Elasticsearch 사용
AI 시맨틱 검색❌ 벡터 DB 사용
동적 서버 사이트❌ 서버 검색 엔진 사용

🧒 어린이를 위한 설명

Pagefind는 도서관 사서 같은 존재예요.

  1. 사서가 색인 카드를 만든다 (빌드 타임): 도서관(사이트)이 완성되면, 사서(Pagefind)가 모든 책을 읽고 "이 책에는 'rust'라는 단어가 있어!"라고 카드를 ㄱ/ㄴ/ㄷ 서랍에 정리한다.

  2. 손님이 단어를 말하면 (런타임): "rust 찾아줘"라고 하면, 사서는 'ㄹ' 서랍(~10KB)만 열어서 바로 찾아준다. 책이 100만 권이어도 그 서랍만 열면 된다.

  3. 서버가 필요 없다: 사서(카드 색인)가 이미 정리돼 있어서, 도서관장(서버)이 없어도 손님이 직접 서랍을 열어볼 수 있다.


참고