Pagefind는 정적 웹사이트를 위해 설계된 클라이언트 사이드 검색 라이브러리이다. 정적 사이트 생성기(SSG)로 만든 웹사이트나 Next.js, Gatsby, Hugo, Jekyll 등의 프레임워크로 구축된 사이트에 강력한 검색 기능을 추가할 수 있게 해준다. 외부 API나 서비스키 없이 로컬에서 설치 및 설정이 가능하기 때문에 아주 쉽고 빠르게 적용해볼 수 있다.
작동 방식
Pagefind는 다음과 같은 프로세스로 작동한다.
- 인덱싱 단계: 사이트 빌드 후 HTML 파일을 분석하여 검색 인덱스를 생성한다. 이 단계에서는 텍스트 콘텐츠, 제목, 메타데이터 등이 추출되고 처리되며, 인덱싱된 데이터를 이용할 수 있게 해주는 js 파일이 생성된다. 이 말은 즉, 빌드시 html을 만들어내는 경우에만 사용이 가능하다는 뜻이다.
- 검색 API: 인덱스 생성 후, 제공된 JavaScript API를 사용하여 사이트에 검색 인터페이스를 구현할 수 있다.
- UI 생성: 사용자가 검색어를 입력하면 Pagefind는 미리 생성된 인덱스를 사용하여 관련 페이지와 섹션을 빠르게 찾아 결과를 제공하고, 우리는 이 결과를 이용해 UI만 구현하면 된다.
다양한 기능과 사용법은 Docs에 나와있다.
Getting Started with Pagefind | Pagefind — Static low-bandwidth search at scale
빠른 시작!
script 작성
거두절미하고 바로 적용해보자. 어떤 패키지도 설치할 필요 없이 아래 내용을 따라가면 된다.개발 환경은 Next.js 15
와 pnpm
을 사용하고 있다.
우선 package.json
에 postbuild
를 추가한다.
"scripts": {
// ...
"postbuild": "npx pagefind --site .next --output-path public/pagefind",
// ...
},
인덱싱이 되려면 html을 생성해야 하고, html이 생성되려면 next가 빌드되어야 한다는 점을 유념하자. 해당 스크립트를 추가하고, 빌드를 해보면 public 폴더 아래 pagefind관련 파일들이 생기는 것을 확인할 수 있다.
이제 여기서 생긴 pagefind.js
만 import해서 사용하면 된다.
API 호출
우선 전체 코드는 아래와 같다.
export default function Search() {
const [search, setSearch] = useState("");
const [results, setResults] = useState<PagefindResult[]>([]);
const [pagefind, setPagefind] = useState<any>(null);
useEffect(() => {
const initPagefind = async () => {
try {
// 런타임에 동적으로 로드 시도
setPagefind(
await import(
// @ts-expect-error
"./pagefind/pagefind.js"
),
);
} catch (error) {
console.error("Pagefind 초기화 실패:", error);
}
};
// 클라이언트 사이드에서만 실행
if (typeof window !== "undefined") {
initPagefind();
}
}, []);
const handleSearch = async (e: any) => {
setSearch(e.target.value);
if (!pagefind || e.target.value === "") {
setResults([]);
return;
}
const search = await pagefind.search(e.target.value);
const results = await Promise.all(search.results.map((r) => r.data()));
console.log(results);
setResults(results);
};
return (
<div>
<input
type="text"
value={search}
onChange={handleSearch}
placeholder="검색어를 입력하세요..."
/>
<div>
{results.map((result, i) => (
<div key={result.url}>
<a href={result.url}>{result.meta.title}</a>
</div>
))}
</div>
</div>
);
}
UI 생성
이제 호출을 했으니 결과를 살펴보자.
주로 살펴볼 수 있는 항목은 아래와 같을 것이다.
- excerpt: 컨텐츠 내에서 키워드를 가지는 부분을 일부 파싱한 것이다.
- meta: 컨텐츠 내에서 meta 정보를 가져온다. title의 경우 가장 처음 만나는
h1
태그, 이미지는h1
태그 이후에 가장 처음 만나는image
태그이다.
모두 추가 옵션등을 통해 커스텀하게 가져올 수 있으니 Docs를 잘 살펴보자!
문제 발생
내 블로그는 한국어, 영어, 중국어, 일본어 총 4개의 언어를 제공하고 있는데, 검색시 모든 언어의 글이 나타나고 있었다.
이런 다국어 페이지는 일반적인 경우이기 때문에 당연히 pagefind에서도 Multilingual search 기능을 제공하고 있다. 근데 이를 판단하는 기준이 html
태그의 lang
속성에 어떤 값이 있느냐로 판단을 하고 있었다.
하지만 Next.js에서는 Layout에서 그 설정을 해야하는데, 내가 찾아 보았을 때는 정적 페이지 빌드시 lang 값을 가져올 수 있는 방법이 없었다.. (있다면 제발 알려주세요)
해결 방법
그래서 우회적인 방법으로 pagefind의 filter 기능을 이용하여 해결하였다. 각 페이지의 h1
태그에 아래와 같이 추가 속성을 부여했다.
<h1
data-pagefind-filter="lang[data-lang]"
data-lang={params.lang}
className="text-3xl md:text-5xl font-bold"
>
{title}
</h1>
그리고 API 요청을 할때 filters
를 추가하면 된다.
const search = await pagefind.search(e.target.value, {
filters: {
lang: "ko",
},
});
마치며
작고 가벼운 라이브러리 pagefind 를 통해 검색 기능을 빠르게 구현해보았다. 외부 서비스에 의존하지 않고 자체적으로 검색 인덱스를 생성하니 프라이버시와 제어 측면에서도 장점이 있고, 또한 사용자 경험 측면에서도 빠른 응답 속도를 제공하니 블로그나 문서 사이트를 운영할경우 매우 유용할듯 하다.