← 전체 글로 돌아가기

TypeScript

TypeScript 타입 에러는 한 곳이 아니다

API 응답 타입이 맞지 않아서 터지는 TypeScript 에러. 그 원인이 항상 명백한 곳에 있지는 않다.

에러 메시지를 먼저 읽는다

TypeScript 컴파일 에러는 대개 정확하지만, 그 메시지 자체가 원인을 가리키지 않을 수도 있다.

npm run build 2>&1 | grep -A3 "error TS"

"Property 'email' does not exist" 같은 에러가 나면, 문제는 property가 아니라 타입 정의 자체일 수 있다.

API 응답이 실제로 뭔지 본다

가장 정확한 방법은 API를 직접 호출해서 응답을 보는 것이다.

# 단순 요청
curl -i 'https://api.example.com/items?page=1'

# 인증이 필요한 경우
curl -i -H "Authorization: Bearer token" \
  'https://api.example.com/items'

JSON 구조를 보고, 타입 정의와 비교한다. nullable 필드가 실제로 null을 반환하는가. 배열이 항상 배열인가, 에러 상황에서 null이 될 수 있는가.

타입 정의 파일을 점검한다

# 타입 정의 위치 확인
find . -name "*.ts" -type f | xargs grep -l "interface.*Item"

문제는 대개 다음 중 하나다.

  1. 인터페이스가 과거 API 스펙을 따르고 있다
  2. optional 필드가 required로 정의되어 있다
  3. 같은 이름의 타입이 여러 곳에 정의되어 있다

HTTP 상태 코드 처리

성공 응답 (200)과 에러 응답 (400, 500)의 타입을 구분해야 한다.

type ApiResponse<T> =
  | { status: 'success'; data: T }
  | { status: 'error'; message: string; code: number };

이렇게 정의하면 응답을 처리할 때 상태에 따라 타입이 자동으로 좁혀진다.

제네릭 타입의 전파 확인

API 응답이 제네릭을 사용한다면, 타입 인수가 제대로 전달되는지 확인한다.

// 정의
function fetchItems<T>(url: string): Promise<T[]> { ... }

// 호출할 때 명시적으로
const items: Item[] = await fetchItems<Item>(url);

타입 인수를 생략하면 TypeScript가 any로 추론할 수 있고, 그러면 타입 체크 효과가 사라진다.

런타임에도 검증한다

TypeScript 컴파일은 성공해도 런타임에 타입이 달라질 수 있다. 특히 외부 API를 받을 때는 더 그렇다.

// 간단한 런타임 타입 가드
function isItem(obj: unknown): obj is Item {
  return typeof obj === 'object' &&
    obj !== null &&
    'id' in obj &&
    'title' in obj &&
    typeof obj.id === 'number' &&
    typeof obj.title === 'string';
}

이런 검증 함수를 API 응답 처리 직후에 실행하면, 예상치 못한 데이터 구조 변화를 초기에 잡을 수 있다.

최종 확인

타입 에러를 고친 후에는:

  1. 실제 API 응답과 타입 정의가 일치하는가
  2. 에러 상황 (네트워크 오류, 인증 실패)에도 타입이 유효한가
  3. 배포 후 다른 클라이언트에서도 같은 응답을 받는가

이 세 가지가 확인되면 안전하다.