TypeScript 유틸리티 타입으로 API 응답 타입 안전하게 관리하기

문제 상황

프로젝트에서 REST API 응답 타입을 정의하던 중, 비슷한 구조의 타입을 반복해서 작성하는 일이 잦았다. 특히 상세 조회와 목록 조회에서 같은 엔티티를 다루지만 필드 일부만 다른 경우가 많았다.

interface UserDetail {
  id: number;
  email: string;
  name: string;
  profile: string;
  createdAt: string;
}

interface UserListItem {
  id: number;
  email: string;
  name: string;
}

백엔드에서 필드가 추가되거나 변경될 때마다 여러 타입을 수정해야 했고, 실수로 누락하는 경우도 있었다.

유틸리티 타입 활용

TypeScript 내장 유틸리티 타입을 사용해 기본 타입에서 파생시키는 방식으로 변경했다.

interface User {
  id: number;
  email: string;
  name: string;
  profile: string;
  createdAt: string;
  updatedAt: string;
}

type UserListItem = Pick<User, 'id' | 'email' | 'name'>;
type UserCreateRequest = Omit<User, 'id' | 'createdAt' | 'updatedAt'>;
type UserUpdateRequest = Partial<UserCreateRequest>;

Pick으로 필요한 필드만 선택하고, Omit으로 자동 생성 필드를 제외했다. Partial로 선택적 업데이트도 표현할 수 있었다.

커스텀 유틸리티 타입

페이지네이션 응답처럼 반복되는 구조는 제네릭 타입으로 추상화했다.

interface PaginatedResponse<T> {
  data: T[];
  total: number;
  page: number;
  pageSize: number;
}

type UserListResponse = PaginatedResponse<UserListItem>;
type PostListResponse = PaginatedResponse<PostListItem>;

결과

타입 정의가 단일 진실 공급원(Single Source of Truth)을 갖게 되면서 유지보수가 수월해졌다. API 스펙 변경 시 기본 타입만 수정하면 파생 타입들이 자동으로 반영됐다. 처음엔 복잡해 보였지만 익숙해지니 타입 안전성과 생산성 모두 개선됐다.

TypeScript 유틸리티 타입으로 API 응답 타입 안전하게 관리하기