API Versioning (v1 / v2 / ...)
When a new API version replaces an older one, follow these layer-by-layer rules strictly.
Principle: each version module is a self-contained, independently exportable unit. The barrel index.ts decides which version consumers see. Legacy modules are not modified — they are simply excluded from the barrel.
1. API layer — folder split by version, barrel re-export (functions + types)
src/api/<domain>/
├── index.ts # barrel: re-exports functions AND types from active version
├── types.ts # shared types across versions (e.g. MenuGroup)
├── v1/
│ ├── index.ts # v1 module (intact, but not re-exported by barrel)
│ └── types.ts # v1 types
└── v2/
├── index.ts # v2 module (active, re-exported by barrel)
└── types.ts # v2 types
api/<domain>/index.ts re-exports functions and types from the active version:
import { getMenus } from './v2';
export type { MenuItem, MenuResponse } from './v2/types';
export const menuAPI = { getMenus };
Consumers import both functions and types from @/api/<domain> — no version path needed:
import { menuAPI } from '@/api/menu';
import type { MenuItem } from '@/api/menu';
Type names inside each version are clean (no version suffix): MenuItem, Role, not MenuItemV2, RoleV2. The folder path (v1/types.ts vs v2/types.ts) provides version context.
v1 modules keep their own export statements intact — they are valid, standalone modules. The barrel simply does not import them, so consumers never see them.
To deprecate v1: remove it from index.ts. To restore: add it back. The v1 code itself is never touched.
When version changes, update the barrel's import paths (./v1/ → ./v2/). Consumer imports stay the same.
2. Service layer — folder split by version, barrel re-export
src/services/<domain>/
├── index.ts # barrel: re-exports active version as `<domain>Service`
├── <domain>Service.v1.ts # v1 service (preserved for reference)
└── <domain>Service.v2.ts # v2 service (active)
index.ts imports from the active version and re-exports as the canonical service name:
import { menuServiceV2 } from './menuService.v2';
export const menuService = menuServiceV2;
Each version file exports its own named object (e.g. menuServiceV1, menuServiceV2).
Function names inside each version file are clean (no version suffix): getMenus, getRoles, etc.
Consumers import from @/services/<domain> and call menuService.getMenus() — no version awareness needed.
For domains without version split, a single <domain>Service.ts file is fine (no folder needed).
3. Utils — keep both versions, use version suffix
If a utility function has v1 and v2 variants (e.g. buildMenuSections / buildMenuSectionsV2), keep both in the same file with version suffix. Do NOT delete old versions — they may be needed for reference or rollback.
4. Hooks / Pages / Components — no version suffix, always use latest
Hooks and UI code do not version-manage. Apply the current version directly.
When v2 replaces v1, update the hook in-place (e.g. useMenuQuery calls v2 service, no V2 suffix).
Do NOT create separate versioned hook files (useMenuQueryV2.ts).
5. Query cache keys — prefer static constants over function factories
For parameter-less keys use static as const arrays. Use spread at call sites for parameterized keys:
export const ROLE_V2_CACHE_KEY = 'roleV2' as const;
export const ROLE_V2_LIST_KEY = [ROLE_V2_CACHE_KEY, 'list'] as const;
queryKey: ROLE_V2_LIST_KEY, // static
queryKey: [...ROLE_V2_USERS_KEY, userId], // parameterized
API 버전 관리 (v1 / v2 / ...)
새 API 버전이 기존 버전을 대체할 때, 아래 레이어별 규칙을 엄격히 따른다.
원칙: 각 버전 모듈은 독립적으로 export 가능한 자체 완결 단위다. barrel index.ts가 소비자에게 어떤 버전을 노출할지 결정한다. 레거시 모듈은 수정하지 않는다 — barrel에서 제외할 뿐이다.
1. API 레이어 — 버전별 폴더 분리, barrel에서 함수 + 타입 re-export
src/api/<domain>/
├── index.ts # barrel: 활성 버전의 함수와 타입만 re-export
├── types.ts # 버전 공통 타입 (예: MenuGroup)
├── v1/
│ ├── index.ts # v1 모듈 (그대로 유지, barrel에서 re-export하지 않음)
│ └── types.ts # v1 타입
└── v2/
├── index.ts # v2 모듈 (활성, barrel에서 re-export)
└── types.ts # v2 타입
api/<domain>/index.ts는 활성 버전의 함수와 타입을 re-export한다:
import { getMenus } from './v2';
export type { MenuItem, MenuResponse } from './v2/types';
export const menuAPI = { getMenus };
소비처는 @/api/<domain>에서 함수와 타입을 모두 import한다 — 버전 경로를 알 필요 없다:
import { menuAPI } from '@/api/menu';
import type { MenuItem } from '@/api/menu';
각 버전 내 타입명은 깔끔하게 (버전 suffix 없이): MenuItem, Role이지 MenuItemV2, RoleV2가 아니다. 폴더 경로(v1/types.ts vs v2/types.ts)가 버전 컨텍스트를 제공한다.
v1 모듈은 자체 export 구문을 그대로 유지한다 — 유효한 독립 모듈이다. barrel이 import하지 않을 뿐이므로 소비자에게 노출되지 않는다.
v1을 폐기하려면: index.ts에서 제거. 복원하려면: 다시 추가. v1 코드 자체는 절대 건드리지 않는다.
버전이 바뀌면 barrel의 import 경로만 변경(./v1/ → ./v2/). 소비처의 import는 그대로 유지된다.
2. 서비스 레이어 — 버전별 폴더 분리, barrel에서 re-export
src/services/<domain>/
├── index.ts # barrel: 활성 버전을 `<domain>Service`로 re-export
├── <domain>Service.v1.ts # v1 서비스 (참조용 보관)
└── <domain>Service.v2.ts # v2 서비스 (활성)
index.ts는 활성 버전에서 import하여 정규 서비스 이름으로 re-export한다:
import { menuServiceV2 } from './menuService.v2';
export const menuService = menuServiceV2;
각 버전 파일은 자체 이름의 객체를 export한다 (예: menuServiceV1, menuServiceV2).
각 버전 파일 내부의 함수명은 깔끔하게 (버전 suffix 없이): getMenus, getRoles 등.
소비처는 @/services/<domain>에서 import하고 menuService.getMenus()로 호출한다 — 버전을 인식할 필요 없다.
버전 분리가 없는 도메인은 단일 <domain>Service.ts 파일로 충분하다 (폴더 불필요).
3. 유틸 — 양쪽 버전 모두 보관, 함수명에 버전 suffix 사용
유틸 함수에 v1/v2 변형이 있으면 (예: buildMenuSections / buildMenuSectionsV2) 같은 파일에 버전 suffix를 붙여 보관한다. 이전 버전 삭제 금지 — 참조 또는 롤백에 필요할 수 있다.
4. 훅 / 페이지 / 컴포넌트 — 버전 suffix 없이 항상 최신 버전 사용
훅과 UI 코드는 버전 관리를 하지 않는다. 현재 버전을 바로 적용한다.
v2가 v1을 대체하면 훅을 그 자리에서 수정한다 (예: useMenuQuery가 v2 서비스를 호출, V2 suffix 안 붙임).
별도 버전 훅 파일(useMenuQueryV2.ts)을 만들지 않는다.
5. 쿼리 캐시 키 — 함수 팩토리보다 정적 상수 사용
파라미터 없는 키는 정적 as const 배열 사용. 파라미터가 있는 키는 호출처에서 spread:
export const ROLE_V2_CACHE_KEY = 'roleV2' as const;
export const ROLE_V2_LIST_KEY = [ROLE_V2_CACHE_KEY, 'list'] as const;
queryKey: ROLE_V2_LIST_KEY, // 정적
queryKey: [...ROLE_V2_USERS_KEY, userId], // 파라미터화