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 증가했고, 검증 로직으로 인한 오버헤드가 있다. 하지만 프로덕션 에러가 눈에 띄게 줄어든 것을 보면 충분히 가치 있는 투자였다고 생각한다.