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"
문제는 대개 다음 중 하나다.
- 인터페이스가 과거 API 스펙을 따르고 있다
- optional 필드가 required로 정의되어 있다
- 같은 이름의 타입이 여러 곳에 정의되어 있다
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 응답 처리 직후에 실행하면, 예상치 못한 데이터 구조 변화를 초기에 잡을 수 있다.
최종 확인
타입 에러를 고친 후에는:
- 실제 API 응답과 타입 정의가 일치하는가
- 에러 상황 (네트워크 오류, 인증 실패)에도 타입이 유효한가
- 배포 후 다른 클라이언트에서도 같은 응답을 받는가
이 세 가지가 확인되면 안전하다.