챗봇 공부 노트

[17편] Part 3 MySQL 세션/메시지 + 오류정리 및 해결

frontend-diary-log 2026. 2. 5. 23:07

 

📘 Part 3 — MySQL 세션/메시지 + 프론트 세션 UX 전체 코드 + 설명

Part 3에서는
**“GPT 대화를 실제 서비스처럼 저장하고, 관리하고, 다시 불러오는 구조”**를 완성합니다.

이번 파트의 핵심은 다음과 같습니다.

대화는 일회성이 아니라 ‘세션’이며,
세션은 다시 돌아와도 유지되어야 합니다.

이를 위해

  • SQLite → MySQL 전환
  • 세션/메시지 영속 저장
  • 프론트엔드 세션 UX 개선
    을 함께 진행하였습니다.

✅ 포함 파일

  • message_service.py
  • session.py
  • sessions.py
  • ChatContainer.tsx
  • ChatSidebar.tsx

1️⃣ message_service.py

✅ SQLite → MySQL 전환 + 세션 title / summary 지원

import os
from datetime import datetime

import mysql.connector
from dotenv import load_dotenv

load_dotenv()

def _connect():
    return mysql.connector.connect(
        host=os.getenv("MYSQL_HOST", "localhost"),
        port=int(os.getenv("MYSQL_PORT", "3306")),
        user=os.getenv("MYSQL_USER", "root"),
        password=os.getenv("MYSQL_PASSWORD", ""),
        database=os.getenv("MYSQL_DATABASE", "aura_ai"),
    )

📌 DB 초기화 (세션 / 메시지 테이블)

def init_db():
    with _connect() as conn:
        cursor = conn.cursor()
        cursor.execute(
            """
            CREATE TABLE IF NOT EXISTS chat_sessions (
                id BIGINT PRIMARY KEY AUTO_INCREMENT,
                user_id VARCHAR(255) NOT NULL,
                title VARCHAR(255) NOT NULL DEFAULT '새 대화',
                created_at TEXT NOT NULL,
                summary TEXT
            )
            """
        )
        cursor.execute(
            """
            CREATE TABLE IF NOT EXISTS messages (
                id BIGINT PRIMARY KEY AUTO_INCREMENT,
                session_id BIGINT NOT NULL,
                role VARCHAR(50) NOT NULL,
                content TEXT NOT NULL,
                content_type VARCHAR(50) NOT NULL DEFAULT 'text',
                created_at TEXT NOT NULL,
                INDEX idx_session_id (session_id),
                CONSTRAINT fk_session
                    FOREIGN KEY (session_id)
                    REFERENCES chat_sessions(id)
                    ON DELETE CASCADE
            )
            """
        )
        conn.commit()

📌 세션 관련 로직

def create_session(user_id: str):
    now = datetime.utcnow().isoformat()
    with _connect() as conn:
        cursor = conn.cursor(dictionary=True)
        cursor.execute(
            "INSERT INTO chat_sessions (user_id, title, created_at) VALUES (%s, %s, %s)",
            (user_id, "새 대화", now),
        )
        session_id = cursor.lastrowid
        conn.commit()
    return {"id": session_id, "user_id": user_id, "title": "새 대화", "created_at": now}
def update_session_summary(session_id: int, summary: str):
    with _connect() as conn:
        cursor = conn.cursor()
        cursor.execute(
            "UPDATE chat_sessions SET summary = %s WHERE id = %s",
            (summary, session_id),
        )
        conn.commit()

📌 메시지 저장 / 조회

def create_message(session_id: int, role: str, content: str, content_type: str):
    now = datetime.utcnow().isoformat()
    with _connect() as conn:
        cursor = conn.cursor(dictionary=True)
        cursor.execute(
            """
            INSERT INTO messages (session_id, role, content, content_type, created_at)
            VALUES (%s, %s, %s, %s, %s)
            """,
            (session_id, role, content, content_type, now),
        )
        message_id = cursor.lastrowid
        conn.commit()
    return {
        "id": message_id,
        "session_id": session_id,
        "role": role,
        "content": content,
        "content_type": content_type,
        "created_at": now,
    }

