서버 DTO와 화면 ViewModel 사이에 도메인 boundary를 둡니다.
OpenAPI spec
-> Orval generated code (`src/lib/api`)
-> domain API wrapper (`src/domains/<domain>/api/client.ts`)
-> mapper (`src/domains/<domain>/api/mapper.ts`)
-> ViewModel (`src/domains/<domain>/model.ts`)
-> screen / component propssrc/lib/api/endpoints/**와src/lib/api/model/**은 직접 수정하지 않습니다.- OpenAPI 스펙 변경 시
npm run api:generate로 재생성합니다. - 생성 타입을 화면 props나 UI state에 직접 사용하지 않습니다.
import { getScheduleDetail } from '@/lib/api/endpoints/schedule-crud/schedule-crud';
import { toScheduleDetail } from '@/domains/schedule/api/mapper';
import type { ScheduleDetail } from '@/domains/schedule/model';
export async function fetchScheduleDetailViewModel(id: number): Promise<ScheduleDetail> {
const response = await getScheduleDetail(id);
return toScheduleDetail(response);
}- unwrap, null 보정, enum/label 매핑은 domain API 또는 mapper에서 처리합니다.
- 화면은 ViewModel과 domain 함수만 사용합니다.
- DTO 필드명이 바뀌어도 화면 변경이 최소화되도록 mapper 경계를 유지합니다.
API wrapper와 ViewModel mapper는 src/domains/<domain>/api에 둡니다. 실제 책임이 있는 파일만 만들고, query/mutation hook이 아직 화면에 붙지 않은 경우 억지로 빈 hook 파일을 만들지 않습니다.
src/domains/<domain>/api/
├── client.ts
├── mapper.ts
├── query-keys.ts
├── queries.ts
└── mutations.tsqueries.ts는 TanStack Queryuse*Queryhook을 둡니다.mutations.ts는 TanStack Queryuse*Mutationhook을 둡니다.- query key factory는
query-keys.ts로 분리합니다. - generated API/DTO는 이 경계 밖으로 직접 퍼뜨리지 않습니다.
- mock/store 기반 화면을 서버 API로 전환하는 작업은 API boundary 정리와 별도 PR로 분리할 수 있습니다.