다차원 검색을 수행하는 데이터베이스 (5) 배포해보기

Pt J·2026년 5월 27일
post-thumbnail

다차원 검색을 수행하는 데이터베이스 (5) 배포해보기

시각화 하면 좋을 것 같아서 프론트엔드까지 실습해 본 김에 배포까지 진행해 보자.
무료 티어로 할 수 있는 수준에서 배포하겠다.

수정 사항

배포를 앞두고 코드를 일부 수정하도록 하겠다.

lib/components/DetailPanel.svelte

논문 부록의 2차원 감정 좌표 데이터를 기반으로
수치를 생략하고 상대적 관계성을 노드형 그래프로 시각화하는 것은
독창적 표현이 더해져 저작권 침해 가능성이 낮아 안전한 가공 방식이라고는 하지만
안전한 서비스를 위해 해당 논문의 출처를 명확히 밝히는 게 좋다.

fronted/src/lib/components/DetailPanel.svelte

<script lang="ts">
    import type { Emotion } from '$lib/generated/emotion_search';

    // 선택된 감정 데이터
    let { selectedEmotion } = $props<{ selectedEmotion: Emotion | null}>();

    // 백엔드의 -1.0 ~ 1.0 값을 CSS 위치인 0% ~ 100% 로 변환하는 헬퍼 함수
    function getPos(value: number) {
        return ((value + 1) / 2) * 100;
    }
</script>

