1. 설계 원칙

  1. 관심사의 분리 (Separation of Concerns)
  2. 재사용성 및 유지보수성
  3. 실시간 데이터 흐름

2. 프로젝트 설계 미리보기

2-1. 전체 디렉터리 레이아웃

react-gemini/
├─ src/                     # 클라이언트(React 19)
│  ├─ components/           # 재사용 UI & 유틸 컴포넌트
│  │  ├─ ChatInput/
│  │  ├─ ChatList/
│  │  ├─ Header/
│  │  ├─ Main/
│  │  ├─ Modal/
│  │  ├─ Sidebar/
│  │  └─ ThreadList/
│  │
│  │── PrivateRoute.tsx     # 인증 보호 라우트 HOC
│  ├─ hooks/                # 커스텀 훅
│  │  └─ useAuth.ts
│  ├─ pages/                # 라우팅 단위 페이지
│  │  ├─ MainPage.tsx
│  │  ├─ ChatDetailPage.tsx
│  │  ├─ ThreadListPage.tsx
│  │  ├─ LoginPage.tsx
│  │  └─ SignupPage.tsx
│  ├─ store/                # Zustand 상태 스토어
│  │  ├─ chatStore.ts
│  │  ├─ modalStore.ts
│  │  └─ streamStore.ts
│  ├─ firebase.ts           # Firebase SDK 초기화
│  ├─ firebaseUtils.ts      # Firestore/Auth 헬퍼
│  ├─ App.tsx               # 최상위 레이아웃 + 라우터
│  └─ index.tsx             # React Dom 진입점
│
└─ server/                  # 백엔드(Express + Gemini)
   ├─ index.ts              # 앱 부트스트랩 및 정적 파일 서빙
   ├─ routes/
   │  ├─ chat.ts            # REST 예시 라우터
   │  └─ geminiStream.ts    # SSE 스트리밍 라우터
   └─ utils/
      └─ gemini.ts          # @google/genai 래퍼 & 멀티턴 로직

2-2. 프런트엔드 컴포넌트 계층

계층 경로 / 이름 핵심 책임
Layout App.tsx React‑Router 등록, Header·Sidebar 고정 렌더
Header/ 로고, 사이드바 토글, 프로필 모달 트리거
Sidebar/ 전역 네비게이션New Chat / Thread List / Profile
Page pages/MainPage.tsx 신규 스레드 생성 후 /chats/:id로 이동
pages/ChatDetailPage.tsx Firestore + SSE로 메시지 구독, ChatList+ChatInput 배치
pages/ThreadListPage.tsx ThreadList에 스레드 요약 전달
pages/LoginPage.tsx, SignupPage.tsx Firebase Auth 폼
Widget ChatList/ Markdown 렌더 + 로딩 스피너
ChatInput/ 입력, /api/gemini-stream POST, 중단(AbortController)
ThreadList/ 첫 질문·답변 미리보기 카드
Modal/ 공용 모달 shell (프로필 등)
PrivateRoute.tsx 로그인 여부에 따라 페이지 가드
State chatStore.ts 메시지·스레드 CRUD, Firestore sync
streamStore.ts 현재 SSE AbortController 저장
modalStore.ts 모달 열림/닫힘 플래그

2-3. 데이터 흐름 (요약)

  1. 사용자 입력 → ChatInput

    fetch('/api/gemini-stream') 로 프록시 POST, streamStore에 AbortController 저장

  2. Express 라우터 geminiStream.ts

    utils/gemini.tsstreamGeminiResponse() 호출 → Gemini API SSE 스트림 수신

  3. SSE chunk → 백엔드에서 그대로 res.write()

    프론트는 ReadableStream.getReader()로 조각 수신

  4. Zustand chatStore 업데이트

    마지막 assistant 메시지에 텍스트 누적 → ChatList가 리렌더