StackUp 프론트엔드. React 19 + TypeScript 5.9 + Vite 8 기반 SPA. 구조는 FSD (Feature-Sliced Design).
상위 컨텍스트: /CLAUDE.md · 횡단 관심사: /docs/
| 영역 | 기술 | 버전 |
|---|---|---|
| Framework | React | 19.x |
| Build | Vite | 8.x |
| Language | TypeScript | 5.9.x |
| Lint | ESLint 9 (flat config) + Prettier | |
| Module | ESM ("type": "module") |
- Router: 후보 — React Router v7 / TanStack Router
- Server State: TanStack Query
- Form: React Hook Form + Zod
- Styling: CSS Variables 기반 + Tailwind 또는 vanilla-extract
- 테스트: Vitest + Testing Library + Playwright(E2E)
신규 의존성 추가 시
/docs/coding-conventions.md §6절차를 따르고 본 표를 갱신.
frontend/
├── public/
├── src/
│ ├── app/ # 앱 부트스트랩 (providers, router, styles)
│ │ ├── providers/
│ │ ├── router/
│ │ └── styles/
│ ├── pages/ # 라우트 단위 페이지
│ │ ├── Login/
│ │ ├── Workspace/
│ │ ├── Interview/
│ │ └── History/
│ ├── features/ # 기능 단위 (사용자 행동)
│ │ ├── auth/
│ │ ├── resume/
│ │ ├── interview/
│ │ └── feedback/
│ ├── domain/ # 도메인 모델·비즈니스 규칙
│ │ ├── user/
│ │ ├── session/
│ │ └── rag/
│ ├── shared/ # 도메인 비종속 재사용
│ │ ├── ui/ # 디자인 시스템 컴포넌트
│ │ ├── api/ # 자동 생성 타입, axios 래퍼
│ │ ├── hooks/
│ │ ├── lib/ # AsyncBoundary 등
│ │ ├── utils/
│ │ └── i18n/
│ └── assets/
├── index.html
├── vite.config.ts
├── tsconfig.json
└── eslint.config.js
각 슬라이스 디렉토리에 자체 CLAUDE.md 가 있으므로 작업 시 가장 가까운 것을 먼저 읽는다.
app ─→ pages ─→ features ─→ domain ─→ shared
- 화살표는 import 가능 방향. 역방향 import는 ESLint로 차단한다.
- 같은 레이어 내 슬라이스 간 import 금지 (예:
features/auth→features/resume✗).- 공통화가 필요하면 한 단계 아래(
domain또는shared)로 추출.
- 공통화가 필요하면 한 단계 아래(
pages가pagesimport도 금지 (라우터에서 lazy import만 허용).
app/providers는pages를 import하지 않지만features/*의 store/provider를 wrap할 수 있다.- 타입(
type-only import)은 의존성 규칙에서 제외 (런타임 의존이 없으므로).
각 슬라이스(features/{name}, domain/{name} 등)는 다음 구조를 권장:
features/auth/
├── ui/ # 컴포넌트
├── model/ # store, hooks, 상태 로직
├── api/ # 이 feature가 호출하는 API
├── lib/ # 슬라이스 내부 유틸
└── index.ts # public API (외부에서 import 가능한 것만 export)
Public API 규칙: 슬라이스 외부에서는 항상 index.ts로 import.
// 좋음
import { LoginButton, useAuth } from '@/features/auth';
// 나쁨 (내부 경로 직접 참조)
import { LoginButton } from '@/features/auth/ui/LoginButton';라우터 결정 전이라도 다음 규칙 유지:
- 라우트 정의는
app/router/에 집중 - 각 페이지는
pages/{Name}/index.ts에서 default export - 페이지는 layout + composition만 담당. 비즈니스 로직은 features로.
예상 라우트 (Phase 1):
/ → / (redirect to /workspace or /login)
/login → pages/Login
/auth/callback → OAuth 콜백 처리
/workspace → pages/Workspace (이력서·레포 관리)
/sessions/new → pages/Interview (세션 설정)
/sessions/:id → pages/Interview (세션 진행)
/sessions/:id/feedback → pages/Interview (피드백)
/history → pages/History
/history/:id → pages/History (상세)
shared/lib/AsyncBoundary 사용. props 이름은 pendingFallback / rejectedFallback:
<AsyncBoundary
pendingFallback={<ResumeListSkeleton />}
rejectedFallback={({ error, reset }) => (
<ErrorState error={error} onRetry={reset} />
)}
>
<ResumeList />
</AsyncBoundary>서버 상태는 TanStack Query 도입 후 useSuspenseQuery로 일원화 (Suspense + Error Boundary와 자연 통합).
- Backend OpenAPI →
shared/api/generated.ts(openapi-typescript) - 빌드 스크립트 (도입 시):
"openapi": "openapi-typescript http://localhost:8080/api/v3/api-docs -o src/shared/api/generated.ts"
shared/api/client.ts단일 클라이언트- 기본 헤더:
Authorization,X-Trace-Id(클라이언트 생성) - 401 응답 시 refresh → 원 요청 재시도 (interceptor)
- 에러는 표준 에러 코드 (
/docs/api-conventions.md §5) 기반 분기
- 각 feature는 자체
api/폴더에서 query/mutation 정의 - 컴포넌트는
useXxxQuery,useXxxMutation훅으로만 호출
- 토큰:
app/styles/tokens.css(CSS variables) - 컴포넌트:
shared/ui/{Component}/ - 상세 토큰·인벤토리:
/docs/design-system.md
원칙: 컴포넌트에서 색상·간격·타이포그래피는 토큰만 참조. 하드코딩 금지.
- 마이크:
navigator.mediaDevices.getUserMedia({ audio: true }) - WebRTC: RealTime 서버와 SDP/ICE 교환
- 코덱: 음성은 Opus, 영상은 VP9
- 권한 거부 시 텍스트 입력 fallback (US-21 AC-05)
- 구현 위치:
features/interview/lib/media/
- SSE 단일화 — 양방향 WebSocket 미사용. 모든 서버 → 클라이언트 푸시는 SSE로 처리.
- 구현:
shared/hooks/useEventStream.ts(자동 재연결 + 폴링 fallback) - 미디어 스트림(음성/영상)만 WebRTC:
features/interview/lib/media/ - 이벤트 스펙:
/docs/event-stream.md
VITE_ 접두 필수 (런타임 노출).
.env.local (커밋 X) 사용. 자세한 키 목록: /docs/environment.md §5.
// shared/config/env.ts
export const env = {
API_BASE_URL: import.meta.env.VITE_API_BASE_URL,
SSE_BASE_URL: import.meta.env.VITE_SSE_BASE_URL,
GITHUB_OAUTH_CLIENT_ID: import.meta.env.VITE_GITHUB_OAUTH_CLIENT_ID,
} as const;strict: true유지any금지 (실수 발견 시unknown+ type guard로 전환)- 함수형 컴포넌트만 (
React.FC사용 안 함, props는 명시적 interface/type)
- 키 prop은 의미 있는 ID (배열 index 회피)
- side-effect는
useEffect최소화 — 가능하면 server state로 위임 useState초기화에 비싼 연산 → lazy initialization
- React / 외부
@/app,@/pages,@/features,@/domain,@/shared- 상대경로
- CSS / asset
- 기본 주석 없음. WHY일 때만. (
/docs/coding-conventions.md §3)
- 단위: Vitest + Testing Library
- E2E: Playwright (
frontend/e2e/) - MSW로 API mocking
- 핵심 시나리오:
/docs/testing-strategy.md §3
npm run dev # 로컬 개발 서버 (default :5173)
npm run build # 타입 체크 + 프로덕션 빌드 (dist/)
npm run preview # 빌드 결과 로컬 서빙
npm run lint # ESLint배포: CloudFront + S3 정적 호스팅 (Phase 2 운영 단계).
| 작업 | 위치 |
|---|---|
| 새 페이지 추가 | pages/{Name}/, 라우터 등록 |
| 새 도메인 기능 | features/{name}/, public API 정의 |
| 새 컴포넌트 (도메인 비종속) | shared/ui/{Name}/ |
| 새 API 엔드포인트 사용 | OpenAPI 재생성 → features/{name}/api/ |
| 토큰 추가/변경 | app/styles/tokens.css + /docs/design-system.md 갱신 |
| 새 라우트 | app/router/ |
App.tsx는 Vite 기본 데모 상태 → 첫 페이지(Login) 구현 시 교체 예정- 라이브러리 결정 전 (라우터, server state, styling) → 첫 PR 시 의사결정 + 본 문서 §1 갱신
- 디자인 시스템 토큰 파일 미생성 → 디자인 시스템 첫 적용 PR에서
app/styles/tokens.css생성