<aside class="detail-panel">
    {#if selectedEmotion}
        <h2 class="title">{selectedEmotion.word}</h2>
        <div class="tag">{selectedEmotion.taxonomyPath}</div>

        <div class="section">
            <h3>사전적 정의</h3>
            <p>{selectedEmotion.definition || '등록된 정의가 없습니다.'}</p>
        </div>
        {#if selectedEmotion.vaVector && selectedEmotion.vaVector.length === 2}
            <div class="section">
                <h3>감정 위치 나침반</h3>
                <div class="va-map-container">
                    <div class="va-map">
                        <div class="axis-x"></div>
                        <div class="axis-y"></div>

                        <span class="label top">흥분(고각성)</span>
                        <span class="label bottom">차분(저각성)</span>
                        <span class="label left">불쾌</span>
                        <span class="label right">유쾌</span>

                        <div 
                            class="emotion-dot" 
                            style="left: {getPos(selectedEmotion.vaVector[0])}%; bottom: {getPos(selectedEmotion.vaVector[1])}%;">
                        </div>
                    </div>
                </div>

                <div class="va-description">
                    이 감정은 
                    <strong>{selectedEmotion.vaVector[0] >= 0 ? '긍정적(유쾌)' : '부정적(불쾌)'}</strong>이며, 
                    에너지 수준이 
                    <strong>{selectedEmotion.vaVector[1] >= 0 ? '높은(흥분)' : '낮은(차분)'}</strong> 상태입니다.
                </div>
            </div>
        {/if}
    {:else}
        <div class="empty-state">
            <p>노드를 클릭하면<br>상세 정보가 표시됩니다.</p>
        </div>
    {/if}

    <div class="source-attribution">
        <strong>데이터 출처 (Data Source)</strong><br />
        이 서비스의 감정 벡터 및 정의는 다음 연구를 기반으로 구성되었습니다.<br />
        <a href="https://accesson.kr/ksppa/assets/pdf/14556/journal-19-1-109.pdf" 
        target="_blank" 
        rel="noopener noreferrer">
            민경환 외 (2005). 한국어 감정단어의 목록 작성과 차원 탐색.<br />
            한국심리학회지: 사회 및 성격, 19(1), 109-129.
        </a>
    </div>
</aside>

<style>
/* 스타일 중략 */

    .source-attribution {
        margin-top: 2rem;
        padding-top: 1rem;
        border-top: 1px dashed var(--border-light);
        font-size: 0.75rem;
        line-height: 1.5;
        color: var(--text-muted);
        text-align: left;
    }

    .source-attribution strong {
        color: var(--text-secondary);
        font-weight: 600;
    }

    .source-attribution a {
        color: var(--color-primary);
        text-decoration: none;
        transition: opacity 0.2s;
    }

    .source-attribution a:hover {
        text-decoration: underline;
        opacity: 0.8;
    }
</style>

python-gateway/main.py

배포 시 백엔드/데이터베이스/프론트엔드를 나누어 배포하게 된다.
이 때, 백엔드의 CORS 미들웨어에 프론트엔드 URL이 하드코딩 되어 있으면
관리하기 번거로워진다.
따라서 이 부분을 환경변수로 빼도록 하겠다.

python-gateway/main.py

from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
from app.core.server import lifespan
from app.api.emotion import router as emotion_router
import os

app = FastAPI(
    title="다차원 감정 검색 API Gateway",
    description="Python FastAPI ↔ Rust gRPC 엔진",
    lifespan=lifespan
)

frontend_urls_str = os.getenv("FRONTEND_URL", "http://localhost:5173")
origins = [url.strip() for url in frontend_urls_str.split(",")]

app.add_middleware(
    CORSMiddleware,
    allow_origins=origins
    allow_credentials=True,
    allow_methods=["*"],
    allow_headers=["*"],
)

app.include_router(emotion_router)

.env

이곳에서 실습할 때 사용했던 데이터는 이미 외부에 유출된 것이므로
배포할 땐 정보를 수정하여 배포하는 것이 안전하다.
다른 것보다도 최소한 POSTGRES_PASSWORD 는 수정해야 한다.

수정한 파일은 생략한다.

데이터베이스 배포

DB는 서버리스 PostgreSQL Neon을 통해 배포하도록 하겠다.

  1. 회원가입 후 [New Project]를 클릭한다. (혹은, 자동으로 첫 프로젝트 생성으로 연결된다.)
  2. Postgres version은 우리 프로젝트에서 사용한 [18]로 선택한다.
  3. 가장 가까이 있는 [AWS Asia Pacific 1 (Singapore)] 서버를 선택한다.
  4. 로그인이 포함되어 있지 않은 서비스이므로 Auth는 생략한다.
  5. 프로젝트 생성이 완료되면 [Connect your app manually] 부분의 [Show password]를 눌러 생성된 비밀번호를 확인하고 [Copy snippet]으로 DB URL을 복사해 적절한 곳에 저장해 둔다.
  6. 대시보드 좌측의 [SQL Editor]로 이동한다.
  7. 우리의 scripts/database 디렉토리에 있는 스크립트를 옮겨 넣고 실행한다.
  8. 로컬 .env 파일의 DATABASE_URL 환경변수를 아까 저장한 DB URL로 변경한다.
  9. 로컬에서 scripts/data_pipeline/seed_emotions.py 을 실행하여 DB에 데이터를 넣는다.
 ~/workspace/emotion-dict$ uv run scripts/data_pipeline/seed_emotions.py
  1. 대시보드 좌측의 [Tables]로 이동하여 데이터가 잘 들어갔음을 확인한다.

백엔드 배포

백엔드는 소규모 컨테이너를 무료로 운영할 수 잇는 Fly.io를 통해 배포하도록 하겠다.

  1. 다음 명령어를 통해 Fly CLI를 설치한다. (Mac/Linux)
 ~/workspace/emotion-dict$ curl -L https://fly.io/install.sh | sh
  1. fly auth login 으로 로그인한다.
  2. 로컬 프로젝트 최상단에 Dockerfile 을 작성한다.

    Dockerfile

    # ==========================================
    # 1. Rust 빌드 스테이지
    # ==========================================
    FROM rust:1.95-slim as builder
    RUN apt-get update && apt-get install -y protobuf-compiler
    
    WORKDIR /usr/src/app
    COPY ./proto ./proto
    COPY ./rust-engine ./rust-engine
    
    WORKDIR /usr/src/app/rust-engine
    
    ENV SQLX_OFFLINE=true
    RUN cargo build --release
    
    # ==========================================
    # 2. Python + uv 실행 스테이지
    # ==========================================
    FROM python:3.12-slim
    WORKDIR /app
    
    COPY --from=ghcr.io/astral-sh/uv:latest /uv /uvx /bin/
    
    # Rust 컴파일된 실행 파일 복사
    COPY --from=builder /usr/src/app/rust-engine/target/release/rust-engine /app/rust_engine
    
    COPY ./python-gateway/pyproject.toml ./python-gateway/uv.lock ./
    
    # --frozen: uv.lock 파일을 기준으로 정확한 버전을 설치합니다.
    # --no-dev: 개발용 패키지(테스트 라이브러리 등)는 제외하고 설치합니다.
    RUN uv sync --frozen --no-dev
    
    # 나머지 파이썬 소스 코드 복사 (이 부분이 바뀌어도 위 패키지 설치 레이어는 캐시로 즉시 넘어갑니다)
    COPY ./python-gateway .
    
    # 실행 스크립트 복사 및 권한 부여
    COPY start.sh .
    RUN chmod +x start.sh
    
    # FastAPI 포트 노출
    EXPOSE 8000
    
    # 시작 스크립트 실행
    CMD ["./start.sh"]
  1. 로컬 프로젝트 최상단에 start.sh 을 작성한다.

    start.sh

    #!/bin/bash
    # Rust gRPC 엔진 백그라운드 실행
    ./rust_engine &
    
    # 🎯 uv run을 통해 가상환경에 설치된 uvicorn으로 FastAPI 실행
    uv run uvicorn main:app --host 0.0.0.0 --port 8000
  1. SQLX 검증을 위한 오프라인 캐시를 생성한다. (이 과정을 생략하면 fly launch 중 검증 오류 발생)
~/workspace/emotion-dict$ cd rust-engine
~/workspace/emotion-dict/rust-engine$ cargo install sqlx-cli --no-default-features --features "postgres rustls"
~/workspace/emotion-dict/rust-engine$ DATABASE_URL="postgresql://[사용자명]:[비밀번호]..." cargo sqlx prepare
  1. 다음 명령어를 통해 앱을 설정하면 설정 파일 fly.toml 이 자동 생성되고 완료 시 https://my-backend.fly.dev 형태의 API 주소가 발급된다.
~/workspace/emotion-dict$ fly launch
  1. 설정 중 [Do you want to tweak these settings before proceeding? (y/N)]가 뜰 때 y를 선택하면 웹 브라우저 창이 열리며 프로젝트 이름 등을 GUI로 설정할 수 있다.
  2. 설정 중 [Create .dockerignore from 4 .gitignore files? (y/N) ]가 뜰 때 y를 선택하면 .gitignore 파일을 기반으로 자동으로 .dockerignore 파일을 생성하여 불필요한 파일의 복사를 막을 수 있다. 자동 생성을 하더라도 추가로 한 번 더 검토해 주는 게 좋다.
  3. 다음 명령어를 통해 환경변수를 전달한다. (추후 대시보드에서도 할 수 있다.)
~/workspace/emotion-dict$ fly secrets set DATABASE_URL="postgresql://[사용자명]:[비밀번호]... (아까 복사한 전체 주소)"
~/workspace/emotion-dict$ fly secrets set GRPC_HOST="127.0.0.1:50051"
  1. 이미 배포된 앱에 수정사항이 있을 경우 다음 명령어로 반영할 수 있다.
~/workspace/emotion-dict$ fly deploy

프론트엔드 배포

프론트엔드는 SvelteKit을 만든 팀이 적극 지원하는 플랫폼 Vercel 을 통해 배포하도록 하겠다.

  1. GitHub에 프로젝트를 위한 저장소를 생성하고 프로젝트를 올린다. (public/private 여부는 상관 없다)
  2. Vercel에 로그인하고 [Add New] -> [Project]를 누른다.
  3. 방금 올린 GitHub 레포지토리 주소를 입력하여 import한다.
  4. import 도중 [Root Directory]라는 항목이 나오면 프론트엔드 디렉토리만 올렸을 경우에는 그냥 두면 되고, 저장소를 모노레포 방식으로 올렸을 경우 [edit]을 통해 프로젝트 루트가 아닌 프론트엔드 루트 (frontend/)로 루트 디렉토리를 변경한다.
  5. [Configure] 창에서 [Environment Variables] 를 열고 환경변수를 추가한다.
    • PUBLIC_API_BASE_URL : (앞서 발급받은 API 주소)/api/v1/search
    • 나중에 추가할 경우 [Setting] 좌측 메뉴의 [Environment Variables]에서 추가한 후 상단 메뉴의 [Deployments] 탭에서 [Redeploy] 버튼을 눌러야 반영된다.
  6. [Deploy] 를 클릭한다.
  7. CORS 설정을 위해 Fly.io 대시보드로 돌아가 좌측 [Secrets]를 누르고 환경변수를 추가한다.
    • FRONTEND_URL : (방금 Deploy하여 얻은 Vercel 프로젝트 URL)

다음 URL을 통해 배포된 웹페이지를 확인할 수 있다.

https://emotion-dict.vercel.app

profile
Peter J Online Space - since July 2020 | 아무데서나 채용해줬으면 좋겠다

0개의 댓글