[Firebase 웹] 파이어스토어 검색 기능 구현하기(feat. 쿼리문 & Algolia)

2022. 6. 19. 10:23Firebase

728x90

이번 포스트에서는 파이어베이스에서 검색을 구현하는 방법에 대해 알아보도록 하겠습니다.

 

일단 가장 중요한 부분을 짚고 넘어가자면, 현재 파이어스토어에서는 문서의 전체 텍스트 검색을 자체적으로 지원하지는 않고 있습니다. 이를 관련해서도 각종 포럼이나 스택오버플로우에서 MongoDB나 다른 noSQL 데이터베이스처럼 왜 제대로 된 검색을 지원하지 않냐는 반발이 많긴 하지만 파이어베이스가 나온 지 10년이 되어가는 시점에도 아직 관련 정보가 없는 것으로 보아 검색은 우선순위는 아닌 것으로 판단됩니다.

 

그렇기 때문에 파이어베이스를 사용하는 개발자들이 검색을 구현하기 위해선 쿼리문을 변형해서 사용하거나 공식문서에 나와있듯이 타사 솔루션인 Algolia나 Elastic 같은 검색엔진 API를 사용해야 합니다.


1. 쿼리문 사용하기

타사 솔루션을 사용하지 않고 파이어베이스에서 제공하는 것만으로 검색을 구현한다면 한정적이지만 쿼리문을 사용하는 방법이 있습니다.

 


startAt, endAt

가장 먼저 orderBy를 사용해 특정 필드(제목)로 정렬 후 startAtendAt로 검색 키워드가 포함되어 있는 문서를 찾을 수 있습니다. 이전 포스트에서는 페이지네이션을 구현하기 위해 startAt에 페이지 커서로 사용할 문서 자체를 남겨 주었지만 문자를 넘겨주는 방법도 유효하기 때문에 아래와 같이 작성할 수 있습니다.

 

const keyword = "검색어";
const q = query(
  collection(db, "post"), // 포스트 컬렉션
  orderBy("title"), // 제목 정렬
  startAt(keyword),
  endAt(keyword + "\uf8ff")
);
const resSnap = await getDocs(q);

 

 

endAt의 인수로 검색하고자 하는 키워드에 "\uf8 ff"를 더해주는 이유는 해당 문자는 유니코드 범위에서 상당히 높은 코드 포인트이기 때문에 검색하고자 하는 키워드 뒤에 어떠한 문자열이 와도 쿼리 범위 내에 있도록 해주기 때문입니다. 

예를 들어서 제목이 bookstore라는 포스트 문서를 찾고자 했을 때 book이라는 문자열만 넘겨줘도 book + \uf8 ff 범주 내에 bookstore 제목을 가진 문서가 존재하기 때문에 검색이 가능합니다. 


where

해당 방법을 아래와 같이 where문으로도 작성할 수 있습니다.

 

const q = query(
  collection(db, "post"), // 포스트 컬렉션
  where("title", ">=", keyword),
  where("title", "<=", keyword + "\uf8ff"),
);

const resSnap = await getDocs(q);

 

 

만약 소규모 프로젝트를 진행하고 있으시다면 위와 같이 쿼리문으로도 검색을 구현하셔도 크게 문제가 되진 않을 것으로 예상합니다. 하지만 조금 규모가 크고 검색이 중요시되는 프로젝트라면 당연히 해당 방법으로는 한계가 있습니다. 이미 쿼리문만 보고 알아내신 분들도 있겠지만 해당 방법은 제목(title) 필드처럼 단일 필드 대상으로 검색을 하기 때문에 여러 필드 대상 (내용이나 메타데이터)으로 검색이 불가능하고 무조건 prefix 검색만 가능합니다. 다시 한번 bookstore라는 제목을 가진 문서를 예로 들어보자면 위 방식대로 쿼리문을 작성할 경우 'boo', 'book', 'books' 같은 키워드로는 해당 문서를 찾을 수 있지만 "ook", "store"처럼 중간에 끼어있는 문자열이나 뒤에 오는 문자열 대상으로 검색이 불가능합니다.

 


