백엔드 API와 연동 작업을 하다 보면 가장 골치 아픈 순간은 응답 구조가 미묘하게 다를 때입니다. 성공 시에는 { data: T } 형태로 오다가, 실패 시에는 { error: string }으로 오거나, 특정 레거시 엔드포인트만 { result: T } 형태로 내려오는 경우가 허다합니다. 이때마다 any를 사용하거나 옵셔널 체이닝(?.)으로 도배하는 것은 프론트엔드 안정성을 심각하게 해치는 행위입니다. 오늘은 TypeScript의 제네릭과 조건부 타입을 활용해 이러한 불규칙성을 컴파일 단계에서 잡아내는 방법을 공유합니다.
API 응답 타입의 딜레마와 분석
최근 MSA(Microservices Architecture) 환경에서 진행한 프로젝트에서는 서비스마다 응답 래퍼(Wrapper)가 달라 타입을 일원화하기 어려웠습니다. 단순히 Union Type(Success | Error)을 사용하는 것만으로는 부족했습니다. 개발자가 응답 데이터를 꺼내 쓸 때마다 타입 가드(Type Guard)를 작성해야 했기 때문입니다.
이 문제를 해결하기 위해 우리는 TypeScript 고급 타입 중 하나인 Conditional Types(조건부 타입)를 도입해야 합니다. 이는 마치 엑셀의 if 함수처럼, 제네릭 T가 특정 형태를 만족하는지에 따라 타입을 동적으로 결정합니다. 여기에 infer 키워드를 조합하면, 런타임 데이터 검증 없이도 중첩된 제네릭 내부의 타입을 "추론"하여 뽑아낼 수 있습니다.
T extends U ? X : Y 형태를 가집니다. 즉, T가 U에 할당 가능하다면 X, 아니면 Y가 됩니다. 이 분기 처리가 API 타입 설계의 핵심입니다.
infer를 활용한 제네릭 언래핑(Unwrapping) 솔루션
가장 강력한 타입스크립트 팁은 infer를 사용하여 이중, 삼중으로 감싸진 응답 객체에서 우리가 진짜 필요한 Data 타입만 추출하는 것입니다. 아래 코드는 다양한 형태의 API 응답에서 실제 데이터 타입을 안전하게 추출하는 유틸리티 타입입니다.
// 1. 백엔드에서 내려오는 다양한 응답 형태 정의
type SuccessResponse<T> = { status: 'success'; data: T; timestamp: number };
type ErrorResponse = { status: 'error'; message: string; code: number };
type LegacyResponse<T> = { result: T; meta: any };
// 2. 통합 응답 타입
type ApiResponse<T> = SuccessResponse<T> | ErrorResponse | LegacyResponse<T>;
// 3. [핵심] 조건부 타입과 infer를 이용한 데이터 추출기
// T가 SuccessResponse 형태라면 내부의 U(실제 데이터)를 반환하고,
// LegacyResponse라면 내부의 U를 반환, 그 외(Error 등)는 never 또는 null 처리
type UnwrapAPI<T> = T extends SuccessResponse<infer U>
? U
: T extends LegacyResponse<infer U>
? U
: never; // 에러 타입이거나 매칭되지 않으면 접근 불가 처리
// 사용 예시: 사용자 정보 인터페이스
interface UserProfile {
id: number;
name: string;
}
// 실제 API 호출 시뮬레이션
// 응답이 SuccessResponse<UserProfile> 형태로 온다고 가정
type MyApiResponse = SuccessResponse<UserProfile>;
// 마법처럼 UserProfile 타입만 쏙 뽑아냅니다.
type RealData = UnwrapAPI<MyApiResponse>;
// 결과: RealData는 UserProfile 타입이 됨.
// 검증 함수
function handleData(data: RealData) {
console.log(data.name); // 자동완성 지원, 타입 안전성 보장
}
위 패턴을 사용하면, 비즈니스 로직에서는 API가 data 프로퍼티에 있는지 result 프로퍼티에 있는지 신경 쓸 필요가 없습니다. UnwrapAPI 유틸리티 타입이 컴파일 타임에 정확한 페이로드 타입을 찾아주기 때문입니다.
infer는 제네릭 제약조건(extends) 내부에서만 사용 가능합니다. 무분별한 사용은 TS 컴파일러 성능에 영향을 줄 수 있으므로, 공통 유틸리티 라이브러리 레벨에서만 제한적으로 사용하는 것이 좋습니다.
| 접근 방식 | 타입 안전성 | 유지보수 용이성 | 특징 |
|---|---|---|---|
| any 사용 | 최하 | 최하 | 런타임 에러 발생 가능성 매우 높음 |
| 단순 Generic | 중 | 중 | 응답 구조가 변경될 때마다 인터페이스 수정 필요 |
| Conditional Types | 최상 | 최상 | 다양한 응답 구조를 하나의 로직으로 추상화 가능 |
Conclusion
TypeScript의 Conditional Types와 infer 키워드는 단순히 복잡한 코드를 자랑하기 위한 것이 아닙니다. 불확실한 외부 시스템(API)과의 경계에서 우리 애플리케이션을 보호하는 강력한 방패입니다. 오늘 소개한 패턴을 프로젝트의 공통 타입 정의 파일(d.ts)에 적용해 보세요. 프론트엔드 안정성이 획기적으로 개선되고, 동료 개발자들은 더 이상 API 응답 구조를 찾아 헤매지 않아도 될 것입니다.
Post a Comment