[TYPESCRIPT] 타입 안전한 API 응답 처리하기 - 런타임 오류를 컴파일 단계에서 차단하는 설계

 

프론트엔드·백엔드 개발에서 API 응답 처리는 가장 빈번하면서도 가장 많은 버그가 발생하는 지점 중 하나입니다. 특히 TypeScript를 사용하면서도 API 응답을 단순히 any나 무분별한 타입 단언(as)으로 처리하면, TypeScript의 장점을 절반도 활용하지 못하는 상태가 됩니다.

 

 

API 응답에서 가장 흔한 문제

다음과 같은 코드는 매우 흔하지만 위험합니다.

const res = await fetch("/api/user");
const data = await res.json();

console.log(data.user.name);
  • 응답 구조가 바뀌어도 컴파일 에러 없음
  • 필드 누락 시 런타임 오류 발생
  • 실제 데이터와 타입 간 불일치 감지 불가

이 문제의 핵심 원인은 단 하나입니다. API 응답이 타입으로 모델링되지 않았기 때문입니다.

 

API 응답은 반드시 타입으로 정의해야 한다

가장 기본적인 원칙은 “API 응답 하나당 타입 하나”입니다.

type User = {
  id: number;
  name: string;
};

type UserResponse = {
  user: User;
};

이제 응답 데이터는 다음과 같이 다룰 수 있습니다.

const data: UserResponse = await res.json();
console.log(data.user.name);

하지만 이 방식에는 아직 중요한 문제가 남아 있습니다.

 

성공/실패를 고려하지 않은 응답 모델의 한계

실제 API 응답은 대부분 다음 두 가지 케이스를 가집니다.

  • 성공 응답
  • 실패(에러) 응답

이를 하나의 타입으로 표현하면 안전하지 않습니다.

type ApiResponse = {
  data?: User;
  error?: string;
};

이 경우 매번 if 체크가 필요하고, 타입 안정성도 떨어집니다.

 

Discriminated Union으로 응답 모델링하기

실무에서 가장 권장되는 패턴은 Discriminated Union 기반의 API 응답 타입입니다.

type ApiSuccess<T> = {
  ok: true;
  data: T;
};

type ApiFailure = {
  ok: false;
  errorCode: string;
  message: string;
};

type ApiResponse<T> = ApiSuccess<T> | ApiFailure;

이제 응답은 반드시 성공 또는 실패 중 하나입니다.

 

타입 안전한 응답 처리

const response: ApiResponse<User> = await res.json();

if (response.ok) {
  response.data.name; // User 타입으로 안전
} else {
  console.error(response.message); // 실패 타입
}

이 구조의 핵심 장점은 다음과 같습니다.

  • ok 값 하나로 타입이 자동 분기됨
  • 잘못된 필드 접근이 컴파일 단계에서 차단
  • 에러 케이스 누락 방지

 

fetch 헬퍼 함수로 공통화하기

실무에서는 API 호출을 공통 함수로 감싸는 경우가 많습니다.

async function fetchJson<T>(
  input: RequestInfo,
  init?: RequestInit
): Promise<ApiResponse<T>> {
  const res = await fetch(input, init);
  return res.json();
}
const userRes = await fetchJson<User>("/api/user");

if (userRes.ok) {
  userRes.data.id;
}

이 패턴은 다음과 같은 장점을 가집니다.

  • API 응답 처리 방식 통일
  • 중복 코드 제거
  • 타입 안전성 전파

 

JSON 파싱 시 unknown부터 시작하기

API 응답은 외부 입력이므로 엄밀하게 말하면 unknown부터 시작하는 것이 가장 안전합니다.

const raw: unknown = await res.json();

그리고 런타임 검증을 통해 타입으로 승격시킵니다.

대규모 서비스나 외부 API 연동 시에는 이 방식이 특히 중요합니다.

 

실무에서 자주 쓰는 응답 패턴 정리

  • 모든 API 응답은 제네릭 ApiResponse<T> 사용
  • 성공/실패는 Discriminated Union으로 분리
  • optional 필드로 성공/실패 표현하지 않기
  • 공통 fetch 헬퍼로 타입 전파
  • 외부 API는 unknown + 검증 패턴 고려

이 원칙을 지키면 API 관련 버그의 상당수를 컴파일 단계에서 제거할 수 있습니다.

 

프론트엔드·백엔드 공통 타입 전략

가능하다면 API 응답 타입은 프론트엔드와 백엔드가 공유하는 것이 가장 이상적입니다.

  • OpenAPI 기반 타입 생성
  • 공통 패키지로 타입 분리
  • DTO와 API Response 타입 분리

이렇게 하면 API 변경 시 컴파일 에러로 즉시 감지할 수 있습니다.

 


 

타입 안전한 API 응답 처리는 “응답을 신뢰하지 않고, 타입으로 통제하는 설계”입니다. Discriminated Union과 제네릭을 활용하면 API 응답은 더 이상 불안정한 JSON이 아니라 신뢰할 수 있는 계약(Contract)이 됩니다.

  • API 응답은 반드시 타입으로 모델링
  • 성공/실패는 명확히 분리
  • 타입은 공통화, 처리는 일관되게