TypeScript의 infer 키워드를 사용하여 API 응답에서 동적 타입 추론
Min-jun Kim
Dev Intern · Leapcell

소개
현대 웹 개발의 세계에서 API와의 상호 작용은 일상의 현실입니다. 요청을 보내고 응답을 받습니다. 데이터 자체도 중요하지만, 로컬 애플리케이션이 그 데이터의 정확한 모양을 이해하도록 보장하는 것은 견고하고 타입이 안전한 코드를 위해 똑같이 중요합니다. 모든 API 응답에 대해 인터페이스를 수동으로 정의하는 것은 특히 복잡하거나 진화하는 API를 다룰 때 빠르게 지루하고 오류가 발생하기 쉬운 작업이 될 수 있습니다. 이는 종종 개발자들이 취약하고 수동으로 타이핑된 정의를 유지하거나, 더 나쁘게는 any에 의존하여 타입 안전성을 완전히 포기하게 만듭니다.
하지만 TypeScript는 infer 키워드라는 강력한 기능을 제공하며, 이는 API 응답 유형을 처리하는 방식을 혁신할 수 있습니다. infer를 활용하면 API 함수의 서명에서 직접 반환 유형을 동적으로 추출하고 추론하여 더 탄력적이고 보일러플레이트가 적은 애플리케이션을 구축할 수 있습니다. 이 글에서는 API 응답에서 반환 유형을 동적으로 추론하는 일반적인 문제를 우아하게 해결하기 위해 infer를 실용적으로 응용하는 방법을 자세히 살펴보고, 개발자 경험과 코드 유지보수성을 크게 향상시킬 것입니다.
핵심 개념 및 원칙
실제 구현에 들어가기 전에, 솔루션의 기초가 되는 몇 가지 주요 TypeScript 개념을 간략하게 정의해 보겠습니다.
- 제네릭: 제네릭을 사용하면 타입 안전성을 유지하면서 다양한 타입으로 작동하는 유연하고 재사용 가능한 코드를 작성할 수 있습니다. 종종 꺾쇠 괄호로 표시됩니다.
Array<T>또는Promise<T>와 같습니다. - 조건부 타입: 이러한 타입은 조건을 기반으로 타입을 선택할 수 있게 합니다. 구문은 삼항 연산자와 유사합니다:
Condition ? TypeIfTrue : TypeIfFalse. infer키워드: 이것이 우리의 핵심입니다. 조건부 타입 내에서infer를 사용하면 다른 타입의 일부인 타입을 "캡처"한 다음 해당 캡처된 타입을 조건부 타입의 참 분기에 사용할 수 있습니다. 이는 타입에 대한 패턴 매칭을 위한 강력한 메커니즘입니다. 예를 들어,T extends Promise<infer U> ? U : T는Promise<U>에서 해결된 타입U를 추출합니다.ReturnType<T>유틸리티 타입: 이 기본 TypeScript 유틸리티 타입은 함수 타입T의 반환 유형을 추출합니다. 예를 들어,ReturnType<() => string>는string으로 확인될 것입니다. 유용하지만, 대부분의 API 호출이 반환하는 프로미스에는 직접적으로 도움이 되지 않습니다.Awaited<T>유틸리티 타입: TypeScript 4.5에서 도입된Awaited<T>는Promise<T>의 완료된 타입을 추출하거나 중첩된 프로미스를 재귀적으로 언래핑합니다. 이는 프로미스를 반환하는 API 호출에 특히 관련이 있습니다.
동적 타입 추론 실전
우리의 목표는 비동기 API 함수(일반적으로 Promise를 반환함)가 주어졌을 때 해당 프로미스를 효과적으로 "언래핑"하고 해결된 데이터의 타입을 제공할 수 있는 유틸리티 타입을 만드는 것입니다.
간단한 API 함수를 상상해 봅시다.
// api.ts interface User { id: number; name: string; email: string; } interface Product { productId: string; productName: string; price: number; } async function fetchUser(userId: number): Promise<User> { // Simulate API call return { id: userId, name: 'John Doe', email: 'john@example.com' }; } async function fetchProducts(): Promise<Product[]> { // Simulate API call return [{ productId: 'P1', productName: 'Laptop', price: 1200 }]; }
이제 infer를 사용하여 InferApiResponse 유틸리티 타입을 만들어 보겠습니다.
// utils.ts type InferApiResponse<T extends (...args: any[]) => Promise<any>> = T extends (...args: any[]) => Promise<infer R> ? R : never;
이 InferApiResponse 타입을 자세히 살펴보겠습니다.
T extends (...args: any[]) => Promise<any>: 이것은 제약 조건입니다.InferApiResponse에 전달된 타입T가 반드시Promise를 반환하는 함수여야 함을 보장합니다. 이는 우리가 특별히 비동기 API 함수를 대상으로 하기 때문에 중요합니다.T extends (...args: any[]) => Promise<infer R>: 마법이 일어나는 조건부 타입입니다.T(우리의 API 함수 타입)가Promise를 반환하는 함수 타입에 할당될 수 있는지 확인하고 있습니다.- 중요하게도,
infer R은 TypeScript에 "이 함수의 반환 유형이 Promise라면, Promise가 해결하는 타입을 추론하여 새 타입 변수R에 할당하십시오."라고 지시합니다.
? R : never:- 조건이 참이면(즉,
T가 Promise를 반환하는 함수이고R을 성공적으로 추론한 경우),InferApiResponse타입은R(Promise의 해결된 타입)으로 확인됩니다. - 조건이 거짓이면 (초기 제약 조건이 충족되면 발생하지 않아야 함),
never로 대체되어 불가능한 타입을 나타냅니다.
- 조건이 참이면(즉,
작동 방식을 살펴봅시다.
// app.ts import { fetchUser, fetchProducts } from './api'; import { InferApiResponse } from './utils'; // InferApiResponse가 정의된 곳으로 가정 type UserApiResponse = InferApiResponse<typeof fetchUser>; // UserApiResponse는 User로 올바르게 추론됩니다. type ProductsApiResponse = InferApiResponse<typeof fetchProducts>; // ProductsApiResponse는 Product[]로 올바르게 추론됩니다. // 사용 예시: async function displayUser(userId: number) { const user: UserApiResponse = await fetchUser(userId); console.log(user.name); // 타입 안전한 접근 // user.id, user.email 또한 올바른 타입으로 사용 가능합니다. } async function displayProducts() { const products: ProductsApiResponse = await fetchProducts(); console.log(products[0].productName); // 타입 안전한 접근 // products[0].productId, products[0].price 또한 사용 가능합니다. } displayUser(1); displayProducts();
보시다시피, UserApiResponse는 자동으로 User로 추론되고 ProductsApiResponse는 Product[]로 추론됩니다. 이는 애플리케이션의 다른 부분에서 이러한 인터페이스를 소비하기 위해 수동으로 다시 타이핑할 필요가 전혀 없습니다.
Awaited<ReturnType<typeof fetchUser>>는 왜 안 되나요?
간단한 경우에 대해 이것도 작동하고 아마도 더 간결할 수 있는 Awaited<ReturnType<typeof fetchUser>>를 사용하지 않는 이유는 무엇일까요?
type UserApiResponse2 = Awaited<ReturnType<typeof fetchUser>>; // User로 확인됨
Awaited<ReturnType<T>>는 이 특정 시나리오에 완벽하게 작동하지만, infer를 사용한 사용자 지정 InferApiResponse는 Awaited 또는 ReturnType만으로는 충분하지 않거나 함수 반환 유형보다 더 복잡한 구조에서 유형을 추출해야 하는 등 더 복잡한 유형 조작에 대해 infer를 사용하는 것이 더 근본적이고 시연합니다. 사용자 지정 InferApiResponse는 Promise를 반환하는 함수만 허용하도록 제약 조건을 적용하며, 이는 유용한 보호 조치가 될 수 있습니다.
infer 키워드는 유형에서 패턴을 일치시키고 해당 패턴의 특정 부분을 추출해야 하는 상황에서 탁월합니다. Awaited 및 ReturnType은 일반적인 패턴에 대한 특수 유틸리티 타입이며 (아마도 내부적으로 infer 사용) 유사한 개념을 사용하여 구축됩니다. infer를 이해하면 일반적인 패턴에 대한 자체 특수 유틸리티 타입을 구축할 수 있는 유연성을 제공합니다.
결론
TypeScript의 infer 키워드는 특히 API 응답을 다룰 때 동적 타입 추론을 위한 매우 강력한 도구입니다. 간단한 유틸리티 타입을 만듦으로써 비동기 API 함수가 반환하는 데이터의 정확한 구조를 자동으로 추론하여 보일러플레이트를 크게 줄이고 애플리케이션 전반에 걸쳐 타입 안전성을 크게 향상시킬 수 있습니다. 이 접근 방식은 개발을 간소화할 뿐만 아니라 API가 진화함에 따라 코드에 대한 더 큰 확신을 제공하여 JavaScript 프로젝트를 더 견고하고 유지보수 가능하게 만듭니다. infer를 수용하면 컴파일러가 타입 추론의 많은 작업을 수행하도록 하여 더 똑똑하고 안전하며 표현력이 풍부한 TypeScript 코드를 작성하는 데 도움이 됩니다.