array-contains

만약 나는 죽어도 Algolia나 Elastic 같은 검색엔진 API를 사용하기 싫고 위 한계점이 극복된 방법을 사용하고 싶다면 검색용으로 사용할 필드를 배열로 직접 나열하는 방법도 있습니다.  

 

또 한 번 bookstore라는 제목(title)의 문서에 "i like bookstore"라는 내용(content)이 있다고 가정을 해보겠습니다. 앞서 설명드린 방법으로는 제목과 내용을 동시에 검색하지 못했지만 keyword라는 필드에 검색 가능한 키워드가 전부 담겨 있다면 어떨까요?

 

파이어스토어 포스트 문서 구조

 

위와 같은 문서 구조를 가진 컬렉션 대상으로는 배열에 해당 문자열이 존재하는지에 대한 쿼리를 array-contains로 확인해주기만 하면 됩니다.

const q = query(
  collection(db, "post"), 
  where("keyword", "array-contains", keyword.toLowerCase()),
);

const resSnap = await getDocs(q);

 

위 예제는 예시로 만들었기 때문에 keyword가 6개밖에 없지만 좀 더 정교한 검색을 위해서는 여러 개의 keyword를 생성해주면 됩니다. 보시다시피 이 방법을 사용하기 위해서 가장 중요한 포인트는 어떤 로직으로 keyword 배열을 생성하냐입니다. 어떤 방법으로 생성하면 좋을지 예시가 필요하다면 아래 영문 포스트를 참고해 보시기를 추천드립니다. 

 

 

Firebase Firestore Text Search and Pagination

Implementing text search using array-contains and lazy loading.

medium.com

 

물론 해당 방법을 사용한다면 앞서 언급했던 문제는 어느 정도 해결할 수 있지만 당연히 단점 또한 존재합니다. 아무리 효율적인 keyword 배열 생성 로직을 짠다고 해도 내용이 길어지게 되면 여러 문자열의 조합에 대응하기 위해서 어쩔 수 없이 배열의 크기는 커지게 됩니다. 현재 파이어 스토어의 최대 문서 크기는 1mb인데 2~3 문장에 대응하기 위한 키워드 문자 조합을 작성하기만 해도 이 수치를 넘어가게 됩니다. 그렇기 때문에 해당 방법은  문장 검색이 아닌 단어 위주 검색 (위 포스트에 사용한 예시처럼 사용자 이름이나 성) 같이 최대한 짧은 문자를 검색해야 할 때 한정적으로 사용할 수 있습니다.

 


2. Algolia 사용하기 

해당 방법을 사용하기 위해선 파이어베이스 무료 플랜인 Spark가 아닌 Blaze 요금제로 업그레이드를 하셔야 합니다. 간단한 프로젝트를 진행 중이라면 많은 요금이 청구될 일은 없습니다 (한 달에 100원 단위).

 

그럼 파이어베이스 공식문서에서 사용하도록 추천하고 있는 검색 제공업체인 Algolia를 사용하면 뭐가 좋은지 같이 알아보도록 합시다.

Elastic, Algolia, Typesence 중 Algolia를 사용

Algolia(알고리아)란?

알고리아는 다양한 검색 api를 제공해주는 검색 서비스 플랫폼입니다. 파이어스토어 데이터베이스의 경우 검색에 사용할 수 있는 인덱스가 따로 생성되지 않기 때문에 제대로 된 검색을 구현하기가 어려운데 어떻게 알고리아를 사용하면 이런 부분이 해결되는 걸까요? 답은 의외로 간단합니다. 인덱싱이 되는 새로운 알고리아 테이블에 저장을 해놓고 검색을 파이어스토어가 아닌 여러 가지 검색 api를 제공하는 알고리아를 통해 하는 것입니다. 이해를 돕기 위해 아래 다이어그램을 만들어보았습니다.

