챗봇 공부 노트

[15편] 채팅 요약 기능 구현 + UX 고도화

frontend-diary-log 2026. 2. 2. 13:41

 

📘 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를 더 똑똑하게 만든 날이 아니라
사용자가 더 편하게 쓰게 만든 날

👉 이게 진짜 실무 개발입니다.