대용량 텍스트 검색 및 전송 최적화 엔진 (1) 환경 구축 및 스캐폴딩

Pt J·2026년 6월 15일
post-thumbnail

대용량 텍스트 검색 및 전송 최적화 엔진 (1) 환경 구축 및 스캐폴딩

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

Rust 스캐폴딩

~/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.rs

use 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

Python 스캐폴딩

~/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 }

환경 변수를 파일에 저장한다.
실무에서는 환경 변수 파일을 외부에 공유하지 않도록 유의한다.

.env

GATEWAY_HOST=0.0.0.0
GATEWAY_PORT=8000
CORS_ORIGINS=http://localhost:5173
ELASTICSEARCH_URL=http://localhost:9200

코드

환경변수를 안전하게 읽어오는 설정 코드를 작성한다.

gateway/app/config.py

from 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.py

from pydantic import BaseModel

class HealthResponse(BaseModel):
    """API 게이트웨이 및 Rust 엔진의 헬스 상태 응답 스키마."""
    status: str
    engine: str

Rust의 테스트용 헬스체크 함수를 호출하는 API를 작성한다.

gateway/app/main.py

import 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"}%    

Svelte 헬스체크 페이지

~/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()}

환경 변수를 파일에 저장한다.
실무에서는 환경 변수 파일을 외부에 공유하지 않도록 유의한다.

.env

PUBLIC_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 프리티어 포맷터 개행 문제를 해결하는 방법을 배웠습니다.
profile
Peter J Online Space - since July 2020 | 아무데서나 채용해줬으면 좋겠다 (지금은 학생 때 하던 거 아무거나 공부하고 있고요, 취업시켜 주시면 그 분야로 공부할게요)

0개의 댓글