Gatsby - 검색 기능 구현하기
7 min read
검색 기능 구현하기
블로그에 글이 많아질 경우 원하는 게시물을 찾기 힘들어질 것입니다. 이에 검색 기능은 블로그에 있어서 필수적이라는 생각이 들었습니다. 그래서 Gastby에서 어떻게 하면 검색 기능을 구현할 수 있을지 알아봤습니다.
검색 기능과 관련하여 Gatsby 공식 문서 Adding Search에서 제안하는 방식이 있습니다.
Client-side serach에서의 js-search
, API-based search engine에서의 Algolia
, Meilisearch
, Typesense
등을 제안합니다.
우선, 저의 상황을 고려했을 때 검색 기능은 처음 구현해보는 기능이어서 API-based search engine 방식은 빠르게 도입하기 어려울 것이라는 판단을 했습니다. 그래서 Client-side search로 방향을 잡고 다른 사람들은 어떻게 구현했나 살펴봤습니다.
다행히도 js-search
보다 더 쉽게 검색 기능을 도와주는 플러그인이 존재했습니다.
gatsby-plugin-fusejs을 사용하면 쉽고 빠르게 검색 기능을 구현할 수 있습니다.
gatsby-plugin-fusejs 설정하기
우선 해당 플러그인을 설치합니다.
yarn add gatsby-plugin-fusejs
gatsby-config 파일에서 관련된 설정을 해줍니다.
{
resolve: 'gatsby-plugin-fusejs',
options: {
query: `
{
allMdx {
nodes {
id
frontmatter {
title
date
slug
category
}
excerpt
}
}
}
`,
keys: ['frontmatter.title', 'excerpt'],
normalizer: ({ data }) =>
data.allMdx.nodes.map((node) => ({
id: node.id,
frontmatter: {
title: node.frontmatter.title,
slug: node.frontmatter.slug,
date: node.frontmatter.date,
category: node.frontmatter.category,
},
excerpt: node.excerpt,
})),
},
},
코드를 구체적으로 살펴보면 다음과 같습니다.
query: `
{
allMdx {
nodes {
id
frontmatter {
title
date
slug
category
}
excerpt
}
}
}
`,
query에서는 만들고자 하는 데이터를 받습니다.
keys: ['frontmatter.title', 'excerpt'],
keys에서는 검색에 사용할 부분을 인덱스로 정합니다. Fuse.createIndex
저는 제목인 title과 게시물별로 미리볼 수 있는 텍스트인 excerpt로 정했습니다.
normalizer: ({ data }: any) =>
data.allMdx.nodes.map((node: any) => ({
id: node.id,
frontmatter: {
title: node.frontmatter.title,
slug: node.frontmatter.slug,
date: node.frontmatter.date,
category: node.frontmatter.category,
},
excerpt: node.excerpt,
})),
normalizer
는 쿼리에서 반환된 데이터를 정규화하는 함수라고 설명되어 있습니다.
GraphQL의 결과물로 확인할 수 있습니다.

index에는 앞서 keys에 넣었던 데이터들이 검색으로 활용될 수 있도록 변형된 것을 확인할 수 있습니다.

Gatsby에서 fuse.js 적용하기
gatsby-plugin-fusejs에서는 react-use-fusejs와 함께 사용할 것을 추천합니다. 하지만 저의 경우 react-use-fusejs와 함께 사용할 시 에러가 발생하더라구요.😂
그래서 react-use-fusejs 내부 코드를 찾아봤습니다.
내부 코드를 살펴봤을 때 useGatsbyPluginFusejs
함수만 사용하는 것 같았습니다.
이 정도면 제가 커스텀해서 사용해도 괜찮겠다는 생각을 하게 되었습니다.
export function useGatsbyPluginFusejs<T>(
query: string,
fusejs: { data: T[]; index: string },
fuseOpts?: Fuse.IFuseOptions<T>,
parseOpts?: Fuse.FuseIndexOptions<T>,
searchOpts?: Fuse.FuseSearchOptions,
) {
const [instance, setInstance] = useState<null | Fuse<T>>(null)
useEffect(() => {
if (!fusejs?.data || !fusejs?.index) {
setInstance(null)
return
}
const inst = new Fuse<T>(
fusejs.data,
fuseOpts,
Fuse.parseIndex(JSON.parse(fusejs.index), parseOpts),
)
setInstance(inst)
}, [fusejs])
return useMemo(() => {
if (!query || !instance) {
return []
}
return instance?.search(query, searchOpts) || []
}, [query, instance])
}
여기서 제가 필요한 부분만 골라서 사용하면 될 것 같습니다.
코드를 하나씩 살펴봤을 때 new Fuse
에서 만든 데이터가 search
메서드를 통해 검색 결과로 변환되는 것 같네요.
Fuse.parseIndex는 문서와 코드의 흐름을 봤을 때 GraphQL에서 만들었던 index로 보입니다.
어떠한 흐름인지 파악했으니 이 부분을 저에게 맞게 작업하면 될 것 같습니다.
이를 위해 fuse.js를 설치합니다.
yarn add fuse.js
useSearch.ts
import Fuse from 'fuse.js'
import { useMemo } from 'react'
export default function useSearch<T>(
query: string,
fusejs: { data: T[]; index: string },
) {
const fuse = useMemo(() => {
return new Fuse()<T>(
fusejs.data,
undefined,
Fuse.parseIndex(JSON.parse(fusejs.index)),
)
}, [fusejs])
return fuse.search(query)
}
해당 훅은 정말 간소화한 버전임을 참고하시기 바랍니다.😅
이렇게 만든 훅을 검색 페이지에 사용하면 되겠습니다.
search.tsx
import styled from '@emotion/styled'
import App from 'App'
import { Layout, Post, PostList } from 'components'
import { Flex } from 'components/@shared'
import { graphql, useStaticQuery } from 'gatsby'
import useIntersectionObserver from 'hooks/useIntersectionObserver'
import useSearch from 'hooks/useSearch'
import React, { useState } from 'react'
import { Content } from 'types/content'
interface searchPost {
fusejs: {
index: string
data: Content[]
}
}
const Search = () => {
const search: searchPost = useStaticQuery(graphql`
{
fusejs {
index
data
}
}
`)
const [query, setQuery] = useState('')
const nodes = useSearch(query, search.fusejs)
const { ref, page } = useIntersectionObserver(nodes.length)
return (
<App>
<Layout>
<Flex flexDirection="column" gap={20}>
<Input
placeholder="Search"
value={query}
onChange={(e) => setQuery(e.currentTarget.value)}
/>
{query !== '' && (
<span>
'<strong>{query}</strong>' 검색 결과 ({nodes.length}개)
</span>
)}
<PostList
render={nodes.slice(0, page).map((node) => (
<Post key={node.refIndex} node={node.item} />
))}
/>
<div ref={ref} />
</Flex>
</Layout>
</App>
)
}
export default Search
const Input = styled.input`
border-radius: 10px;
width: 100%;
padding: 10px 20px;
border: 3px solid ${({ theme }) => theme.colors.secondary.light};
&:focus {
outline: none;
}
`
마무리

플러그인의 도움을 통해 검색 기능도 쉽고 빠르게 할 수 있었습니다. 해당 과정에서는 플러그인 내부의 코드도 살펴보며 커스터마이징하는 과정을 거쳤습니다. 다행히 성공적으로 구현하여 만족스럽네요.
이번 글을 통해 Gatsby에서 검색 기능을 구현하려는 분들에게 도움이 되었으면 합니다.🙇♂️