elasticsearch를 이용하여 검색 시스템을 만들어 보자.
그 전에, 대용량 텍스트 페이로드 최적화 작업을 수행한 후
elasticsearch Docker service를 생성하여 연결하도록 하겠다.
🤖 AI AGENT | Intro
단순히 데이터를 화면에 띄우는 것을 넘어, "수십~수백 MB의 텍스트 데이터를 브라우저부터 백엔드, 검색 엔진까지 얼마나 빠르고 효율적으로 파이프라이닝 할 것인가"를 해결하는 풀스택 아키텍처를 구축합니다. 실무에서 대용량 트래픽과 데이터를 다룰 때 필수적인 네트워크 병목 해소, 메모리 안전성, 그리고 사용자 경험(UX) 최적화 기술을 내재화하는 것이 핵심입니다.
단계별 개발 흐름 (Roadmap)
Phase 1: 대용량 페이로드 최적화 (Payload & Network Optimization)
검색 엔진을 붙이기 전, 거대한 데이터를 네트워크를 통해 브라우저로 안전하고 가볍게 보내는 기반을 다집니다.
- Rust 엔직 설계: 수만 줄의 텍스트 데이터를 메모리에 안전하게 할당하고 Python으로 넘겨주는 FFI 인터페이스 구현.
- FastAPI 미들웨어: 응답 데이터를 압축하여 대역폭을 절약하고, ETag와 Cache-Control을 구현하여 불필요한 중복 다운로드를 방지.
- 검증: SvelteKit 프론트엔드에서 데이터를 요청하고, 브라우저 네트워크 탭에서 압축률과 304 Not Modified(캐시 적중) 상태를 직접 확인.
Phase 2: 검색 엔진 도입 (Full-text Search Integration)
최적화된 파이프라인 위에 본격적인 검색 기능을 얹습니다.
- 인프라 구성: docker-compose를 통한 Elasticsearch 환경 구축.
- 데이터 색인(Indexing): Rust에서 생성한 대량의 데이터를 Python을 거쳐 Elasticsearch Bulk API로 밀어 넣기.
- 검색 API 구현: FastAPI에서 클라이언트의 검색 쿼리를 받아 Elasticsearch로 전달하고, 매칭된 결과를 반환.
Phase 3: 프론트엔드 고도화 (UX Enhancement)
사용자가 대용량 검색 시스템을 쾌적하게 사용할 수 있도록 UI를 다듬습니다.
- Debounce 처리: 사용자가 타이핑할 때마다 API가 호출되는 것을 방지하여 서버 부하 감소.
- 결과 시각화: 검색된 키워드 형광펜 처리(Highlighting) 및 결과가 많을 경우 무한 스크롤(Infinite Scroll) 적용.
일단 전체적인 흐름을 스캐폴딩으로 잡아놓고 내용을 구현하는 방식으로 진행해 보겠다.
~/workspace$ mkdir fast-text-search && cd fast-text-search ~/workspace/fast-text-search$ # 필요한 것들이 다 설치되어 있는지 확인해 보면, ~/workspace/fast-text-search$ rustc --version && uv --version && node --version && pnpm --version && docker --version rustc 1.95.0 (59807616e 2026-04-14) uv 0.11.5 (95eaa68c8 2026-04-08 aarch64-apple-darwin) v25.6.1 11.2.2 Docker version 29.2.1, build a5c7197 ~/workspace/fast-text-search$ # PyO3를 사용하는 Rust 엔진 ~/workspace/fast-text-search$ cargo new --lib engine && cd engine ~/workspace/fast-text-search/engine$ touch pyproject.toml ~/workspace/fast-text-search/engine$ cd .. ~/workspace/fast-text-search$ # FastAPI를 사용하는 Python 게이트웨이 ~/workspace/fast-text-search$ mkdir gateway && cd gateway ~/workspace/fast-text-search/gateway$ uv init ~/workspace/fast-text-search/gateway$ uv add fastapi "uvicorn[standard]" pydantic-settings ~/workspace/fast-text-search/gateway$ uv add --dev ruff mypy httpx2 maturin ~/workspace/fast-text-search/gateway$ cd .. ~/workspace/fast-text-search$ # Typescript를 사용하는 Sveltekit 프론트엔드 ~/workspace/fast-text-search$ pnpm dlx sv create frontend --template minimal --types ts --no-add-ons --no-install && cd frontend ~/workspace/fast-text-search/frontend$ pnpm install ~/workspace/fast-text-search/frontend$ cd .. ~/workspace/fast-text-search$ # 기타 인프라 ~/workspace/fast-text-search$ mkdir infra && touch infra/compose.yml .env
~/workspace/fast-text-search$ cd engine ~/workspace/fast-text-search/engine$ tree -I target . ├── Cargo.lock ├── Cargo.toml ├── pyproject.toml ├── src │ └── lib.rs └── uv.lock 2 directories, 5 files
설정 파일을 작성해 준다.
engine/Cargo.toml[package] name = "engine" version = "0.1.0" edition = "2024" description = "대용량 텍스트 데이터 생성 및 전처리 Rust 엔진" authors = ["Joowon Jung <neont21@gmail.com>"] [lib] crate-type = ["cdylib"] [dependencies] pyo3 = { version = "0.29", features = ["extension-module"] }
engine/pyproject.toml[project] name = "fast-text-engine" version = "0.1.0" description = "대용량 텍스트 데이터 생성 및 전처리 Rust 엔진" requires-python = ">=3.12" [build-system] requires = ["maturin>=1.8"] build-backend = "maturin" [tool.maturin] features = ["pyo3/extension-module"] module-name = "fast_text_engine"
테스트용 헬스체크 함수를 작성한다.
engine/src/lib.rsuse pyo3::prelude::*; /// 엔진 모듈이 정상 로드되는지 확인하는 헬스 함수. #[pyfunction] fn engine_health() -> PyResult<&'static str> { Ok("ok") } /// `fast_text_engine` Python 확장 모듈. #[pymodule] fn fast_text_engine(m: &Bound<'_, PyModule>) -> PyResult<()> { m.add_function(wrap_pyfunction!(engine_health, m)?)?; Ok(()) }
작성한 헬스체크 코드를 빌드한다.
~/workspace/fast-text-search/engine$ cargo check ~/workspace/fast-text-search/engine$ # 게이트웨이의 가상환경을 사용해야 컴파일 결과물이 그곳에 생성된다 ~/workspace/fast-text-search/engine$ VIRTUAL_ENV=../gateway/.venv uv run --active maturin develop
~/workspace/fast-text-search/engine$ cd ../gateway ~/workspace/fast-text-search/gateway$ mkdir app && mv main.py app ~/workspace/fast-text-search/gateway$ touch app/config.py app/schemas.py app/__init__.py ~/workspace/fast-text-search/gateway$ touch .env ~/workspace/fast-text-search/gateway$ tree . ├── README.md ├── app │ ├── __init__.py │ ├── config.py │ ├── main.py │ └── schemas.py ├── pyproject.toml └── uv.lock 2 directories, 7 files
설정 파일을 작성해 준다.
대체로 자동 작성되어 프로젝트 설명 정도만 추가로 작성해 주고
Rust Engine에서 작성한 모듈을 추가하기만 하면 된다.
gateway/pyproject.toml[project] name = "gateway" version = "0.1.0" description = "대용량 텍스트 검색 및 최적화 시스템의 FastAPI 게이트웨이" readme = "README.md" requires-python = ">=3.12" dependencies = [ "fastapi>=0.137.0", "pydantic-settings>=2.14.1", "uvicorn[standard]>=0.49.0", "fast-text-engine>=0.1.0", ] [dependency-groups] dev = [ "httpx2>=2.4.0", "mypy>=2.1.0", "ruff>=0.15.17", "maturin>=1.14.0", ] [tool.uv.sources] fast-text-engine = { path = "../engine", editable = true }
환경 변수를 파일에 저장한다.
실무에서는 환경 변수 파일을 외부에 공유하지 않도록 유의한다.
.envGATEWAY_HOST=0.0.0.0 GATEWAY_PORT=8000 CORS_ORIGINS=http://localhost:5173 ELASTICSEARCH_URL=http://localhost:9200
환경변수를 안전하게 읽어오는 설정 코드를 작성한다.
gateway/app/config.pyfrom pydantic_settings import BaseSettings, SettingsConfigDict class Settings(BaseSettings): """API 게이트웨이의 환경변수 및 설정을 로드하고 관리하는 클래스.""" GATEWAY_HOST: str GATWAY_PORT: int CORS_ORIGIN: str ELASTICSEARCH_URL: str model_config = SettingsConfigDict( env_file=".env", env_file_encoding="utf-8", extra="ignore" ) settings = Settings()
Python 코드에서 사용될 데이터 스키마를 정의하는 파일에서
헬스체크 응답 모델을 작성한다.
gateway/app/schemas.pyfrom pydantic import BaseModel class HealthResponse(BaseModel): """API 게이트웨이 및 Rust 엔진의 헬스 상태 응답 스키마.""" status: str engine: str
Rust의 테스트용 헬스체크 함수를 호출하는 API를 작성한다.
gateway/app/main.pyimport logging from contextlib import asynccontextmanager from fastapi import FastAPI from fastapi.middleware.cors import CORSMiddleware import fast_text_engine from .config import settings from .schemas import HealthResponse # 로깅 설정 logging.basicConfig( level=logging.INFO, format="%(asctime)s [%(levelname)s] %(name)s: %(message)s" ) logger = logging.getLogger("gateway") @asynccontextmanager async def lifespan(app: FastAPI): """FastAPI 애플리케이션의 수명 주기를 관리하는 컨텍스트 매니저.""" logger.info("Fast Text Search Gateway 서버 기동...") logger.info(f"설정 로드 완료 - Host: {settings.GATEWAY_HOST}, Port: {settings.GATEWAY_PORT}") yield logger.info("Fast Text Search Gateway 서버 종료...") app = FastAPI( title="Fast Text Search Gateway", description="대용량 텍스트 최적화 전송 및 Elasticsearch 검색을 처리하는 API 게이트웨이", version="0.1.0", lifespan=lifespan ) origins = [origin.strip() for origin in settings.CORS_ORIGINS.split(",")] app.add_middleware( CORSMiddleware, allow_origins=origins, allow_credentials=True, allow_methods=["*"], allow_headers=["*"], ) @app.get("/health", response_model=HealthResponse) async def health_check() -> HealthResponse: """게이트웨이 자체 상태와 Rust 연산 엔진의 동작 여부를 검사합니다. Returns: HealthResponse: 상태 검사 결과 객체. """ engine_status = "error" try: engine_status = fast_text_engine.engine_health() except Exception as e: logger.error(f"Rust 엔진 헬스 체크 실패: {e}") return HealthResponse( status="ok" if engine_status == "ok" else "error", engine=engine_status )
작성한 코드를 실행 확인한다.
[터미널 A | 게이트웨이 실행]
~/workspace/fast-text-search/gateway$ uv run uvicorn app.main:app --reload
[터미널 B | 응답 확인]
~$ curl -i http://localhost:8000/health HTTP/1.1 200 OK date: Mon, 15 Jun 2026 06:21:21 GMT server: uvicorn content-length: 29 content-type: application/json {"status":"ok","engine":"ok"}%
~/workspace/fast-text-search/gateway$ cd ../frontend ~/workspace/fast-text-search/frontend$ touch src/app.css .env ~/workspace/fast-text-search/frontend$ tree -I node_modules . ├── README.md ├── package.json ├── pnpm-lock.yaml ├── src │ ├── app.css │ ├── app.d.ts │ ├── app.html │ ├── lib │ │ ├── assets │ │ │ └── favicon.svg │ │ └── index.ts │ └── routes │ ├── +layout.svelte │ └── +page.svelte ├── static │ └── robots.txt ├── tsconfig.json └── vite.config.ts 6 directories, 13 files
디자인은 부차적인 영역이므로 AI가 제공한 디자인 시스템을 그대로 사용하겠다.
frontend/src/app.css/* CSS 변수를 이용한 글로벌 디자인 시스템 정의 */ :root { --bg-primary: #0f172a; /* 슬레이트 다크 배경 */ --bg-secondary: #1e293b; /* 카드 컴포넌트 배경 */ --text-primary: #f8fafc; /* 메인 텍스트 */ --text-secondary: #94a3b8; /* 설명 텍스트 */ --color-primary: #3b82f6; /* 메인 파란색 버튼 */ --color-primary-hover: #2563eb; --color-success: #10b981; /* 성공 상태 초록색 */ --color-error: #ef4444; /* 오류 상태 빨간색 */ --border-radius: 8px; --transition-smooth: all 0.2s ease-in-out; } body { background-color: var(--bg-primary); color: var(--text-primary); font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; margin: 0; padding: 0; display: flex; justify-content: center; align-items: center; min-height: 100vh; }
전역 CSS를 레이아웃에 적용한다.
frontend/src/routes/+layout.svelte<script lang="ts"> import favicon from "$lib/assets/favicon.svg"; import "../app.css"; let { children } = $props(); </script> <svelte:head> <link rel="icon" href={favicon} /> </svelte:head> {@render children()}
환경 변수를 파일에 저장한다.
실무에서는 환경 변수 파일을 외부에 공유하지 않도록 유의한다.
.envPUBLIC_API_URL=http://localhost:8000
연결 테스트용 헬스체크 페이지를 작성한다.
frontend/src/routes/+page.svelte<script lang="ts"> import { PUBLIC_API_URL } from "$env/static/public"; let isLoading = $state(false); let healthData = $state<{ status: String; engine: String } | null>(null); let errorMessage = $state<string | null>(null); async function checkHealth(): Promise<void> { isLoading = true; errorMessage = null; healthData = null; try { const response = await fetch(`${PUBLIC_API_URL}/health`); if (!response.ok) { throw new Error(`HTTP 에러 발생: ${response.status}`); } healthData = await response.json(); } catch (e) { if (e instanceof Error) { errorMessage = e.message; } else { errorMessage = "알 수 없는 에러가 발생했습니다"; } console.error("Health check failed:", e); } finally { isLoading = false; } } </script> <main class="container"> <div class="card"> <h1 class="title">대용량 텍스트 검색 최적화 엔진</h1> <p class="subtitle">Rust + FastAPI + Elasticsearch + SvelteKit</p> <button class="btn" onclick={checkHealth} disabled={isLoading}> {#if isLoading} 서버 응답 대기 중... {:else} 서버 헬스 체크 실행 {/if} </button> <div class="results-area"> {#if isLoading} <div class="status-msg loading"> FastAPI 게이트웨이 및 Rust 연산 엔진 통신을 테스트하는 중입니다... </div> {:else if healthData} <div class="status-box success"> <h2>연결 성공</h2> <ul> <li> <strong>API Gateway:</strong> <span class="badge success">{healthData.status}</span> </li> <li> <strong>Rust FFI Engine:</strong> <span class="badge success">{healthData.engine}</span> </li> </ul> </div> {:else if errorMessage} <div class="status-box error"> <h2>연결 실패</h2> <p>{errorMessage}</p> </div> {/if} </div> </div> </main> <style> .container { display: flex; justify-content: center; align-items: center; width: 100%; padding: 20px; box-sizing: border-box; } .card { background-color: var(--bg-secondary); border: 1px solid rgba(255, 255, 255, 0.05); border-radius: var(--border-radius); padding: 40px; width: 100%; max-width: 480px; box-shadow: 0 10px 25px -5px rgba(0, 0, 0, 0.3), 0 8px 10px -6px rgba(0, 0, 0, 0.3); text-align: center; } .title { font-size: 1.8rem; margin: 0 0 10px 0; font-weight: 700; } .subtitle { color: var(--text-secondary); font-size: 0.95rem; margin: 0 0 30px 0; } .btn { background-color: var(--color-primary); color: var(--text-primary); border: none; padding: 14px 24px; font-size: 1rem; font-weight: 600; border-radius: var(--border-radius); cursor: pointer; width: 100%; transition: var(--transition-smooth); } .btn:hover:not(:disabled) { background-color: var(--color-primary-hover); } .btn:disabled { opacity: 0.6; cursor: not-allowed; } .results-area { margin-top: 30px; min-height: 120px; text-align: left; } .status-msg.loading { color: var(--text-secondary); text-align: center; font-size: 0.9rem; padding: 20px 0; } .status-box { padding: 20px; border-radius: var(--border-radius); font-size: 0.95rem; } .status-box h2 { margin: 0 0 12px 0; font-size: 1.1rem; } .status-box.success { background-color: rgba(16, 185, 129, 0.1); border: 1px solid rgba(16, 185, 129, 0.2); } .status-box.success ul { list-style: none; padding: 0; margin: 0; } .status-box.success li { margin-bottom: 8px; display: flex; justify-content: space-between; align-items: center; } .status-box.error { background-color: rgba(239, 68, 68, 0.1); border: 1px solid rgba(239, 68, 68, 0.2); color: #fca5a5; } .status-box.error p { margin: 0; line-height: 1.5; } .badge { padding: 4px 8px; font-size: 0.8rem; font-weight: 700; text-transform: uppercase; border-radius: 4px; } .badge.success { background-color: var(--color-success); color: var(--text-primary); } </style>
작성한 코드를 실행 확인한다.
~/workspace/fast-text-search/frontend$ pnpm dev $ vite dev VITE v8.0.16 ready in 230 ms ➜ Local: http://localhost:5173/ ➜ Network: use --host to expose ➜ press h + enter to show help
🤖 AI AGENT | 학습 회고
Phase 0
- 완료일: 2026-06-15
- 성과:
- Rust 2024 기반 PyO3 연산 엔진, Python 3.12 기반 FastAPI 게이트웨이, SvelteKit 프론트엔드 전체의 스캐폴딩 셋업을 성공적으로 마쳤습니다.
- 게이트웨이는
app/패키지 형태로 구조화하고config.py,schemas.py를 분리하여 단일 책임 원칙(SRP)을 따르도록 했습니다.- Svelte 5 Runes 문법 및 CSS 변수 기반의 전역 디자인 시스템(
app.css)을 구축하고, 빌트인 모듈을 통한 백엔드 환경 변수(PUBLIC_API_URL) 연동을 완료했습니다.- 브라우저 UI ➔ 게이트웨이 ➔ Rust FFI 엔진에 이르는 3-Tier 전체 헬스체크 통합 시나리오 작동을 확인했습니다.
- 배운 점 & 트러블슈팅:
- 모노레포에서의
uv run과 외부 가상환경 연동 시 경로 에러(--active옵션 필요)를 극복했습니다.- SvelteKit 환경 변수의 TypeScript 타입 파일 동기화 방법(
pnpm svelte-kit sync등)과 HTML 프리티어 포맷터 개행 문제를 해결하는 방법을 배웠습니다.