UI 컴포넌트를 Layout / Page / Widget 계층으로 나누고,
공통 레이아웃(Header
, Sidebar
)과 개별 페이지(MainPage
, ChatDetailPage
)를 구분합니다.
Zustand 스토어와 Firestore 연동 로직은 별도 모듈로 관리하여, 어디서든 동일한 방식으로 상태를 읽고 쓸 수 있게 합니다.
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 래퍼 & 멀티턴 로직
계층 | 경로 / 이름 | 핵심 책임 |
---|---|---|
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 |
모달 열림/닫힘 플래그 |
사용자 입력 → ChatInput
fetch('/api/gemini-stream')
로 프록시 POST, streamStore
에 AbortController 저장
Express 라우터 geminiStream.ts
utils/gemini.ts
의 streamGeminiResponse()
호출 → Gemini API SSE 스트림 수신
SSE chunk → 백엔드에서 그대로 res.write()
프론트는 ReadableStream.getReader()
로 조각 수신
Zustand chatStore 업데이트
마지막 assistant 메시지에 텍스트 누적 → ChatList
가 리렌더