보시는 바와 같이 클라이언트에서 특정 문서에 대한 작업 요청이 들어가게 되면 Cloud Function이 이를 감지해 알고리아 테이블에도 인덱스가 추가된 복제본을 만들어 놓는 형식입니다. 파이어스토어에 1000개의 문서가 있으면 알고리아에도 1000개의 문서가 존재하게 되는 형식입니다. 그리고 이후에 검색 요청이 들어올 경우, 파이어스토어가 아닌 알고리아 테이블을 조회하고 해당 서비스에서 제공하는 강력한 검색 api를 통해서 정확하고 빠르게 검색 결과를 반환해줄 수 있습니다. 

 

알고리아를 사용할지 말지 고민하시는 분들의 의사결정을 돕기 위해 필자가 알고리아를 사용하면서 느꼈던 장단점을 나열해보자면 이렇습니다.

 

장점

 

- 인덱스가 만들어진 시점부터는 검색이 확실히 빠름

- 매우 정확한 검색 결과 (오타가 있더라도 검색 가능)

- 프론트엔드 라이브러리를 제공하기 때문에 세팅이 매우 간편함

- 어떤 검색 요청이 들어왔는지 확인 가능 (analytics)

 

단점

 

- 인덱스가 만들어지는 시간이 생각보다 오래 걸림 (문서가 10000개 이상일 경우)

- 조그만 규모가 커져도 가격이 부담되고 예측하기 어려움 (사이드 프로젝트 정도는 무료로 가능)

 


알고리아 세팅 & 확장프로그램 설치

 

필자는 이미 세팅해놓은 게 있긴 하지만 더 정확한 정보를 전달해드리기 위해 같이 처음부터 진행을 해보도록 하겠습니다. 일단 파이어 베이스 프로젝트 세팅은 완료되었다고 가정을 하고 (create react app) 진행을 할 것이기 때문에 아직 세팅을 못하신 분들은 예전에 올린 포스트를 참고해주시길 바랍니다.

 

 

[Firebase 웹] 파이어베이스 시작하기 - 웹 앱 초기설정

이번 포스트에서는 파이어 베이스에서 새 프로젝트를 생성하는 방법에 대해 알아보겠다. 마침 관리자 페이지를 만들어야하기 때문에 앞으로 Firebase 관련 예제는 React로 작성을 해보겠다. 일단

mingeesuh.tistory.com

 

파이어베이스 설정을 다하셨다면 알고리아도 설정을 해주고 API KEY를 받아오도록 하겠습니다. 아래 링크로 이동하셔서 이름이랑 사용처를 기재해주시면 빠르게 가입할 수 있습니다.

 

 

Site Search & Discovery powered by AI

Create AI-powered search & discovery across websites & apps.

www.algolia.com

 

기본 정보 입력이 완료되면 인덱스 이름을 적으라고 나올 텐데 파이어스토어 컬렉션 이름을 적어주시고 이미 문서 데이터가 있는 상태라면  JSON,CSV,TSV 파일로 저장한 뒤 수동으로 업로드해주시면 됩니다. 

 

필자는 아무것도 없는 백지상태에서 시작할 것이기 때문에 그냥 파이어 스토어에 데이터를 넣고, 추후에 설치할 확장 프로그램이 이를 감지해 알고리아 테이블에 업로드되도록 해보겠습니다.

 

요구하는 정보를 다 입력하시고 데이터 센터 (데이터가 저장될 위치)도 비교해보시고 가장 가까운 거리로 설정해주시고 API keys 메뉴에 Your API Keys에 있는 아래 키 정보를 적어두도록 합시다.

 

Application ID - 앱 고유 ID, 검색 요청 + 확장 프로그램 설치시 필요

Search-Only API Key - 추후에 검색 요청할 때 필요한 key

 

그리고 확장 프로그램을 설치하기 위해선 “addObject”, “deleteObject”, “listIndexes”, “deleteIndex”, “editSettings”, 그리고 “settings” 권한이 있는 하나의 API key를 더 만들어야 합니다. 

 

 

창이 뜨면 아래와 같이 입력해주시면 됩니다. ACL부분에 꼭 위에 언급한 권한을 주셔야 추후에 데이터 변화를 감지해서 알고리아 테이블에 데이터가 들어가기 때문에 6개 항목 전부 잘 입력되어있는지 다시 한번 확인해주시고 만들어주시길 바랍니다.

 

 

 

