Pagefind를 30분에 통합 — 인라인 모달 + dev 동작까지
Astro 정적 사이트에 Pagefind 검색을 통합하면서 만난 함정 두 개 (dev에서 인덱스 404, trailingSlash 충돌)와 인라인 모달 UX 패턴 (/단축키 + ESC + lazy load).
CPATOOLS 사이트에 검색 기능이 필요해졌다. 도구가 늘고 블로그가 늘면서 “어떤 글에서 본 거였더라” 상황이 생기기 시작.
Pagefind를 골랐다. 정적 사이트 전용 검색 인덱서. 빌드 결과물(dist/)을 스캔해서 인덱스를 만들고 클라이언트에서 검색이 동작한다. 서버·DB 필요 없음.
통합은 30분만에 끝났는데, 함정 두 개가 있었다. 정리해둔다.
통합 절차
1. 설치 + 빌드 스크립트
{
"scripts": {
"build": "astro build && pagefind --site dist --output-subdir pagefind"
},
"devDependencies": {
"pagefind": "^1.5.2"
}
}
2. detail 페이지에 마커
검색 인덱싱 대상 페이지의 본문에 data-pagefind-body를 달아준다.
<article
data-pagefind-body
data-pagefind-meta={`type:블로그`}
>
<Content />
</article>
data-pagefind-meta는 결과 필터에 쓸 메타데이터.
3. 검색 페이지
/search 에 PagefindUI를 마운트.
<div id="search"></div>
<link href="/pagefind/pagefind-ui.css" rel="stylesheet" />
<script is:inline src="/pagefind/pagefind-ui.js"></script>
<script is:inline>
window.addEventListener('DOMContentLoaded', () => {
new window.PagefindUI({
element: '#search',
showImages: false,
translations: { placeholder: '도구, 글, AI 서비스 검색…' },
});
});
</script>
CSS 변수로 사이트 톤에 맞춰 스타일도 가능 (--pagefind-ui-primary, --pagefind-ui-background 등).
여기까지가 30분. 결과 검증 (한국어 36 페이지, 1,922 단어 인덱싱 확인) 후 다음 두 함정.
함정 1 — dev 서버에서 인덱스 404
pagefind --site dist --output-subdir pagefind는 dist/pagefind/에 인덱스를 출력한다. astro build 후에만 존재. npm run dev로 띄우면 dist/를 안 쓰니 /pagefind/pagefind-ui.js가 404.
해결: 빌드 시점에 public/pagefind/로도 출력. Astro의 public/은 dev에서 그대로 서빙된다.
"build": "astro build && pagefind --site dist --output-subdir pagefind && pagefind --site dist --output-path public/pagefind"
.gitignore에 public/pagefind/ 추가 (빌드 산출물). 이제 npm run build 한 번 돌리면 dev 서버에서도 검색이 동작한다.
함정 2 — trailingSlash 충돌
CPATOOLS는 astro.config.mjs에서 trailingSlash: 'never'로 설정했다. URL 끝에 / 안 붙이는 정책.
근데 Pagefind는 인덱싱할 때 빌드 산출물을 그대로 읽어서 /blog/foo/ 형태로 저장한다. PagefindUI에서 결과 클릭 → /blog/foo/ 이동 → Astro가 /foo로 redirect 시도 → 404.
해결: PagefindUI의 processResult 콜백에서 trailing slash 제거.
new window.PagefindUI({
element: '#search',
processResult: function (result) {
if (result?.url && result.url !== '/' && result.url.endsWith('/')) {
result.url = result.url.replace(/\/$/, '');
}
if (result?.sub_results) {
for (const sub of result.sub_results) {
if (sub.url && sub.url !== '/' && sub.url.split('#')[0].endsWith('/')) {
const [path, hash] = sub.url.split('#');
sub.url = path.replace(/\/$/, '') + (hash ? '#' + hash : '');
}
}
}
return result;
},
});
UX 진화: 페이지 → 인라인 모달
처음엔 헤더 검색 아이콘이 /search 페이지로 이동하는 형태였다. 한 번 사용해보고 거슬렸다 — “검색 한 번 하려고 페이지 이동하는 거 토스 같은 사이트는 안 그러는데.”
인라인 모달로 변경:
- 헤더 검색 아이콘 클릭 → 모달 토글
/단축키로 모달 열기ESC/ 백드롭 클릭으로 닫기- PagefindUI는 모달 첫 오픈 시 lazy load (페이지 로드 비용 절감)
- 모바일 햄버거 메뉴의 “검색” 항목도 같은 모달로 통합
let mounted = false;
async function ensureMounted() {
if (mounted) return;
mounted = true;
await Promise.all([
loadAsset('link', { rel: 'stylesheet', href: '/pagefind/pagefind-ui.css' }),
loadAsset('script', { src: '/pagefind/pagefind-ui.js' }),
]);
new window.PagefindUI({ element: '#header-search', /* ... */ });
}
trigger.addEventListener('click', () => {
modal.classList.remove('hidden');
ensureMounted().then(() => {
modal.querySelector('input.pagefind-ui__search-input')?.focus();
});
});
페이지 이동이 사라지니 검색이 빨라졌다.
회고
- Pagefind는 진짜 빠르다. 36 페이지 + 1,922 단어 인덱싱이 0.25초.
- 한국어는 stemming이 안 되지만 (Pagefind 경고) 실용에는 문제없음. 정확한 키워드 매칭으로 충분.
- dev에서 동작 안 하면 사용자(나)가 안 쓴다. 함정 1을 안 풀었으면 검색 기능이 라이브 후 첫날만 점검하고 잊혔을 것.
- 인라인 모달 + 단축키가 페이지 이동보다 5배 빠른 UX. 토스/Linear/GitHub Pro 다 같은 패턴인 이유.
CPATOOLS 사이트의 / 단축키 한번 눌러보시길.
댓글을 불러오는 중...