챗봇 공부 노트

[12편] 모달 선택값 전역 저장 및 채팅 context 연동

frontend-diary-log 2026. 1. 30. 19:03

📘 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가지

  1. 모달에서 선택한 값을 사라지지 않게 저장
  2. 채팅 요청 시 그 값을 함께 전송
  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로 고른 값이 실제 채팅과 서버 로직에서 쓰이도록 처음으로 완전히 연결한 날”