API 응답 타입을 런타임에 검증하기

문제 상황

프로젝트에 TypeScript를 도입한 지 6개월 정도 지났다. 내부 로직의 타입 안정성은 확보됐지만, 외부 API 응답에서 예상치 못한 런타임 에러가 계속 발생했다.

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

const response = await fetch('/api/user');
const user: UserResponse = await response.json();
// 실제로는 email 필드가 없을 수 있음

TypeScript의 타입 체크는 컴파일 타임에만 동작하기 때문에, 실제 API가 다른 형태의 데이터를 반환해도 런타임에서는 알 수 없다.

io-ts 도입

io-ts 라이브러리를 사용해 런타임 타입 검증을 추가했다. fp-ts 스타일이 처음엔 낯설었지만, 타입과 검증 로직을 하나로 관리할 수 있다는 점이 마음에 들었다.

import * as t from 'io-ts';
import { isRight } from 'fp-ts/Either';

const UserResponseCodec = t.type({
  id: t.number,
  name: t.string,
  email: t.string
});

type UserResponse = t.TypeOf<typeof UserResponseCodec>;

const response = await fetch('/api/user');
const data = await response.json();
const result = UserResponseCodec.decode(data);

if (isRight(result)) {
  const user = result.right;
  // 타입이 보장됨
} else {
  // 에러 처리
  console.error('Invalid response format');
}

실제 적용

API 클라이언트 레이어에 공통 유틸리티를 만들어 적용했다.

async function fetchWithValidation<A>(
  url: string,
  codec: t.Type<A>
): Promise<A> {
  const response = await fetch(url);
  const data = await response.json();
  const result = codec.decode(data);
  
  if (isRight(result)) {
    return result.right;
  }
  throw new Error('Validation failed');
}

운영 환경에 배포 후 실제로 API 스펙 불일치를 몇 건 잡아냈다. 백엔드 팀과 협의해 API 문서를 업데이트하고, 프론트엔드에서는 옵셔널 처리를 추가했다.

트레이드오프

번들 사이즈가 약 30KB 증가했고, 검증 로직으로 인한 오버헤드가 있다. 하지만 프로덕션 에러가 눈에 띄게 줄어든 것을 보면 충분히 가치 있는 투자였다고 생각한다.

API 응답 타입을 런타임에 검증하기