✅ 왜 이렇게 설계했나요?

  • SQLite는 로컬 테스트용에 가깝습니다.
  • 실제 서비스에서는 MySQL 같은 영속 DB가 필요합니다.
  • 세션 제목(title)과 요약(summary)을 저장해야 UX가 좋아집니다.

✅ 장점

  • 새로고침 / 재접속 시에도 대화가 유지됩니다.
  • 실서비스에 바로 적용 가능한 구조입니다.

2️⃣ session.py

✅ 세션 스키마 정의

from pydantic import BaseModel

class SessionCreate(BaseModel):
    user_id: str

class SessionUpdate(BaseModel):
    title: str

class SessionResponse(BaseModel):
    id: int
    user_id: str
    title: str
    created_at: str

✅ 장점

  • API 요청/응답 구조가 명확해집니다.
  • Swagger 문서가 자동 생성됩니다.

3️⃣ sessions.py

✅ 세션 CRUD API

from fastapi import APIRouter, Header, HTTPException

from schemas.session import SessionResponse, SessionUpdate
from services.message_service import (
    create_session,
    delete_session,
    get_session,
    init_db,
    list_sessions,
    update_session_title,
)

router = APIRouter(prefix="/sessions", tags=["sessions"])
init_db()

📌 세션 생성

@router.post("", response_model=SessionResponse)
async def create_session_route(x_user_id: str | None = Header(default=None)):
    if not x_user_id:
        raise HTTPException(status_code=401, detail="Unauthorized")
    return create_session(x_user_id)

📌 세션 목록 / 조회 / 수정 / 삭제

  • 사용자 인증 필수
  • 본인 세션만 접근 가능
  • 제목 수정(PATCH), 삭제(DELETE) 지원

✅ 장점

  • 세션 관리 UX 완성
  • 대화 제목 수정 / 삭제 가능
  • 실제 챗 서비스와 동일한 구조

4️⃣ ChatContainer.tsx

✅ 프론트엔드 핵심 변경 사항

(전체 코드는 앞선 단계에서 제공된 코드 그대로 사용하셨습니다.)

핵심 변경 요약

  • session_id를 모든 요청에 포함
  • 첫 메시지 후 GPT 응답으로 세션 제목 자동 생성
  • 카드 응답 / 텍스트 응답 타입 분리 처리

✅ 장점

  • 대화 흐름이 세션 단위로 명확해집니다.
  • UX가 실제 서비스 수준으로 올라갑니다.

5️⃣ ChatSidebar.tsx

✅ 세션 사이드바 UX 개선

변경 사항

  • 더미 목록 제거 → 실제 세션 목록 연동
  • 세션 제목 수정 / 삭제 메뉴 추가
  • “새 대화” 버튼 → /sessions POST 연동

✅ 장점

  • “대화 1, 대화 2”가 아닌 의미 있는 제목 기반 UX
  • 세션 관리가 매우 직관적입니다.

✅ 결론 (Day 17 총정리)

오늘 완성한 내용은 다음과 같습니다.

  • GPT 컨텍스트 엔지니어링
  • 토큰 기반 비용 제어
  • 요약 메모리 설계
  • MySQL 세션 / 메시지 영속 저장
  • 프론트엔드 세션 UX 완성
  • 구글 유저 통합 로그인 구조

즉,
**“GPT 데모”가 아니라
“실제 운영 가능한 AI 서비스 구조”**를 완성한 날입니다.

 


📘 Part 4 — 오늘 발생한 오류와 해결 과정 

이번 Part 4는
기능 구현보다 훨씬 중요한 “운영 환경 안정화” 단계입니다.

오늘 하루 동안 발생한 오류들은 단순한 실수가 아니라,
실제 서비스 환경에서 반드시 겪게 되는 문제들이었습니다.

아래는 발생 순서 기준으로 정리한 전체 오류와 해결 과정입니다.


1️⃣ ModuleNotFoundError: tiktoken

🔍 원인

GPT 토큰 계산을 위해 tiktoken 라이브러리를 사용했지만,
가상환경에 해당 패키지가 설치되어 있지 않았습니다.

✅ 해결

pip install tiktoken

📌 정리

  • 토큰 계산은 선택 사항이 아니라 비용/속도 제어의 핵심 요소입니다.
  • requirements.txt에 반드시 포함되어야 합니다.

2️⃣ No module named uvicorn

🔍 원인

