📘 Aura_AI Day 15
🔥 채팅 요약 기능 구현 + UX 고도화 (A~Z 완전 정복 · 심화 해설판)
안녕하세요.
Aura AI 프로젝트 Day 15 개발 기록입니다.
오늘은 단순 기능 추가가 아니라,
👉 “사용성(UX)을 진짜로 개선한 날” 입니다.
🚨 왜 이 기능을 만들었을까? (문제 인식부터)
Day 14까지 우리는 GPT 추천 품질을 계속 올렸습니다.
- 더 정확한 코디 추천
- 더 자연스러운 대화
- 더 똑똑한 답변
하지만…
실제 사용을 해보니 전혀 다른 문제가 발생했습니다.
❌ 실제 사용자 관점 문제
채팅이 길어질수록:
- 스크롤 계속 내려야 함
- 예전 추천 다시 찾기 힘듦
- 정보가 너무 많아 피로감 증가
- “그래서 오늘 뭐 입으라는 거지?” 발생
즉,
GPT는 똑똑한데
❌ 사람이 쓰기 불편함
이건 AI 서비스에서 가장 흔한 실패 케이스입니다.
기능은 좋은데 UX가 나쁜 경우죠.
✅ 오늘 목표 (한 줄 정의)
"긴 대화를 핵심만 자동 요약해서, 한눈에 이해 가능하게 만들자"
💡 핵심 설계 전략
여기서 중요한 의사결정이 하나 있었습니다.
🤔 GPT를 언제 호출할 것인가?
❌ 방법 1 — 항상 자동 요약
- 매 메시지마다 GPT 호출
- 실시간 요약
문제점:
- 💸 비용 폭발
- 🐢 느려짐
- 🔥 서버 부하
✅ 방법 2 — On-Demand 요약 (채택)
- 버튼 클릭 시만 호출
- 사용자가 필요할 때만 실행
장점:
- 비용 절감
- 서버 안정성
- UX 명확
- 실무에서 가장 많이 쓰는 패턴
👉 그래서 On-Demand 방식 채택
🧠 전체 구조 먼저 이해하기 (가장 중요)
코드 보기 전에 흐름 먼저 이해하세요.
이걸 모르면 코드 100번 봐도 이해 안 됩니다.
🔥 전체 데이터 흐름
사용자 채팅
↓
요약 버튼 클릭
↓
메시지 필터링 (user + assistant)
↓
POST /summaries 요청
↓
FastAPI Router
↓
summary_service → GPT 호출
↓
요약 결과 반환
↓
Zustand 전역 저장
↓
UI 렌더링
👉 핵심 포인트:
- 프론트 = UI 담당
- 백엔드 = GPT 호출 담당
- 상태 = Zustand 관리
책임 분리가 명확합니다. (⭐ 실무 필수 설계)
✅ 변경/추가 파일 전체 목록 (절대 누락 없음)
✅ 새로 생성
backend/schemas/summary.py
backend/services/summary_service.py
backend/routers/summaries.py
store/summaryStore.ts
components/chat/ChatSummary.tsx
✅ 수정
backend/main.py
components/chat/ChatContainer.tsx
components/chat/MessageList.tsx
components/chat/MessageBubble.tsx
📂 1️⃣ backend/schemas/summary.py
👉 요청/응답 데이터 "형식 정의 파일"
✅ 전체 코드
from typing import List
from pydantic import BaseModel
class SummaryRequest(BaseModel):
messages: List[str]
class SummaryResponse(BaseModel):
summary: str
🧠 이 파일은 뭐하는 파일인가요?
👉 API 요청/응답의 데이터 구조를 정의하는 곳
쉽게 말하면:
"프론트야, 이런 형식으로 보내줘"
라고 약속(계약)하는 파일입니다.
📌 문법 기초 설명
BaseModel
FastAPI + Pydantic에서 사용하는 데이터 검증 클래스
class A(BaseModel):
👉 자동으로:
- 타입 검사
- JSON 변환
- 에러 처리
해줍니다.
List[str]
messages: List[str]
의미:
👉 문자열 배열
예:
[
"사용자: 오늘 추워",
"AI: 코트 추천"
]
❓ 왜 이 파일을 따로 만들었을까?
❌ 그냥 dict 쓰면 안 되나요?
가능은 합니다.
하지만…
- 타입 안전성 ❌
- 자동 문서화 ❌
- 유지보수 ❌
✅ schema 분리 장점
장점설명
| 타입 안정성 | 잘못된 데이터 자동 차단 |
| Swagger 자동 문서화 | API 문서 자동 생성 |
| 유지보수 쉬움 | 구조 변경 시 한 곳만 수정 |
| 실무 표준 | 거의 모든 FastAPI 프로젝트 방식 |
👉 그래서 무조건 분리합니다
📂 2️⃣ backend/services/summary_service.py
👉 GPT 호출 "핵심 비즈니스 로직"
✅ 전체 코드
import logging
import os
from dotenv import load_dotenv
from openai import AsyncOpenAI
load_dotenv()
client = AsyncOpenAI(api_key=os.getenv("OPENAI_API_KEY"))
MAX_MESSAGES = 20
async def summarize_messages(messages: list[str]) -> str:
if len(messages) < 3:
return "대화 내용이 부족하여 요약할 수 없습니다."
try:
messages = messages[-MAX_MESSAGES:]
joined = "\n".join(messages)
prompt = f"""
당신은 패션 코디 기록 전문가입니다.
[규칙]
- 날씨 상황
- 추천된 코디 아이템
- 핵심 정보만 3줄 이내
- "~함", "~임" 개조식 문체
[대화]
{joined}
"""
res = await client.chat.completions.create(
model="gpt-4o-mini",
temperature=0.3,
timeout=20,
messages=[
{"role": "system", "content": "핵심 정보 요약 전문가"},
{"role": "user", "content": prompt},
],
)
return res.choices[0].message.content
except Exception as exc:
logging.error(f"Summary Error: {exc}")
return "요약 생성에 실패했습니다. 잠시 후 다시 시도해주세요."
🧠 이 파일 역할 (중요 ⭐)
👉 GPT 호출 전담 파일
Router에서 직접 GPT 호출 ❌
Service에서만 GPT 호출 ⭕
❓ 왜 Router에서 바로 안 부르나요?
❌ Router 직접 호출
- 코드 섞임
- 테스트 어려움
- 재사용 불가
✅ Service 분리
- 로직 재사용 가능
- 테스트 쉬움
- 책임 분리 명확
👉 실무 100% 패턴
🔥 핵심 설계 포인트 해설
① MAX_MESSAGES = 20
messages = messages[-MAX_MESSAGES:]
👉 최근 20개만 사용
이유
- 토큰 비용 절약
- GPT 속도 향상
- 불필요한 과거 제거
👉 실무에서 "Context Window 최적화"라고 부름
② temperature = 0.3
temperature=0.3
- 0 → 딱딱, 정확
- 1 → 창의적, 랜덤
요약은?
👉 정확성이 중요
그래서 0.3
③ timeout = 20
timeout=20
GPT가 멈추면 서버도 멈춤 ❌
→ 20초 후 강제 종료
👉 서버 안정성 필수 옵션
④ async/await 사용 이유
async def summarize_messages()
await client.chat...
동기(sync)
- 요청 하나 끝날 때까지 대기
- 느림
비동기(async)
- 다른 요청 처리 가능
- 빠름
👉 FastAPI = 비동기 최적화 프레임워크
👉 그래서 async 필수
✅ 여기까지 Part 1
지금까지:
✅ 문제 정의
✅ 설계 전략
✅ 전체 구조
✅ schemas 설명
✅ service 완전 해설
📘 Aura_AI Day 15 (Part 2)
🔥 채팅 요약 기능 구현 + UX 고도화 — 프론트엔드 & 아키텍처 완전 해설
앞 Part 1에서는
✅ 왜 요약이 필요한지
✅ 전체 설계 전략
✅ schema / service 구조
✅ GPT 호출 로직
까지 모두 구현했습니다.
이제 남은 작업은:
👉 프론트에서 “사용자가 실제로 쓰는 경험 만들기”
사실…
👉 UX는 프론트에서 결정됩니다.
GPT 아무리 좋아도
버튼 불편하면 → 안 씁니다.
그래서 Day 15의 핵심은 프론트 설계라고 봐도 됩니다.
📂 3️⃣ backend/routers/summaries.py
👉 API 엔드포인트 (프론트와 서버 연결 지점)
✅ 전체 코드
from fastapi import APIRouter
from schemas.summary import SummaryRequest, SummaryResponse
from services.summary_service import summarize_messages
router = APIRouter(prefix="/summaries", tags=["summaries"])
@router.post("", response_model=SummaryResponse)
async def summarize(body: SummaryRequest):
result = await summarize_messages(body.messages)
return {"summary": result}
🧠 이 파일은 뭐 하는 곳인가요?
👉 "HTTP 요청을 받는 입구"
브라우저 → FastAPI 요청 시
가장 먼저 도착하는 곳입니다.
🔥 코드 해설 (노베이스 기준)
APIRouter
router = APIRouter(prefix="/summaries")
의미:
👉 이 파일 안의 모든 API는
/summaries
로 시작함
즉:
POST /summaries
@router.post("")
@router.post("")
👉 POST 요청 받을게요
프론트에서:
fetch("/summaries", { method: "POST" })
하면 여기로 옴
body: SummaryRequest
async def summarize(body: SummaryRequest):
👉 자동으로 JSON → 객체 변환
예:
{
"messages": ["a", "b"]
}
↓
body.messages
바로 사용 가능
이게 Pydantic의 힘
response_model
response_model=SummaryResponse
👉 반환 타입 강제
장점:
- 타입 안전
- Swagger 자동 문서
- 프론트 협업 쉬움
❓ 왜 Router/Service 나눴을까?
❌ 한 파일에 다 넣으면?
- 테스트 어려움
- 코드 복잡
- 유지보수 지옥
✅ 분리하면?
역할담당
| Router | 요청/응답 |
| Service | GPT 로직 |
👉 실무 표준 구조 (Controller / Service 패턴)
면접 단골 질문입니다 ⭐
📂 4️⃣ backend/main.py
👉 라우터 등록
✅ 코드
from routers.summaries import router as summaries_router
app.include_router(summaries_router)
🧠 왜 필요?
Router 만들기만 하면 ❌
FastAPI에 등록해야 ⭕
이 코드 없으면:
404 Not Found
뜹니다.
👉 “API 활성화 스위치”라고 생각하세요.
💻 이제 프론트엔드 시작
여기부터가 Day 15 핵심입니다.
📂 5️⃣ store/summaryStore.ts
👉 Zustand 전역 상태 관리
✅ 전체 코드
import { create } from "zustand";
interface SummaryState {
summary: string;
setSummary: (summary: string) => void;
clearSummary: () => void;
}
export const useSummaryStore = create<SummaryState>((set) => ({
summary: "",
setSummary: (summary) => set({ summary }),
clearSummary: () => set({ summary: "" }),
}));
🧠 왜 상태 관리가 필요할까?
❌ local state만 쓰면?
useState()
- 컴포넌트 이동 시 초기화
- 공유 불가능
✅ 전역 상태 쓰면?
- 어디서든 접근
- 재사용 쉬움
- UX 안정적
👉 그래서 Zustand 사용
❓ 왜 Redux 말고 Zustand?
ReduxZustand
| 복잡 | 매우 간단 |
| 보일러플레이트 많음 | 코드 짧음 |
| 러닝커브 높음 | 초보자 친화 |
👉 작은~중간 규모 프로젝트 최적
📂 6️⃣ components/chat/ChatSummary.tsx
👉 ⭐ Day 15 핵심 UI 컴포넌트
✅ 전체 코드
"use client";
import { useState } from "react";
import { apiFetch } from "@/lib/apiClient";
import { useSummaryStore } from "@/store/summaryStore";
type Props = {
messages: { role: string; content: string }[];
};
export default function ChatSummary({ messages }: Props) {
const { summary, setSummary } = useSummaryStore();
const [loading, setLoading] = useState(false);
const handleSummary = async () => {
const summaryMessages = messages
.filter((m) => m.role === "assistant" || m.role === "user")
.map((m) => `${m.role === "user" ? "사용자" : "AI"}: ${m.content}`);
if (summaryMessages.length < 4) {
alert("대화가 조금 더 쌓이면 요약할 수 있어요!");
return;
}
setLoading(true);
try {
const res = await apiFetch("/summaries", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({ messages: summaryMessages }),
});
setSummary(res.summary);
} finally {
setLoading(false);
}
};
return (
<div>
{summary ? (
<p>{summary}</p>
) : (
<button onClick={handleSummary}>
{loading ? "요약 중..." : "요약하기"}
</button>
)}
</div>
);
}
🔥 여기 진짜 중요합니다 (UX 핵심)
① "use client"
Next.js App Router에서
👉 브라우저에서 실행할 컴포넌트 표시
없으면:
- 이벤트 안 됨
- 버튼 안 눌림
② 메시지 필터링
.filter((m) => m.role === "assistant" || m.role === "user")
왜?
- system 메시지 제외
- 내부 데이터 제외
👉 GPT 요약 품질 ↑
③ On-Demand 호출
onClick={handleSummary}
자동 호출 ❌
버튼 클릭 시 호출 ⭕
👉 비용 절약 핵심
④ 로딩 UX
loading ? "요약 중..." : "요약하기"
없으면?
- 버튼 눌렸는지 모름
- UX 최악
👉 작은 디테일 = 큰 만족도
📂 7️⃣ ChatContainer.tsx 수정
✅ 코드
const { clearSummary } = useSummaryStore();
useEffect(() => {
clearSummary();
}, [clearSummary]);
<ChatSummary messages={messages} />
🧠 왜 필요?
문제
다른 채팅방 이동 시
이전 요약 남아있음 ❌
해결
👉 방 바뀌면 초기화
📂 8️⃣ MessageList / MessageBubble
Day 15에서는
👉 구조 그대로 유지
왜?
👉 책임 분리
- Day 15 → 요약 기능
- Day 16 → UI 개선
한 번에 다 하면 디버깅 지옥
실무는 작게 나눠 개발
🎯 Day 15 최종 결과
✅ 기능
- 요약 버튼
- GPT 요약
- 재요약
- 로딩 UI
- 상태 관리
✅ 기술 설계
- Router/Service 분리
- Zustand 전역 상태
- On-Demand GPT
- 토큰 제한
- 비동기 처리
💼 면접에서 이렇게 말하면 합격각
Q. 요약 기능 왜 만들었나요?
👉
"GPT 성능 개선보다 UX 병목 해결이 더 중요하다고 판단했습니다."
Q. 왜 On-Demand?
👉
"비용 + 서버 부하 + 사용자 의도 기반 UX 때문입니다."
Q. Router/Service 분리 이유?
👉
"관심사 분리로 테스트/유지보수성 향상을 위해서입니다."
Q. Zustand 선택 이유?
👉
"Redux 대비 간단하고 작은 프로젝트에 최적이기 때문입니다."
🎯 Day 15 한 줄 결론
GPT를 더 똑똑하게 만든 날이 아니라
사용자가 더 편하게 쓰게 만든 날
👉 이게 진짜 실무 개발입니다.
'챗봇 공부 노트' 카테고리의 다른 글
| [17편] Part 1 구글 유저 통합 저장 (0) | 2026.02.05 |
|---|---|
| [16편] 채팅 세션 단위 메시지 저장 (1) | 2026.02.03 |
| [14편] 날씨 기반 GPT 추천/위치 검색 연동 및 JSON 응답 고도화 (0) | 2026.02.01 |
| [13편] GPT 연동 + 모달 상태 저장 (0) | 2026.02.01 |
| [12편] 모달 선택값 전역 저장 및 채팅 context 연동 (0) | 2026.01.30 |