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

문제 상황

프로젝트에서 사용자 정보를 다루는 API가 여러 개 있었는데, 각 API마다 미묘하게 다른 응답 구조를 가지고 있었다. 처음엔 각각 인터페이스를 따로 정의했는데, 필드가 추가되거나 변경될 때마다 여러 곳을 수정해야 하는 문제가 발생했다.

해결 방법

기본 타입을 정의하고 TypeScript 유틸리티 타입으로 필요한 형태를 파생시키는 방식으로 변경했다.

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

// 로그인 응답: 비밀번호 제외
type LoginResponse = Omit<User, 'password'>;

// 회원가입 요청: id, 날짜 필드 제외
type SignupRequest = Omit<User, 'id' | 'createdAt' | 'updatedAt'>;

// 프로필 수정: 일부 필드만 선택적으로
type UpdateProfileRequest = Partial<Pick<User, 'name' | 'email'>>;

더 복잡한 케이스에서는 조합도 가능했다.

// 목록 조회용 (민감 정보 제외)
type UserListItem = Pick<User, 'id' | 'name' | 'email'>;

// 관리자 전용 (전체 정보 + 추가 필드)
type AdminUser = User & {
  role: 'admin' | 'user';
  lastLoginAt: string | null;
};

결과

타입 정의 코드가 30% 정도 줄었고, User 인터페이스만 수정하면 관련된 모든 타입에 자동으로 반영되어 유지보수가 편해졌다. 특히 신규 입사자가 API 타입 구조를 파악하기도 쉬워졌다는 피드백을 받았다.

참고사항

ReturnType, Parameters 같은 유틸리티 타입도 함수 시그니처 재사용에 유용하다. 다음에는 이 부분도 정리해봐야겠다.

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