|
| 1 | +# 프론트엔드 타입 시스템 — TypeScript |
| 2 | + |
| 3 | +> StackUp 프론트엔드의 TypeScript 타입 규약. **타입은 사용되는 레이어에 산다.** 이름의 접미사가 책임 경계를 선언한다. |
| 4 | +> |
| 5 | +> 상위 컨텍스트 — [coding-conventions.md](./coding-conventions.md) (언어 공통 규약), [`frontend/CLAUDE.md`](../frontend/CLAUDE.md) (FSD 구조) |
| 6 | +> 적용 범위 — [`frontend/src/`](../frontend/src/) |
| 7 | +
|
| 8 | +--- |
| 9 | + |
| 10 | +## 목차 |
| 11 | + |
| 12 | +1. [원칙](#1-원칙) |
| 13 | +2. [접미사 ↔ 레이어 매트릭스](#2-접미사--레이어-매트릭스) |
| 14 | +3. [레이어별 타입 흐름](#3-레이어별-타입-흐름) |
| 15 | +4. [`type` vs `interface`](#4-type-vs-interface) |
| 16 | +5. [정의 위치](#5-정의-위치) |
| 17 | +6. [shared 승격 임계](#6-shared-승격-임계) |
| 18 | +7. [안티패턴](#7-안티패턴) |
| 19 | + |
| 20 | +--- |
| 21 | + |
| 22 | +## 1. 원칙 |
| 23 | + |
| 24 | +- **PascalCase + 의미 있는 접미사**. 접미사가 타입의 책임·레이어를 신고한다. |
| 25 | +- `I` / `T` Hungarian prefix 금지 — TypeScript 컴파일러가 `type` / `interface` 를 충분히 구분한다. |
| 26 | +- **단방향 흐름**: `Dto → Entity → Model → Props`. 역방향 import 금지. |
| 27 | +- **전역 `types/` 폴더 금지**. 슬라이스 내부에서 시작, 진짜 공유될 때만 끌어올린다. |
| 28 | + |
| 29 | +--- |
| 30 | + |
| 31 | +## 2. 접미사 ↔ 레이어 매트릭스 |
| 32 | + |
| 33 | +| 접미사 | 레이어 | 책임 | 예 | |
| 34 | +|---|---|---|---| |
| 35 | +| `XxxDto` | `shared/api/` (OpenAPI 자동 생성) | 백엔드 wire format 스냅샷. 수정 금지 | `SessionDto`, `UserDto` | |
| 36 | +| `XxxRequest` / `XxxPayload` | API 호출부, 이벤트 publisher | HTTP 요청 body, RabbitMQ 메시지 payload | `CreateSessionRequest`, `ResumeAnalyzedPayload` | |
| 37 | +| `XxxResponse` | API 호출부 | HTTP 응답 raw shape (수동 정의 시) — **함수 반환에 사용 ✗** | `LoginResponse` | |
| 38 | +| `Xxx` (접미사 없음) | `domain/{slice}/model/` | 비즈니스 엔터티. Dto에서 매핑됨 | `User`, `Session`, `Resume` | |
| 39 | +| `XxxResult` | 도메인 / 유즈케이스 함수 반환 | 비즈니스 로직 출력 (HTTP 비종속) | `AnalysisResult`, `ValidationResult` | |
| 40 | +| `XxxModel` | `features/*/ui/`, `widgets/*/ui/` | UI 표시 전용 가공 데이터 | `SessionListItemModel`, `ServiceCardModel` | |
| 41 | +| `XxxProps` | 컴포넌트 옆 | React props | `ButtonProps`, `HomeHeroProps` | |
| 42 | +| `XxxState` | store / state machine | **명시적** 상태 모델. 단발 `useState` shape 에 사용 ✗ | `AuthState`, `SessionMachineState` | |
| 43 | +| `XxxOptions` | 함수·훅 설정 | 옵셔널 인자 묶음 | `UseTypewriterOptions` | |
| 44 | +| `XxxId` | 어디든 | branded identifier | `type SessionId = number & { readonly __brand: 'SessionId' }` | |
| 45 | + |
| 46 | +--- |
| 47 | + |
| 48 | +## 3. 레이어별 타입 흐름 |
| 49 | + |
| 50 | +``` |
| 51 | + [Backend OpenAPI] |
| 52 | + │ openapi-typescript 자동 생성 |
| 53 | + ▼ |
| 54 | + shared/api/generated.ts ── XxxDto |
| 55 | + │ 매퍼 함수 (toUser, toSession) |
| 56 | + ▼ |
| 57 | + domain/{slice}/model/ ── Xxx (Entity) |
| 58 | + │ feature / widget 에서 UI 가공 |
| 59 | + ▼ |
| 60 | + features|widgets/*/ui/ ── XxxModel |
| 61 | + │ props 로 주입 |
| 62 | + ▼ |
| 63 | + React Component ── XxxProps |
| 64 | +``` |
| 65 | + |
| 66 | +**경계 규칙**: |
| 67 | + |
| 68 | +- `XxxDto` 는 `shared/api/` 밖으로 새지 않는다. UI · 도메인이 Dto 를 직접 import 하면 anti-corruption layer 가 무너진다. |
| 69 | +- `Xxx` (Entity) 는 `domain/` 이 소유. features / widgets 는 read-only 로 참조한다. |
| 70 | +- `XxxModel` 은 슬라이스 내부에 머문다. 다른 슬라이스로 export 하지 않는다 — UI 표시 형태가 누수되면 변경 비용이 폭증한다. |
| 71 | + |
| 72 | +--- |
| 73 | + |
| 74 | +## 4. `type` vs `interface` |
| 75 | + |
| 76 | +`type` 기본값. `interface` 는 두 경우에만: |
| 77 | + |
| 78 | +1. 클래스가 `implements` 할 때 (`ErrorBoundary` 등) |
| 79 | +2. declaration merging 필요 시 (외부 라이브러리 augmentation) |
| 80 | + |
| 81 | +```ts |
| 82 | +// 좋음 |
| 83 | +type SessionStatus = 'READY' | 'IN_PROGRESS' | 'COMPLETED'; |
| 84 | +type CreateSessionRequest = { userId: UserId; mode: SessionMode }; |
| 85 | +type AnalysisResult = { score: number; suggestions: string[] }; |
| 86 | + |
| 87 | +// 좋음 — implements 가 필요한 케이스 |
| 88 | +interface ErrorBoundaryProps { children: ReactNode; fallback: ReactNode } |
| 89 | +class ErrorBoundary extends Component<ErrorBoundaryProps, ErrorBoundaryState> { ... } |
| 90 | +``` |
| 91 | + |
| 92 | +--- |
| 93 | + |
| 94 | +## 5. 정의 위치 |
| 95 | + |
| 96 | +| 무엇 | 어디 | |
| 97 | +|---|---| |
| 98 | +| 컴포넌트 props (단일 사용) | `{Component}.tsx` 상단, **export 안 함** | |
| 99 | +| 외부에서도 import 되는 props | `{Component}.types.ts` | |
| 100 | +| 도메인 엔터티 | `domain/{slice}/model/types.ts` | |
| 101 | +| 슬라이스 내 `XxxModel` | `features/{slice}/model/types.ts` 또는 `ui/` 내부 | |
| 102 | +| API `XxxDto` | `shared/api/generated.ts` (OpenAPI 재생성으로만 갱신) | |
| 103 | + |
| 104 | +**전역 `src/types/` 폴더는 만들지 않는다.** 슬라이스 내부에서 시작하고, 진짜 공유될 때만 끌어올린다. |
| 105 | + |
| 106 | +--- |
| 107 | + |
| 108 | +## 6. shared 승격 임계 |
| 109 | + |
| 110 | +타입을 `shared/` 로 올리는 시점은 **서로 다른 슬라이스 3곳 이상에서 동일한 shape 이 반복될 때**. 그 전엔 슬라이스 안에 산다. |
| 111 | + |
| 112 | +> "언젠가 쓸지도 모르니 미리 shared 로" 는 항상 잘못된 추상화로 끝난다. |
| 113 | +
|
| 114 | +--- |
| 115 | + |
| 116 | +## 7. 안티패턴 |
| 117 | + |
| 118 | +- ❌ `XxxResponse` 를 함수 반환 타입으로 사용 — `XxxResult` 로. |
| 119 | +- ❌ UI 컴포넌트가 `SessionDto` 를 props 로 받음 — `SessionListItemModel` 로 매핑. |
| 120 | +- ❌ 로컬 `useState<{ ... }>` shape 에 `XxxState` 이름 붙이고 export. |
| 121 | +- ❌ `export type Props = { ... }` 같은 generic export — `{Component}Props` 로. |
| 122 | +- ❌ `src/types/` 또는 `src/shared/types/` 전역 폴더 (junk drawer 화). |
| 123 | +- ❌ `IXxx`, `TXxx` Hungarian prefix. |
| 124 | +- ❌ `Dto` 가 `features/` / `widgets/` / `domain/` 에서 import 되는 경우 — boundary 위반. |
0 commit comments