이제 파이어베이스 콘솔에서 extension을 하나 깔아보도록 하겠습니다. 위에서 언급했다시피 이 extension은 파이어스토어의 데이터를 감지하는 Cloud Function을 직접 작성하지 않기 위해 설치하는 것이기 때문에 직접 작성해보고 싶으신 분은 아래 포스트를 한번 읽어보시고 적용해보시길 바랍니다.

 

 

[Firebase 웹] Cloud Function 으로 파이어스토어 컬렉션과 문서 변화 (추가, 업데이트, 삭제) 감지

Cloud Function이 뭔지 잘 모르겠다던가 프로젝트에 세팅하는 방법을 알고 싶다면 아래 포스트를 참고하길 바란다. [Firebase 웹] Cloud Function이란? 초기 세팅하기 오늘은 Firebase 앱에 생기를 불어넣어

mingeesuh.tistory.com

 

직접 작성하신 분들은 이 부분을 스킵하시고 그렇지 않다면 파이어베이스 콘솔에 접속해서 Extensions탭에 Search With Algolia를 선택해서 설치해보겠습니다. 아마 많은 개발자분들이 사용하는 확장 프로그램이라 상단에 있겠지만 없다면 스크롤을 조금만 내리다 보면 나올것입니다.

 

설치를 해줍시다

 

설치를 누르시고 마지막 단계에서 아래와 같은 형식으로 입력해주시면 됩니다. 

 

 

설치를 누르시면 3-5분뒤에 확장프로그램 설치가 완료됩니다. 

 

제대로 적용이 되나 확인해보기 위해 KoreanJSON API로 아래와 같은 형식의 포스트 데이터를 200개정도 받아온뒤 파이어스토어 post 컬렉션에 넣어보도록하겠습니다. 추가한 방법은 해당 레포의 upload.js를 확인해주시길 바랍니다.

 

  {
    "id": 1,
    "title": "정당의 목적이나 활동이 민주적 기본질서에 위배될 때에는 정부는 헌법재판소에 그 해산을 제소할 수 있고, 정당은 헌법재판소의 심판에 의하여 해산된다.",
    "content": "모든 국민은 인간으로서의 존엄과 가치를 가지며, 행복을 추구할 권리를 가진다. 모든 국민은 종교의 자유를 가진다. 국가는 농·어민과 중소기업의 자조조직을 육성하여야 하며, 그 자율적 활동과 발전을 보장한다. 모든 국민은 양심의 자유를 가진다. 누구든지 체포 또는 구속을 당한 때에는 즉시 변호인의 조력을 받을 권리를 가진다.",
    "createdAt": "2019-02-24T16:17:47.000Z",
    "updatedAt": "2019-02-24T16:17:47.000Z",
    "UserId": 1
  }

 

파이어스토어에 추가된걸 확인하셨으면  Algolia 콘솔에도 접속해서 post 인덱스에 문서들이 생성되었는지 확인해주시면 됩니다.

 

200개가 성공적으로 생성된걸 확인할 수 있습니다.

 

이제 세팅은 이걸로 마무리 되었고 본격적으로 검색을 사용해볼수 있게 되었습니다.


알고리아 API로 검색 구현하기 (feat. React)

앞서 언급했다시피 알고리아의 장점중 하나는 바로 사용해볼수 있는 컴포넌트를 제공해주기 때문에 요구하는 값만 잘 넘겨주면 쉽게 사용하실수 있습니다. 그럼 알고리아에서 제공해주는 컴포넌트와 훅을 사용하기 위해 설치를 해주겠습니다. 필자는 검색을 할때마다 바로바로 검색 결과를 반환해주는 Instant Search를 사용할것이기 때문에 아래와 같이 설치를 해주겠습니다.

 

npm install algoliasearch react-instantsearch-hooks-web

 

import algoliasearch from "algoliasearch/lite";
import { InstantSearch } from "react-instantsearch-hooks-web";

