Use Cases (업데이트: 2026. 6. 1.)

Claude Code로 검색 기능 구현하기: Postgres, Meilisearch, Algolia 실전 가이드

요구사항 프롬프트, 인덱스, 동기화 작업, 필터, debounce UI, 테스트까지 Claude Code 검색 구현을 정리합니다.

Claude Code로 검색 기능 구현하기: Postgres, Meilisearch, Algolia 실전 가이드

검색 기능은 입력창이 아니라 발견 경험이다

검색 기능은 사용자의 입력에 맞춰 후보를 찾고, 필터링, 정렬, 하이라이트까지 돌려주는 경험입니다. 단순히LIKE '%term%'로 목록을 반환하는 것은 내부 도구에서는 충분할 수 있지만, 블로그, 교육 콘텐츠, SaaS 문서처럼 PV와 전환이 중요한 화면에서는 부족합니다. 좋은 검색은 오래된 글을 다시 발견하게 만들고, 0건 검색어를 다음 콘텐츠 아이디어로 바꿉니다.

Masa가 콘텐츠 사이트 검색을 만들며 반복해서 배운 점은 UI부터 만들면 되돌림이 많다는 것입니다. 어떤 필드를 인덱싱할지, 비공개 글을 어떻게 제외할지, locale별 URL을 어떻게 만들지, 클릭 로그를 어떻게 개선에 쓸지 먼저 정해야 합니다. Claude Code는 빠르게 구현하지만, 얇은 요구사항을 주면 얇은 검색도 빠르게 만들어집니다.

관련 주제는 Claude Code Algolia 검색, Claude Code API 개발, Claude Code 성능 최적화를 함께 보면 좋습니다.

먼저 사용 사례를 나눈다

사용 사례예시중요한 점추천 구성
콘텐츠/문서 검색블로그, FAQ, 가이드제목 가중치, 요약, 태그, localePostgres 전체 텍스트 또는 Meilisearch
카탈로그 검색상품, 강의, 템플릿facet, 정렬, 동의어, 클릭 분석Meilisearch 또는 Algolia
관리자 검색고객, 청구, 로그권한, 정확한 필터, 감사 가능성Postgres 우선
다국어 검색한국어, 영어, 일본어 문서locale 분리, 현지 키워드Meilisearch 또는 Algolia

작은 사이트라면 Postgres 전체 텍스트 검색으로 시작해도 충분합니다. 오타 허용, facet, 더 나은 기본 랭킹이 필요해지면 Meilisearch가 자연스럽습니다. 검색이 매출이나 강의 구매에 직접 연결된다면 Algolia의 InstantSearch.jsReact InstantSearch가 강합니다.

Claude Code에 줄 요구사항 프롬프트

기존 Next.js 앱에 production search를 구현합니다.

목표:
- published article을 검색해 콘텐츠 회유를 늘린다.
- query, locale, category, tags 필터를 지원한다.
- title, summary, tags, body를 검색하고 title 가중치를 가장 높게 둔다.
- 결과 카드와 하이라이트에 필요한 필드를 반환한다.

제약:
- draft, private record, email, internal note, restricted content는 반환하지 않는다.
- admin key나 write key를 브라우저에 노출하지 않는다.
- UI는 300ms debounce와 AbortController를 사용한다.
- 0건 검색, 느린 검색, 클릭 결과를 로그로 남긴다.

산출물:
- Postgres full-text, Meilisearch, Algolia 비교 메모.
- index schema.
- sync job.
- /api/search route.
- React search UI.
- tests and rollout checklist.

Claude Code가 파일을 수정하기 전에 기존 DB schema, MDX frontmatter, 인증 규칙, URL 구조를 읽게 하세요. 검색 버그는 관련도 문제보다 데이터 노출 문제인 경우가 많습니다.

백엔드 선택 기준

PostgreSQL 공식 Full Text Searchtsvector, tsquery, ranking을 다룹니다. 권한 조건이 복잡하고 데이터가 이미 Postgres에 있다면 좋은 시작점입니다. Meilisearch의 quick startfiltering/sorting/faceting은 콘텐츠 검색을 빠르게 강화할 때 유용합니다. Algolia는 검색 UI, 분석, 전환 개선까지 함께 운영할 때 적합합니다.

