📘 Aura_AI Day 12
모달 선택값을 “채팅에서 실제로 쓰는 상태”로 만든 날 — A~Z 완전 정리
안녕하세요.
오늘은 Aura_AI 프로젝트 Day 12에서 진행한 작업을 정리합니다.
Day 12는 기능적으로 보면 화려하지 않습니다.
하지만 서비스 구조 관점에서는 매우 중요한 날입니다.
“사용자가 고른 값이
UI를 넘어서 → 상태가 되고 → 채팅 요청이 되고 → 서버까지 전달된다”
이 흐름을 처음으로 완성한 날이기 때문입니다.
이 글은 노베이스 기준으로 작성되었습니다.
React, Zustand, 전역 상태 관리가 처음이셔도
이 글 하나만 읽으면 Day 12 전체가 이해되도록 설명드리겠습니다.
✅ 0. Day 12 이전 상태부터 정확히 짚고 가기
✔ Day 11까지 이미 되어 있던 것
- 날짜 선택 모달 UI
- 위치 입력 모달 UI
- 성별 / 스타일 선택 모달 UI
- 클릭, 전환, 디자인 모두 정상 동작
👉 사람 눈에는 “선택 잘 되는 앱”처럼 보이는 상태
❌ 하지만 치명적인 문제
- 모달에서 선택한 값이 어디에도 저장되지 않음
- 모달 닫으면 값이 사라짐
- 채팅 컴포넌트는 이 값을 모름
- 서버(FastAPI)는 당연히 모름
즉,
UI = 있음
데이터 = 없음
이 상태로는:
- 날씨 기반 코디 ❌
- GPT 프롬프트 ❌
- “사용자 조건에 따른 추천” ❌
✅ 1. 그래서 Day 12의 목표는 무엇이었나요?
Day 12의 목표는 아주 명확합니다.
🎯 Day 12 목표 3가지
- 모달에서 선택한 값을 사라지지 않게 저장
- 채팅 요청 시 그 값을 함께 전송
- 헤더 / UI 어디서든 같은 값 사용
한 문장으로 요약하면:
“선택 → 데이터화 → 채팅에서 사용” 흐름 완성
✅ 2. 핵심 개념: 왜 ‘전역 상태’가 필요한가요?
❓ 전역 상태란?
어느 컴포넌트에서든 접근 가능한 저장소
React 컴포넌트는 기본적으로:
- 자기 상태는 자기가 가짐
- 컴포넌트가 사라지면 상태도 사라짐
👉 모달은 “열렸다 닫히는 컴포넌트”
👉 그래서 모달 안 state는 쓰기엔 부적합
❌ 전역 상태가 없으면 생기는 문제
- DateModal에서 고른 날짜를
→ ChatContainer로 전달하려면? - LocationModal에서 입력한 위치를
→ Header에서도 보여주려면?
👉 props 지옥 시작
👉 구조 복잡
👉 유지보수 불가능
✅ 3. 그래서 선택한 해결책: Zustand
❓ Zustand란?
- React용 아주 가벼운 전역 상태 관리 라이브러리
- Redux보다 훨씬 단순
- Context API보다 코드가 짧고 명확
❓ 다른 방법도 있었는데 왜 Zustand인가요?
방법단점
| props 전달 | 구조 깊어질수록 유지보수 불가 |
| Context API | 코드 길어짐, 리렌더링 제어 어려움 |
| Zustand | 간단, 직관적, 확장 쉬움 |
👉 이미 인증 상태에서도 Zustand를 쓰고 있었기 때문에
👉 프로젝트 전체 상태 관리 방식이 통일됨
✅ 4. Day 12에 새로 추가된 파일
📄 src/store/chatContextStore.ts
Day 12의 핵심 파일
🔹 왜 이 파일을 추가했나요?
- 날짜 / 위치 / 성별 / 스타일은
👉 “채팅을 위한 공통 컨텍스트” - 모달, 채팅, 헤더, API 요청
👉 여러 곳에서 동시에 사용
그래서 이 값들을 한 곳에 모아 관리하는
전용 전역 상태 저장소가 필요했습니다.
🔹 왜 store/ 폴더에 두었나요?
- store = 전역 상태 관리 전용 폴더
- 인증 상태 store와 역할이 같음
- “이 파일은 UI가 아니라 데이터다”를 명확히 표현
👉 의미 기반 폴더 분리
✅ 5. chatContextStore.ts 전체 코드 + 기초 설명
📄 chatContextStore.ts
import { create } from "zustand";
설명
- create는 Zustand에서 스토어를 만드는 함수
- Redux의 createStore 같은 역할
export interface ChatContextState {
date: Date | null;
location: string;
gender: "남자" | "여자" | "성별무관" | null;
style: string | null;
설명 (아주 중요)
이건 “채팅에 필요한 데이터 설계도” 입니다.
- date: 날짜 선택 (없을 수도 있으니 null 허용)
- location: 위치 문자열
- gender: 제한된 값만 허용 (타입 안정성)
- style: 최종 스타일 텍스트
👉 TypeScript 덕분에
👉 잘못된 값이 들어오는 걸 컴파일 단계에서 차단
setDate: (date: Date) => void;
setLocation: (location: string) => void;
setGender: (gender: ChatContextState["gender"]) => void;
setStyle: (style: string) => void;
}
설명
- 상태를 직접 수정하지 않고
- 반드시 setter 함수로만 변경
👉 상태 변경 흐름이 예측 가능
👉 디버깅 쉬움
👉 실무에서 매우 중요
export const useChatContextStore = create<ChatContextState>((set) => ({
date: null,
location: "",
gender: null,
style: null,
설명
- 실제 초기 상태 값
- 아직 아무것도 선택 안 한 상태
setDate: (date) => set({ date }),
setLocation: (location) => set({ location }),
setGender: (gender) => set({ gender }),
setStyle: (style) => set({ style }),
}));
설명
- set은 Zustand 내부 함수
- { date }는 { date: date } 축약 문법
- 상태 일부만 업데이트해도 자동 병합
✅ 6. 여기까지 정리하면 Day 12의 “뼈대”
지금까지 한 것만 정리하면:
- “채팅에 필요한 정보”를 정의했고
- 그 정보를 저장할 전역 저장소를 만들었고
- 어떤 컴포넌트든 접근 가능해졌습니다
👉 이제 남은 건?
“각 모달에서 이 저장소에 값을 넣어주는 것”
모달 선택값을 채팅·서버까지 연결한 전 과정 — 구현·연동·마무리
Part 1에서는
**“왜 전역 상태가 필요했고, chatContextStore를 왜 만들었는지”**를 설명드렸습니다.
Part 2에서는 이제 실제로:
- 각 모달 UI가 어떻게 전역 상태와 연결되는지
- 그 값이 채팅 요청에 어떻게 포함되는지
- 백엔드에서는 왜 context 필드를 추가했는지
- .gitignore는 왜 건드렸는지
- Day 12 전체 흐름을 A → Z로 완성
까지 정리합니다.
✅ 7. DateModal.tsx — 날짜 선택을 ‘데이터’로 만드는 과정
📌 이 파일의 역할
- 사용자가 캘린더에서 날짜를 고른다
- 그 날짜를 전역 상태(Zustand)에 저장
- 모달을 닫아도 값이 유지된다
🔹 수정된 전체 코드
"use client";
import { type ComponentProps, useState } from "react";
import Calendar from "react-calendar";
import { useChatContextStore } from "@/store/chatContextStore";
type Props = { onClose: () => void };
type CalendarValue = ComponentProps<typeof Calendar>["value"];
기초 설명
- "use client"
→ Next.js App Router에서 클라이언트 컴포넌트 선언 - ComponentProps<typeof Calendar>
→ 라이브러리 컴포넌트의 props 타입을 그대로 재사용 - CalendarValue
→ Date | Date[] | null 가능성을 모두 포함한 안전한 타입
export default function DateModal({ onClose }: Props) {
const [date, setDate] = useState<CalendarValue>(new Date());
const setGlobalDate = useChatContextStore((state) => state.setDate);
왜 로컬 state + 전역 state를 같이 쓰나요?
- date (로컬 state)
- 캘린더 UI 제어용
- setGlobalDate (전역 state)
- “적용하기” 눌렀을 때만 저장
👉 선택 중 상태와 확정된 데이터를 분리한 설계
<button
type="button"
className="modal-submit"
onClick={() => {
if (date instanceof Date) {
setGlobalDate(date);
}
onClose();
}}
>
적용하기
</button>
핵심 포인트
- instanceof Date
- Calendar는 여러 타입을 반환할 수 있음
- 실제 Date 객체일 때만 저장
- 저장 후 모달 닫기
👉 이 순간부터
👉 “날짜는 채팅 컨텍스트의 일부”가 됩니다
✅ 8. LocationModal.tsx — 위치 문자열을 전역 상태로 저장
📌 이 파일의 역할
- 사용자가 직접 위치를 입력하거나
- 현재 위치 좌표를 사용
- 결과를 전역 상태에 저장
🔹 핵심 코드
const [value, setValue] = useState("");
const setLocation = useChatContextStore((state) => state.setLocation);
- value → input 제어용
- setLocation → 전역 저장용
if (trimmed) {
setLocation(trimmed);
onClose();
return;
}
왜 trim()을 쓰나요?
- " 서울 " 같은 입력 방지
- 빈 문자열 저장 방지
👉 사용자 입력은 항상 정제 후 저장
navigator.geolocation.getCurrentPosition(
(pos) => {
const next = `${pos.coords.latitude.toFixed(4)}, ${pos.coords.longitude.toFixed(4)}`;
setLocation(next);
onClose();
},
() => {
alert("현재 위치를 불러오지 못했습니다.");
}
);
여기서 중요한 개념
- 브라우저 API (navigator.geolocation)
- 실패 가능성 고려 → alert 처리
- 좌표를 문자열로 저장
👉 나중에 서버에서:
- 날씨 API
- 위치 기반 추천
등으로 확장 가능
✅ 9. StyleModal.tsx — 가장 복잡한 모달
📌 이 모달이 특별한 이유
- 2단계 UI (성별 → 스타일)
- 성별 + 스타일을 조합
- 최종 결과만 전역 상태에 저장
🔹 전역 상태 연결
const setGlobalGender = useChatContextStore((state) => state.setGender);
const setGlobalStyle = useChatContextStore((state) => state.setStyle);
- 성별은 1단계에서
- 스타일은 2단계에서
onClick={() => {
setGender(label as ChatContextState["gender"]);
setGlobalGender(label as ChatContextState["gender"]);
setStep(2);
}}
왜 성별을 먼저 저장하나요?
- 스타일 목록 분기
- 헤더에서도 즉시 반영 가능
- “선택 중 이탈” 상황에서도 값 유지
const nextStyle =
prefix ? `${prefix} ${item.name}` : item.name;
setGlobalStyle(nextStyle);
왜 문자열로 합쳤나요?
예:
- "여자 베이직 캐주얼"
- "남자 모던 미니멀"
👉 서버 / GPT 프롬프트에서
👉 문맥 이해가 훨씬 쉬움
✅ 10. ChatContainer.tsx — 선택값을 실제 채팅 요청에 사용
📌 이 파일의 역할
- 채팅 입력
- API 호출
- 컨텍스트 검증
- 요청 payload 생성
const { date, location, gender, style } = useChatContextStore();
👉 이제 채팅은
👉 모달에서 고른 모든 정보를 알고 있음
if (!date || !location || !gender || !style) {
alert("날짜, 위치, 성별, 스타일을 선택해 주세요.");
return;
}
왜 여기서 검증하나요?
- 서버에 쓰레기 요청 방지
- UX 명확
- 백엔드는 “항상 완성된 데이터”만 받음
const contextText =
`날짜: ${dateText}, 위치: ${location}, 성별: ${gender}, 스타일: ${style}`;
👉 이 문자열이
👉 GPT 프롬프트의 핵심 재료
✅ 11. ChatHeader.tsx — 선택값을 사용자에게 보여주기
📌 역할
- “내가 뭘 선택했는지” 바로 확인
- 신뢰감 + UX 개선
const dateText = date ? date.toLocaleDateString("ko-KR") : "미설정";
👉 데이터가 없을 수도 있다는 가정
👉 항상 안전한 UI
✅ 12. 백엔드 수정 — context를 받기 위해
📄 chat.py
class ChatRequest(BaseModel):
message: str
context: str | None = None
설명
- 기존엔 message만 받았음
- 이제 추가 정보(context) 수용
📄 chats.py
context = f" | {body.context}" if body.context else ""
- context가 있을 때만 사용
- 문자열 합성 방식
👉 실제 GPT 연동 시
👉 이 부분이 prompt에 들어감
✅ 13. .gitignore — pycache 정리
❓ __pycache__란?
- Python 실행 시 자동 생성
- 바이트코드 캐시
- 코드 아님 / 결과물 아님
❌ 왜 Git에 올리면 안 되나요?
- 사람마다 다름
- 환경 종속
- 저장소 더러워짐
✔ .gitignore에 추가
__pycache__/
*.pyc
✔ 이미 올라간 파일 제거
git rm -r --cached backend/__pycache__
✅ 14. Day 12 전체 연동 흐름 (A → Z)
모달 선택
→ Zustand 저장
→ ChatHeader 표시
→ ChatContainer 검증
→ API 요청(context 포함)
→ FastAPI 수신
→ 응답 생성
✅ 15. Day 12 수정 / 추가 파일 요약
➕ 추가
- chatContextStore.ts
✏ 수정
- DateModal.tsx
- LocationModal.tsx
- StyleModal.tsx
- ChatContainer.tsx
- ChatHeader.tsx
- chat.py
- chats.py
- .gitignore
✅ 최종 요약 한 문장
“UI로 고른 값이 실제 채팅과 서버 로직에서 쓰이도록 처음으로 완전히 연결한 날”
'챗봇 공부 노트' 카테고리의 다른 글
| [14편] 날씨 기반 GPT 추천/위치 검색 연동 및 JSON 응답 고도화 (0) | 2026.02.01 |
|---|---|
| [13편] GPT 연동 + 모달 상태 저장 (0) | 2026.02.01 |
| [11편] API 구조 & 데이터 계약(Contract) 고정 (0) | 2026.01.26 |
| [10편] FastAPI 백엔드 분리 & 프론트엔드 연동 (1) | 2026.01.26 |
| [9편] Zustand로 인증 상태 통합 (0) | 2026.01.25 |