const searchClient = algoliasearch(
  process.env.REACT_APP_ALGOLIA_ID,
  process.env.REACT_APP_ALGOLIA_SEARCH_KEY
); // 환경 변수로 관리

const App = () => {
  return (
    <InstantSearch searchClient={searchClient} indexName="post">
	 {/* ... */}
    </InstantSearch>
  );
};

export default App;

 

위와 같이 앱을 알고리아와 연동 시켜주는 역활을 해주는 InstantSearch 컴포넌트를 불러와 앞서 적어둔 ID키와 검색용 키값을 searchClient와 인덱스이름을 prop으로 넘겨주었습니다. 여러개의 인덱스가 있으시다면 InstantSearch 개별 컴포넌트로 관리하셔서 재사용하시는걸 추천드립니다. 이 예제에서는 한눈에 보실수 있도록 그냥 App에 작성하겠습니다. 이제 주석처리된 Instant Search 사이에는 사용하고자 하는 hook이나 컴포넌트를 추가해서 사용하시면 됩니다. 어떤 훅이랑 컴포넌트를 제공하는지는 알고리아 문서를 확인해주세요.

 

일단은 검색을 하기 위한 SearchBox랑 반환된 문서를 어떤식으로 보여줄건지 설정할 수 있는 Hits 컴포넌트를 넣어보겠습니다.

 

import algoliasearch from "algoliasearch/lite";
import { InstantSearch, SearchBox, Hits } from "react-instantsearch-hooks-web";

const searchClient = algoliasearch(
  process.env.REACT_APP_ALGOLIA_ID,
  process.env.REACT_APP_ALGOLIA_SEARCH_KEY
);

const App = () => {
  const Post = ({ hit }) => {
    return (
      <article>
        <h1>{hit.title}</h1>
        <p>{hit.content}</p>
      </article>
    );
  };

  return (
    <InstantSearch searchClient={searchClient} indexName="post">
      <SearchBox />
      <Hits hitComponent={Post} />
    </InstantSearch>
  );
};

이제 실행을 한번 해보시면 검색창이 생겼을것이고 해당 창에 검색을 해보시면 앞서 설정해놓은 Indexable Fields에 명시해둔 필드에 대해서 전체 검색이 가능하게 됩니다.

 

그외에도 Highlight 컴포넌트로 매칭된 부분을 하이라이트 할수 있고, Configure로 몇개의 검색결과를 반환할껀지 (기본 20개) 또는 Pagination 으로 페이지네이션 처리도 아래와 같이 쉽게 가능합니다.

 

import algoliasearch from "algoliasearch/lite";
import {
  InstantSearch,
  SearchBox,
  Hits,
  RefinementList,
  Highlight,
  Configure,
} from "react-instantsearch-hooks-web";

const App = () => {
  const Post = ({ hit }) => {
    return (
      <article>
        <h1>
          <Highlight attribute="title" hit={hit} />
        </h1>
        <p>
          <Highlight attribute="content" hit={hit} />
        </p>
      </article>
    );
  };

  return (
    <InstantSearch searchClient={searchClient} indexName="post">
      <SearchBox />
      <RefinementList attribute="UserId" />
      <Hits hitComponent={Post} />
      <Configure hitsPerPage={10} />
    </InstantSearch>
  );
};

실행을 해보시면 아래와 같이 검색이 잘되는걸 확인하실수 있습니다. 이후에는 css만 잘 적용해서 앱에 맞게 잘 꾸며주시면 되겠습니다.

 

 


이번 포스트에서는 파이어베이스에서 검색을 구현하는 방법에 대해서 알아보았는데요. 혹시 도중에 문제가 있으시거나 궁금하신점이 있다면 댓글로 남겨주시길 바랍니다. 포스트가 생각보다 길어져서 알고리아측에서 제공하는 컴포넌트를 사용해서 간단하게만 구현을 해보았기 때문에 조금 더 구체적인 필터링이라던지 커스터마이징 방법에 대해서 궁금하신분들이 있다면 댓글을 남겨주세면 따로 더 디테일한 사용방법을 담아 포스트를 준비해보도록 하겠습니다.