📘 Aura_AI Day 16 — Part 1 (완전판)
“채팅을 상태(State)가 아니라 데이터(Data)로 만든 날”
세션 구조 + 메시지 영속화 + OAuth 통합 준비 + content_type 설계
안녕하세요.
Aura AI 프로젝트 Day 16 기록입니다.
오늘은 UI 개선이나 기능 추가가 아닙니다.
👉 서비스의 ‘기초 공사’를 한 날입니다.
눈에 보이는 기능은 거의 없지만,
사실상 이 날이 전체 아키텍처의 뼈대를 만든 가장 중요한 날이었습니다.
✅ 오늘 목표 (한 줄 요약)
채팅을 React 상태가 아니라 DB에 저장되는 데이터 구조로 전환
❌ Day 15까지의 한계 (문제 상황)
기존 구조는 단순했습니다.
messages → React useState
즉, 브라우저 메모리 안에만 존재하는 임시 상태였습니다.
그래서 실제로 다음 문제가 발생했습니다.
실제 문제들
- 새로고침 → 대화 전부 삭제 ❌
- 로그인 재접속 → 기록 복원 불가 ❌
- 세션(대화방) 개념 없음 ❌
- 사용자별 분리 불가 ❌
- 카드 UI 복원 불가 ❌
결론:
👉 “토이 프로젝트 수준 구조”
실서비스라고 보기 어려웠습니다.
✅ 오늘 설계 철학 4가지 (핵심 방향성)
Day 16은 단순 구현이 아니라
설계 기준을 먼저 세웠습니다.
1️⃣ Session 기반 구조
→ 대화를 묶는 단위 필요
2️⃣ 메시지 영속 저장 (Persistence)
→ 새로고침/재로그인에도 유지
3️⃣ OAuth user_id 통합
→ 모든 로그인 방식 동일한 사용자 체계
4️⃣ content_type 저장
→ 카드/텍스트 UI 복원 가능
👉 “UI가 아닌 데이터 중심 설계”
이 철학이 Day 16 전체를 관통합니다.
✅ 변경/추가 파일 (Part 1 기준)
📦 추가
backend/models/chat_session.py
backend/models/message.py
backend/schemas/session.py
backend/schemas/message.py
backend/services/message_service.py
backend/routers/sessions.py
backend/routers/messages.py
backend/chat.db
✏ 수정
backend/main.py
🧠 1단계 — DB 모델 설계 (데이터 구조 정의)
📁 backend/models/chat_session.py
역할
👉 대화방(Session) 정의
from dataclasses import dataclass
from datetime import datetime
@dataclass
class ChatSession:
id: int
user_id: str
created_at: datetime
✅ 필드 설명
필드의미
| id | 세션 고유값 |
| user_id | 세션 소유자 |
| created_at | 생성 시간 |
✅ 왜 세션이 필요한가?
세션이 없으면:
모든 메시지 = 한 덩어리
즉,
- 대화 분리 불가
- 히스토리 불가
- 새 채팅 시작 불가
세션이 있으면:
User
├─ Session 1
│ ├─ Message
│ ├─ Message
├─ Session 2
👉 대화 단위 관리 가능
📁 backend/models/message.py
역할
👉 실제 메시지 정의
@dataclass
class Message:
id: int
session_id: int
role: str
content: str
created_at: datetime
✅ 설계 이유
필드이유
| session_id | 세션 1:N 관계 |
| role | GPT context 재사용 |
| content | 실제 메시지 |
| created_at | 시간순 정렬 필수 |
👉 GPT API 재호출 시 그대로 history 사용 가능
이게 실무적으로 굉장히 중요합니다.
🧠 2단계 — Pydantic Schema (API 계약)
FastAPI에서
Model = DB 구조
Schema = API 입출력 계약
입니다.
📁 backend/schemas/session.py
class SessionCreate(BaseModel):
user_id: str
class SessionResponse(BaseModel):
id: int
user_id: str
created_at: str
왜 필요한가?
- 타입 검증
- 프론트/백 계약 명확화
- Swagger 자동 문서화
- 런타임 에러 방지
📁 backend/schemas/message.py
class MessageCreate(BaseModel):
session_id: int
role: str
content: str
content_type: str = "text"
class MessageResponse(BaseModel):
id: int
session_id: int
role: str
content: str
content_type: str
created_at: str
⭐ 핵심: content_type
이 필드가 오늘 Part 1의 가장 중요한 설계 포인트입니다.
왜 필요한가?
만약 content만 저장하면:
"니트 + 패딩 추천해드려요"
👉 카드인지 텍스트인지 구분 불가
하지만
content_type = card
이면
👉 UI 복원 가능
즉,
content_type = “UI 복원을 위한 메타데이터”
입니다.
🧠 3단계 — Service 레이어 분리 (실무 구조)
📁 backend/services/message_service.py
목적
👉 Router에서 DB 직접 접근 ❌
👉 Service로 분리 ⭕
왜?
이유효과
| 책임 분리 | router 가벼움 |
| 재사용 | 다른 API에서도 활용 |
| 테스트 | unit test 가능 |
| 실무 표준 | 유지보수 ↑ |
핵심 코드 (요약)
def init_db():
CREATE TABLE IF NOT EXISTS chat_sessions
CREATE TABLE IF NOT EXISTS messages
ALTER TABLE messages ADD COLUMN content_type
왜 init_db 필요?
SQLite는 파일 기반 DB라
👉 테이블 없으면 바로 에러 발생
그래서:
앱 시작 시 자동 테이블 생성 = 안정성 확보
🧠 4단계 — Router 설계 (RESTful)
📁 sessions.py
POST /sessions
GET /sessions
GET /sessions/{id}
📁 messages.py
POST /messages
GET /messages/sessions/{id}
설계 이유
URL만 봐도 구조 이해 가능
세션 → 메시지 종속 관계 명확
👉 RESTful + 가독성 + 유지보수 ↑
🔐 보안 포인트 (실무에서 필수 ⭐)
Header 기반 사용자 검증
x_user_id = Header(...)
if session["user_id"] != x_user_id:
raise HTTPException(status_code=404)
왜 필요한가?
이 코드 없으면:
👉 다른 사람 세션 ID만 알면 조회 가능 (치명적 보안 문제)
즉,
“사용자 소유권 검증”
실서비스에서는 반드시 필요합니다.
✅ main.py 라우터 등록 (많이 놓치는 부분 ⭐)
app.include_router(sessions_router)
app.include_router(messages_router)
등록 안 하면?
- API 호출 안 됨
- Swagger 안 뜸
- 404 발생
👉 의외로 자주 실수하는 포인트
🧠 전체 데이터 흐름 (Day 16 Part 1 완성 구조)
사용자 요청
↓
Session 생성
↓
Message 저장
↓
DB 영속화
↓
새로고침
↓
DB 재조회
↓
대화 복원
👉 이제 채팅은 State가 아니라 Data
✅ 오늘 Part 1 핵심 성과
✅ 세션 기반 채팅 구조
✅ 메시지 DB 영속 저장
✅ content_type 설계
✅ Router/Service 분리
✅ Header 기반 보안 검증
✅ SQLite 영속화 완료
🎯 면접에서 이렇게 말하면 끝입니다
“채팅을 단순 프론트 상태가 아니라 세션 기반 DB 구조로 설계했습니다.
메시지에 content_type을 추가해 카드 UI 복원까지 고려했고,
Router/Service 레이어 분리와 사용자 소유권 검증을 적용해 실무형 구조로 구현했습니다.”
이 한 문장 나오면
👉 ‘아, 서비스 구조 이해하는 개발자구나’ 바로 전달됩니다.
⭐ Day 16 Part 1 한 줄 요약
👉 “채팅을 기능이 아니라 데이터 모델로 재설계한 날”
📘 Aura_AI Day 16 — Part 2 (완전판)
프론트 연결 + 카드 복원 + Google 로그인 오류 해결
“채팅을 화면이 아니라 DB와 동기화된 구조로 만든 단계”
안녕하세요.
Day 16 Part 2 기록입니다.
Part 1에서 우리는:
- 세션/메시지 DB 구조 설계
- Router / Service 분리
- content_type 도입
👉 백엔드 영속 구조를 완성했습니다.
그렇다면 이제 질문이 하나 생깁니다.
❓ “DB는 만들었는데… 프론트는 어떻게 연결하지?”
Part 2는 바로
👉 프론트 ↔ DB 실제 연결 작업입니다.
즉,
채팅을 “React state 앱” → “DB 기반 서비스”로 바꾸는 단계
입니다.
✅ Part 2에서 해결한 핵심
오늘 한 일을 한 줄로 요약하면:
프론트 = 단순 UI
DB = 진짜 데이터
이 구조로 전환했습니다.
오늘 구현 목록
✅ Zustand 전역 상태(chatStore)
✅ 세션 복원/생성 로직
✅ 메시지 저장/조회
✅ 카드 UI 복원
✅ Google 로그인 user_id 불안정 문제 해결
🧠 1단계 — chatStore.ts (전역 상태 설계)
📁 store/chatStore.ts
역할
👉 세션 + 메시지를 전역으로 관리
import { create } from "zustand";
export type ChatMessage = {
id: string;
role: "user" | "assistant" | "system";
content: string;
cards?: { label: string; value: string }[];
};
interface ChatState {
sessionId: number | null;
messages: ChatMessage[];
setSessionId: (sessionId: number | null) => void;
setMessages: (messages: ChatMessage[]) => void;
addMessage: (message: ChatMessage) => void;
updateMessage: (id: string, patch: Partial<ChatMessage>) => void;
clearMessages: () => void;
}
export const useChatStore = create<ChatState>((set) => ({
sessionId: null,
messages: [],
setSessionId: (sessionId) => set({ sessionId }),
setMessages: (messages) => set({ messages }),
addMessage: (message) =>
set((state) => ({
messages: [...state.messages, message],
})),
updateMessage: (id, patch) =>
set((state) => ({
messages: state.messages.map((msg) =>
msg.id === id ? { ...msg, ...patch } : msg
),
})),
clearMessages: () => set({ messages: [] }),
}));
✅ 이 코드가 하는 일
sessionId
→ 현재 대화방 ID
messages
→ 화면에 렌더링할 메시지 목록
상태 함수들
함수역할
| setMessages | 전체 교체 (복원 시 사용) |
| addMessage | 메시지 추가 |
| updateMessage | 기존 메시지 수정 |
| clearMessages | 초기화 |
⭐ updateMessage가 중요한 이유
GPT 응답은 스트리밍/비동기입니다.
흐름:
1. "답변 생성중..." 메시지 먼저 표시
2. GPT 완료 후 실제 내용으로 교체
그래서:
updateMessage(id, { content: realAnswer })
👉 기존 메시지를 덮어쓰기 위해 필수
❓ 왜 전역 상태가 필요했을까?
처음엔 useState로 충분해 보였습니다.
하지만 실제로는:
- ChatContainer
- MessageList
- MessageBubble
- Summary
- Sidebar
👉 여러 컴포넌트가 메시지를 공유
로컬 상태 문제
useState → 컴포넌트 이동 시 초기화
즉,
👉 복원 불가능
그래서:
“채팅 데이터는 전역 상태가 맞다”
결론: Zustand 채택
🧠 2단계 — ChatContainer (실제 데이터 흐름 핵심 ⭐)
📁 components/chat/ChatContainer.tsx
여기가 실제 컨트롤 타워입니다.
모든 흐름이 여기서 시작됩니다.
✅ 전체 흐름 한눈에 보기
[페이지 진입]
↓
세션 복원(localStorage)
↓
세션 없으면 생성
↓
DB에서 메시지 조회
↓
화면 렌더링
↓
사용자 입력
↓
DB 저장
↓
GPT 호출
↓
응답 DB 저장
① 세션 복원
const saved = window.localStorage.getItem(`chatSessionId:${userId}`);
if (saved) {
const parsed = Number(saved);
setSessionId(parsed);
}
왜 필요한가?
새로고침 시:
React state = 전부 초기화
그래서 마지막 세션을 기억해야 합니다.
👉 localStorage 사용
핵심 포인트
chatSessionId:${userId}
👉 사용자별로 분리 저장
구글/일반 로그인 충돌 방지
② 세션 확보 (ensureSession)
const list = await apiFetch("/sessions", {
headers: { "x-user-id": userId },
});
의미
1️⃣ 기존 세션 존재 → 최신 세션 사용
2️⃣ 없으면 → 새 세션 생성
👉 항상 “유효한 세션 ID 확보”
③ 메시지 복원
const res = await apiFetch(`/messages/sessions/${id}`, {
headers: { "x-user-id": userId },
});
의미
DB → 메시지 조회
이 코드 하나로:
✅ 새로고침 복원
✅ 재접속 복원
✅ 기기 변경 복원 가능
👉 DB가 Source of Truth
④ 사용자 메시지 저장
await apiFetch("/messages", {
method: "POST",
body: JSON.stringify({
session_id: id,
role: "user",
content: message,
content_type: "text",
}),
});
핵심
이제부터:
❌ state 기준
⭕ DB 기준
👉 “대화는 항상 DB에 먼저 저장”
⑤ GPT 응답 저장
await apiFetch("/messages", {
method: "POST",
body: JSON.stringify({
session_id: id,
role: "assistant",
content: replyResult.raw,
content_type: replyResult.cards ? "card" : "text",
}),
});
핵심 설계
카드면 → content_type = card
텍스트면 → text
👉 이게 Part 3 카드 복원의 기반
🧠 3단계 — 카드 UI 복원
if (item.content_type === "card") {
cards = parseCardsFromContent(item.content);
}
왜 이렇게 했을까?
카드는 단순 문자열이 아닙니다.
JSON 구조 데이터
그래서:
1️⃣ JSON 그대로 저장
2️⃣ content_type으로 구분
3️⃣ 복원 시 JSON.parse
👉 새로고침 후에도 100% 동일 UI 복원
🧠 4단계 — Google 로그인 버그 해결 (실무 트러블슈팅 ⭐)
이게 오늘 진짜 실무 경험 포인트입니다.
❌ 문제
구글 로그인 시:
- 새로고침하면 대화 사라짐
❓ 원인
session.user.id
이 값이
👉 로그인할 때마다 랜덤 UUID
결과:
세션 저장 user_id ≠ 조회 user_id
세션 매칭 실패 ❌
✅ 해결 전략
방법 1 (실무 정석)
OAuth 매핑 테이블 생성
providerAccountId ↔ 내부 user_id
항상 동일 id 사용
방법 2 (현재 적용)
Google은 email 고정
const userId = user?.id || user?.email || "";
👉 email fallback 사용
결과
✅ 새로고침 유지
✅ 재로그인 유지
✅ 구글 로그인 정상 복원
✅ Part 2 최종 성과
오늘 프론트에서 바뀐 것:
✅ sessionId 복원
✅ 메시지 DB 저장
✅ DB 기준 복원
✅ 카드 UI 복원
✅ Google OAuth 안정화
✅ 프론트 = 단순 View 구조 완성
🎯 면접에서 이렇게 말하면 좋습니다
“프론트는 단순 UI 역할만 하도록 하고,
세션 기반으로 백엔드 DB와 동기화하도록 구조를 변경했습니다.
OAuth 사용자도 동일 user_id 체계를 유지해
새로고침 후에도 대화가 복원되는 구조로 설계했습니다.”
👉 이 한 문장 = 실무 경험 인증 멘트
⭐ Part 2 한 줄 요약
👉 “프론트를 상태 앱이 아니라 DB 동기화 클라이언트로 만든 날”
.
📘 Aura_AI Day 16 — Part 3 (완전판)
카드형 응답 저장/복원 & 실제 데이터 흐름 완전 정리
“UI를 그린 것이 아니라, UI가 복원되도록 설계한 날”
안녕하세요.
Aura AI 프로젝트 Day 16 Part 3 기록입니다.
Day 16은 단순 기능 추가가 아닙니다.
- Part 1 → DB 구조 설계
- Part 2 → 프론트 ↔ DB 연결
- Part 3 → UI 복원 설계 (UX 완성)
즉,
👉 “보여주는 채팅”이 아니라
👉 “언제 다시 와도 동일하게 복원되는 채팅”을 만드는 단계
입니다.
✅ 오늘 해결한 핵심 문제
Day 15까지는 이런 상태였습니다.
❌ 문제점
- GPT 응답을 카드 UI로 예쁘게 렌더링
- 하지만 새로고침하면 전부 텍스트로 깨짐
- 카드 디자인 사라짐
- UX 일관성 붕괴
왜 그럴까요?
👉 UI는 상태(state)였고, 데이터가 아니었기 때문입니다.
🎯 오늘 목표 (한 줄 요약)
카드 UI를 "렌더링 결과"가 아니라
"저장 가능한 데이터 구조"로 만든다
즉,
👉 UI = 데이터 기반 복원 가능 구조
🧠 1단계 — 왜 카드형 저장이 필요한가?
GPT 응답은 실제로 이런 형태입니다.
[
{ "label": "상의", "value": "니트" },
{ "label": "하의", "value": "슬랙스" },
{ "label": "신발", "value": "로퍼" }
]
이건 이미 구조화된 데이터(JSON) 입니다.
그런데 만약 이렇게 저장하면?
content: "니트 + 슬랙스 + 로퍼 추천입니다"
❌ 문제
- 구조 사라짐
- 카드 복원 불가능
- 텍스트만 표시 가능
그래서 선택한 방식
👉 JSON 원본 그대로 저장
🧠 2단계 — content_type 설계 (핵심 ⭐)
Part 1에서 이미 추가했던 필드입니다.
content_type: str = "text"
역할
타입의미
| text | 일반 텍스트 |
| card | 카드형 JSON |
| markdown | 마크다운 |
| summary | 요약 |
즉,
👉 렌더링 방식을 결정하는 메타데이터
⭐ 왜 이게 중요할까?
만약 content_type이 없으면:
이 메시지가 카드인지? 텍스트인지?
→ 알 수 없음
👉 복원 불가능 ❌
그래서:
content_type = UI 복원의 핵심 키
🧠 3단계 — 저장 시 데이터 흐름 (DB 저장 과정)
📌 GPT 응답 수신
const replyResult = {
raw: JSON.stringify(cards),
cards,
};
📌 DB 저장 코드
await apiFetch("/messages", {
method: "POST",
body: JSON.stringify({
session_id: id,
role: "assistant",
content: replyResult.raw, // JSON 원본
content_type: replyResult.cards ? "card" : "text",
}),
});
✅ 여기서 핵심 3가지
① JSON.stringify 그대로 저장
👉 구조 유지
② content_type = "card"
👉 복원 시 판단 기준
③ DB가 진짜 데이터
👉 새로고침해도 그대로 존재
🧠 4단계 — 복원 시 데이터 흐름 (새로고침 과정)
📌 DB 조회
const res = await apiFetch(`/messages/sessions/${id}`);
📌 복원 로직
if (item.content_type === "card") {
cards = parseCardsFromContent(item.content);
}
📌 파싱
function parseCardsFromContent(content: string) {
try {
return JSON.parse(content);
} catch {
return undefined;
}
}
✅ 전체 복원 과정
DB에서 JSON 문자열 조회
↓
content_type === card 확인
↓
JSON.parse
↓
cards 배열 생성
↓
카드 UI 렌더링
👉 결과: 새로고침해도 100% 동일 UI 복원
🧠 5단계 — MessageBubble 렌더링 구조
📁 MessageBubble.tsx
if (message.cards) {
return <CardMessage cards={message.cards} />;
}
return <TextMessage content={message.content} />;
의미
카드 있으면
👉 카드 UI
없으면
👉 텍스트 UI
즉,
UI 분기 = 데이터 기반
❌ if(role === assistant && 뭐시기...) 이런 하드코딩 아님
⭕ 데이터 구조 기반 설계
🧠 6단계 — 이 설계의 실무적 장점
✅ 1. 새로고침 복원 가능
UX 안정성 ↑
✅ 2. 서버 재시작해도 유지
영속성(Persistence)
✅ 3. UI 확장 쉬움
향후:
- carousel 카드
- 이미지 카드
- 버튼 카드
- markdown
👉 content_type 추가만 하면 끝
✅ 4. 프론트/백 분리 설계
프론트:
👉 “어떻게 보여줄지만 결정”
백엔드:
👉 “무슨 데이터인지 정의”
👉 역할 분리 명확 (실무 아키텍처)
🧠 7단계 — 최종 데이터 흐름 한눈 정리
GPT 응답(JSON)
↓
JSON 그대로 DB 저장
↓
content_type = card
↓
새로고침
↓
DB 조회
↓
JSON.parse
↓
카드 UI 렌더링
👉 UI는 데이터에서 생성된다
✅ Day 16 Part 3 최종 성과
오늘 완성된 것:
✅ 카드형 응답 JSON 저장
✅ content_type 기반 UI 분기
✅ 새로고침 후 카드 100% 복원
✅ 구조화 데이터 기반 렌더링
✅ 확장 가능한 메시지 설계
🎯 면접에서 이렇게 말하면 좋습니다
“카드 UI를 단순히 렌더링하지 않고
JSON 데이터를 그대로 DB에 저장했습니다.
content_type으로 렌더링 방식을 구분해
새로고침 후에도 동일한 UI가 복원되도록 설계했습니다.
즉, UI를 상태가 아니라 데이터 기반으로 만들었습니다.”
👉 이 멘트 = 실무 설계 경험 어필 최강
⭐ Day 16 Part 3 한 줄 요약
👉 “UI를 그린 것이 아니라, 복원 가능한 데이터로 설계한 날”
'챗봇 공부 노트' 카테고리의 다른 글
| [17편] Part 2 GPT 컨텍스트 엔지니어링 (0) | 2026.02.05 |
|---|---|
| [17편] Part 1 구글 유저 통합 저장 (0) | 2026.02.05 |
| [15편] 채팅 요약 기능 구현 + UX 고도화 (1) | 2026.02.02 |
| [14편] 날씨 기반 GPT 추천/위치 검색 연동 및 JSON 응답 고도화 (0) | 2026.02.01 |
| [13편] GPT 연동 + 모달 상태 저장 (0) | 2026.02.01 |