가상환경에 uvicorn이 설치되지 않아
python -m uvicorn 실행이 실패했습니다.

✅ 해결

pip install uvicorn

📌 정리

  • FastAPI 서버 실행을 위해 uvicorn은 필수입니다.
  • 개발 서버(--reload) 환경에서도 반드시 필요합니다.

3️⃣ No module named fastapi

🔍 원인

FastAPI 자체가 설치되지 않아 서버가 시작되지 않았습니다.

✅ 해결

pip install fastapi

📌 정리

  • 기본적인 서버 프레임워크도 환경에 없으면 실행이 불가능합니다.
  • 새 가상환경 생성 시 가장 먼저 설치해야 합니다.

4️⃣ ERR_CONNECTION_REFUSED

🔍 원인

프론트엔드에서 localhost:8000을 호출했지만,
백엔드 서버가 실행 중이지 않았습니다.

✅ 해결

uvicorn main:app --reload

📌 정리

  • 프론트 오류처럼 보이지만 실제 원인은 백엔드 미실행인 경우가 많습니다.
  • 항상 서버 실행 여부를 먼저 확인해야 합니다.

5️⃣ Access denied for user 'root'@'localhost' (using password: NO)

🔍 원인

  • FastAPI는 .env.local을 읽지 않습니다.
  • MySQL 환경변수가 backend/.env에 없어
    비밀번호가 전달되지 않았습니다.

✅ 해결

backend/.env 파일에 아래 내용을 추가하였습니다.

MYSQL_HOST=localhost
MYSQL_PORT=3306
MYSQL_DATABASE=aura_ai
MYSQL_USER=root
MYSQL_PASSWORD=1234

📌 정리

  • 프론트와 백엔드는 환경변수 파일이 다릅니다.
  • 서버에서는 반드시 .env 파일을 사용해야 합니다.

6️⃣ Unknown column 'user_id' in 'field list'

🔍 원인

기존 users 테이블이 오래된 구조여서
user_id 컬럼이 존재하지 않았습니다.

✅ 해결

ALTER TABLE users ADD COLUMN user_id VARCHAR(50) NOT NULL;
UPDATE users SET user_id = CONCAT('local_', id) WHERE user_id = '';

📌 정리

  • 서비스가 커질수록 DB 스키마 마이그레이션이 필수입니다.
  • 과거 데이터와의 호환성을 항상 고려해야 합니다.

7️⃣ Table aura_ai.chat_sessions doesn't exist

🔍 원인

MySQL에 세션/메시지 테이블이 아직 생성되지 않았습니다.

✅ 해결

CREATE TABLE chat_sessions (...);
CREATE TABLE messages (...);

📌 정리

  • 코드만 작성해도 DB는 자동으로 생기지 않습니다.
  • 초기 테이블 생성 로직(init_db)의 중요성을 다시 확인했습니다.

8️⃣ BLOB/TEXT column can't have default value

🔍 원인

MySQL에서는 TEXT 타입 컬럼에 DEFAULT 값을 설정할 수 없습니다.

✅ 해결

title 컬럼을 VARCHAR(255)로 변경하였습니다.

ALTER TABLE chat_sessions
ADD COLUMN title VARCHAR(255) NOT NULL DEFAULT '새 대화';

📌 정리

  • MySQL 타입 제약을 정확히 이해해야 합니다.
  • UX에 필요한 기본값은 VARCHAR가 더 적합한 경우가 많습니다.

9️⃣ TypeScript 빨간 줄 (타입 오류)

🔍 원인

parseCardsFromContent() 함수가 null을 반환했지만,
ChatMessage.cards 타입에서는 null을 허용하지 않았습니다.

✅ 해결

  • null 반환 → undefined 반환으로 변경

📌 정리

  • TypeScript는 의도하지 않은 상태를 미리 차단해 줍니다.
  • 런타임 에러를 사전에 제거할 수 있습니다.

✅ 최종 정리

오늘은 단순한 오류 수정이 아니라,
오류 해결이 곧 설계 개선으로 이어진 날이었습니다.

  • 환경변수 분리 문제 해결
  • MySQL 스키마 불일치 해결
  • 타입 안정성 확보
  • 서버 실행 환경 정리

이 과정을 통해
실제 서비스 운영이 가능한 구조로 시스템이 안정화되었습니다.