Postgres 인덱스 schema

CREATE TABLE IF NOT EXISTS articles (
  id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
  slug text NOT NULL UNIQUE,
  locale text NOT NULL,
  status text NOT NULL CHECK (status IN ('draft', 'published', 'private')),
  title text NOT NULL,
  summary text NOT NULL,
  body text NOT NULL,
  category text NOT NULL,
  tags text[] NOT NULL DEFAULT '{}',
  popularity integer NOT NULL DEFAULT 0,
  updated_at timestamptz NOT NULL DEFAULT now(),
  search_vector tsvector GENERATED ALWAYS AS (
    setweight(to_tsvector('simple', coalesce(title, '')), 'A') ||
    setweight(to_tsvector('simple', coalesce(summary, '')), 'B') ||
    setweight(to_tsvector('simple', coalesce(array_to_string(tags, ' '), '')), 'B') ||
    setweight(to_tsvector('simple', coalesce(body, '')), 'C')
  ) STORED
);

CREATE INDEX IF NOT EXISTS articles_search_vector_idx
  ON articles USING GIN (search_vector);

CREATE INDEX IF NOT EXISTS articles_locale_status_idx
  ON articles (locale, status, updated_at DESC);

핵심은 제목, 요약, 태그, 본문에 다른 가중치를 주는 것입니다. 독자는 제목에 나온 단어를 더 관련 있는 결과로 기대합니다. 한국어 검색 품질을 더 올리려면 별도 형태소 분석 또는 외부 검색 엔진도 검토해야 합니다.

Meilisearch 동기화 작업

검색 엔진은 원본 데이터 저장소가 아닙니다. 공개 가능한 필드만 보내고, DB나 CMS를 source of truth로 둡니다.

// scripts/sync-meilisearch.ts
import "dotenv/config";
import { MeiliSearch } from "meilisearch";

type ArticleRecord = {
  id: string;
  title: string;
  summary: string;
  body: string;
  locale: string;
  status: "published";
  category: string;
  tags: string[];
  url: string;
  popularity: number;
  updatedAtTimestamp: number;
};

const client = new MeiliSearch({
  host: process.env.MEILISEARCH_HOST ?? "http://127.0.0.1:7700",
  apiKey: process.env.MEILISEARCH_ADMIN_KEY
});

const index = client.index<ArticleRecord>("articles");

await index.updateSettings({
  searchableAttributes: ["title", "summary", "body", "tags"],
  filterableAttributes: ["locale", "status", "category", "tags"],
  sortableAttributes: ["updatedAtTimestamp", "popularity"],
  displayedAttributes: ["id", "title", "summary", "locale", "category", "tags", "url"]
});

const task = await index.addDocuments(
  [
    {
      id: "ko_claude-code-search-functionality",
      title: "Claude Code로 검색 기능 구현하기",
      summary: "검색 백엔드 선택부터 UI, 테스트, 배포까지 다루는 실전 가이드.",
      body: "공개된 MDX 또는 CMS 본문에서 추출한 검색 대상 텍스트입니다.",
      locale: "ko",
      status: "published",
      category: "use-cases",
      tags: ["Claude Code", "검색 기능", "전체 텍스트 검색"],
      url: "/ko/blog/claude-code-search-functionality",
      popularity: 18,
      updatedAtTimestamp: 1780272000
    }
  ],
  { primaryKey: "id" }
);

console.log(`Queued Meilisearch task ${task.taskUid}`);

표시용 facet은 처음부터 많이 만들지 마세요. 콘텐츠 검색은category, tags, locale이면 충분한 경우가 많습니다. 권한 필터는 사용자가 조작할 수 없는 서버 쪽 조건으로 분리합니다.

Debounce가 있는 React UI

// components/ArticleSearchBox.tsx
"use client";

import { useEffect, useMemo, useState } from "react";

type SearchHit = {
  id: string;
  title: string;
  summary: string;
  url: string;
  category: string;
};

function useDebounce<T>(value: T, delayMs: number) {
  const [debounced, setDebounced] = useState(value);

  useEffect(() => {
    const timer = window.setTimeout(() => setDebounced(value), delayMs);
    return () => window.clearTimeout(timer);
  }, [value, delayMs]);

  return debounced;
}

