TypeScript 4.2 템플릿 리터럴 타입으로 API 경로 타입 안전하게 관리하기
문제 상황
회사 프로젝트에서 API 경로를 문자열로 하드코딩하다 보니 오타로 인한 404 에러가 자주 발생했다. /api/users/${id}를 /api/user/${id}로 잘못 쓰는 경우가 대표적이었다.
// 기존 방식
const getUserUrl = (id: string) => `/api/users/${id}`;
const endpoint = getUserUrl('123'); // 오타 가능성
TypeScript 4.2 템플릿 리터럴 타입
지난 2월 말에 릴리즈된 TypeScript 4.2에서 템플릿 리터럴 타입이 추가되었다. 문자열 리터럴 타입을 조합해 새로운 타입을 만들 수 있다.
type APIResource = 'users' | 'posts' | 'comments';
type APIVersion = 'v1' | 'v2';
type APIPath = `/api/${APIVersion}/${APIResource}`;
// 자동완성되는 경로들
// '/api/v1/users' | '/api/v1/posts' | ... (총 6개 조합)
실제 적용
프로젝트에 적용해본 패턴이다.
type RESTMethod = 'GET' | 'POST' | 'PUT' | 'DELETE';
type Endpoint = `/api/users` | `/api/users/${string}` | `/api/posts`;
interface APIRequest {
method: RESTMethod;
endpoint: Endpoint;
}
const fetchAPI = ({ method, endpoint }: APIRequest) => {
return fetch(endpoint, { method });
};
// 타입 안전한 호출
fetchAPI({ method: 'GET', endpoint: '/api/users' }); // OK
fetchAPI({ method: 'GET', endpoint: '/api/user' }); // 컴파일 에러
한계와 대안
동적 경로 파라미터 처리는 여전히 까다롭다. /api/users/${string}으로 표현할 수 있지만 타입 추론이 약하다. 실무에서는 주요 엔드포인트만 리터럴로 정의하고, 동적 부분은 헬퍼 함수로 분리했다.
const createUserEndpoint = (id: string): `/api/users/${string}` => {
return `/api/users/${id}`;
};
팀 내 리뷰에서는 호평이었다. 단순하지만 실수를 줄이는 데 확실히 효과가 있었다. 다음 스프린트부터 모든 API 클라이언트 코드에 적용하기로 했다.