본문으로 건너뛰기
CPATOOLS

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 pagefinddist/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"

.gitignorepublic/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 사이트의 / 단축키 한번 눌러보시길.

automation ai

댓글

댓글을 불러오는 중...