export function ArticleSearchBox({ locale = "ko" }: { locale?: string }) {
  const [query, setQuery] = useState("");
  const [category, setCategory] = useState("");
  const [hits, setHits] = useState<SearchHit[]>([]);
  const [loading, setLoading] = useState(false);
  const debouncedQuery = useDebounce(query, 300);

  const params = useMemo(() => {
    const next = new URLSearchParams({ q: debouncedQuery, locale });
    if (category) next.set("category", category);
    return next;
  }, [category, debouncedQuery, locale]);

  useEffect(() => {
    if (debouncedQuery.trim().length < 2) {
      setHits([]);
      return;
    }

    const controller = new AbortController();
    setLoading(true);

    fetch(`/api/search?${params.toString()}`, { signal: controller.signal })
      .then((response) => {
        if (!response.ok) throw new Error("Search request failed");
        return response.json();
      })
      .then((data: { hits: SearchHit[] }) => setHits(data.hits))
      .catch((error) => {
        if (error.name !== "AbortError") console.error(error);
      })
      .finally(() => setLoading(false));

    return () => controller.abort();
  }, [debouncedQuery, params]);

  return (
    <section aria-label="Article search">
      <input
        aria-label="Search keywords"
        type="search"
        value={query}
        onChange={(event) => setQuery(event.target.value)}
        placeholder="Search Claude Code articles"
      />
      <select aria-label="Category" value={category} onChange={(event) => setCategory(event.target.value)}>
        <option value="">All</option>
        <option value="use-cases">Use cases</option>
        <option value="advanced">Advanced</option>
      </select>
      {loading && <p>Searching...</p>}
      <ul>
        {hits.map((hit) => (
          <li key={hit.id}>
            <a href={hit.url}>{hit.title}</a>
            <p>{hit.summary}</p>
          </li>
        ))}
      </ul>
    </section>
  );
}

테스트, 배포 체크, 실패 사례

흔한 실패는 draft를 인덱싱하는 것, admin key를 브라우저에 두는 것, private field를 외부 검색 서비스에 보내는 것, 동의어를 과하게 묶는 것, DB 컬럼을 전부 facet으로 만드는 것입니다. 최소한 짧은 검색어 차단, published만 반환, category 필터, 모바일 레이아웃을 확인합니다.

// tests/search-query.test.ts
import { describe, expect, it } from "vitest";

function shouldSearch(query: string) {
  return query.trim().length >= 2 && query.length <= 80;
}

describe("search request rules", () => {
  it("rejects empty and one-character queries", () => {
    expect(shouldSearch("")).toBe(false);
    expect(shouldSearch("a")).toBe(false);
    expect(shouldSearch("api")).toBe(true);
  });
});

출시 전에는 공개 데이터만 들어갔는지, 0건 UI가 있는지, p95 latency가 맞는지, 긴 쿼리를 자르는지, 로그에 개인정보가 남지 않는지 확인하세요. 출시 후에는 0건 검색과 클릭률 낮은 쿼리를 Claude Code에 넘겨 제목, 동의어, 내부 링크, 새 글 주제를 개선합니다.

ClaudeCodeLab은 Claude Code 검색 설계, API 구현, 콘텐츠 회유 개선을 교육과 상담 주제로 다룹니다. 체계적인 지원이 필요하다면 training page를 확인해 주세요.

정리

검색 기능은 요구사항, 백엔드 선택, 인덱스 schema, 동기화, 필터와 facet, debounce UI, 테스트, 배포 순서로 설계해야 합니다. 실제로 적용해 보면 인덱스 필드와 반환 필드를 초기에 좁힌 구성이 후속 수정이 가장 적었고, 0건 검색 리뷰가 PV 성장에 필요한 새 콘텐츠 아이디어를 가장 많이 줬습니다.

#Claude Code #검색 기능 #전체 텍스트 검색 #Meilisearch #Algolia
무료

무료 PDF: Claude Code 치트시트

이메일을 입력하면 명령, 리뷰 습관, 안전한 워크플로를 정리한 PDF를 받을 수 있습니다.

개인정보를 안전하게 관리하며 스팸을 보내지 않습니다.

Masa

작성자 소개

Masa

Claude Code 실무 워크플로와 팀 도입을 검증하는 엔지니어입니다.