
하아.. 일주일마다 쓰려고 했는데 또 늦어버렸다.. 그래도 꾸준히 쓰려는 마음이 더 중요한거 아닐까? ㅎㅎㅎ.. 쨋든 오늘도 타입스크립트 관련해서 써보려고 한다.
처음엔 단순히 비동기 함수를 만들 때 Promise<any> 같은 식으로 다 썼다. 그냥 값이 나중에 오니까 그거 받고 .then() 붙이거나 await 쓰면 되겠지 싶었다. 근데 프로젝트 규모가 커질수록 이런 애매한 타입들이 점점 문제를 일으키기 시작했다.
이 함수가 뭘 반환하는지를 명확히 알 수 없으니, await로 받아도 타입 추론이 안되고, 결국 또 타입을 단언하거나 따로 주석으로 적는 일이 많아졌다. 이건 타입스크립트를 쓰는 이유랑 거리가 멀어 보였다.
그래서 알게 된 것이 제네릭을 활용한 Promise 타입 지정이다.
예를 들어서 아래와 같은 API 호출 함수가 있다고 해보자.
function fetchData(url: string): Promise<any> {
return fetch(url).then(res => res.join());
}
표면적으로는 문제가 없어 보인다. 하지만 실제로 await fetchData(...)를 한 그다음 줄 부터는 그 갑싱 어떤 구조인지 IDE도 모르고, 나도 확신할 수 없다. 타입스크립트를 쓰는 이유가 그 "확신"을 갖기 위함인데, 이 코드에는 그게 없다..!
그래서 아래처럼 바꿔본다.
function fetchData<T>(url: string): Promise<T> {
return fetch(url).then(res => res.join());
}
이제 이 함수는 내가 직접 예상하는 타입을 명시적으로 지정할 수 있게 된다.
type User = {
id: number;
name: string;
}
const getUser = async () => {
const user = await fetchData<User>('/api/user/1');
console.log(user.name); // <- 타입이 보장됨
}
진짜 중요한건 여기서 fetchData 함수가 User 타입을 반환한다는 약속을 함수 정의가 아니라 호출부에서 직접 명시했다는 점이다. 이건 엄청나게 큰 차이다. 호출하는 사람이 의도를 명확히 표현할 수 있고, 타입 추론까지 자연스럽게 따라오게 된다.
더 나아가면, 요즘 흔히 쓰는 API 구조처럼 응답이 다음과 같이 생겨 있을 수 있다.
{
"success" : true,
"data" : {
"id" : 1,
"name" : "Lee"
}
}
이런 경우에 일반화를 하고 싶다면?
type ApiResponse<T> = {
success: boolean;
data: T;
};
function fetchApi<T>(url:string): Promise<ApiResponse<T>> {
return fetch(url).then(res => res.json());
}
이렇게 하면 어떤 데이터든 data 안에 들어간다는 구조를 유지하면서도 T 를 통해 유연하게 타입을 바꿀 수 있다.
type Product = {
id: number;
price: number;
};
const getProduct = async () => {
const response = await fetchApi<Product>('/api/product/1');
if (response.success) {
console.log(response.data.price); // <- 타입에 안전하게 접근 가능.
}
}
이걸 써보면 느끼는게 있다.
제네릭은 단순히 타입을 뿌옇게 만드는 게 아니라, 타입을 더 정확하게 유지하면서도 확장성을 높여주는 방식이다.
정리하자면, 타입스크립트에서 Promise<T> 는 단순히 "나중에 올 거야"를 표현하는게 아니라,
"이 값이 어떤 타입인지 정확하게 예측할 수 있어"라는 확신을 유지한 채 비동기 흐름을 다루는 도구가 된다.
그리고 이걸 제네릭으로 만들면, 다음과 같은 이점이 생긴다
T 하나로 통일된 패턴 유지 가능처음엔 Promise<T>처럼 생긴 문법이 복잡하고 과하다고 느껴질 수 있다.
하지만 프로젝트를 계속 진행을 하다 보면, "이 함수가 어떤 데이터를 반환하지?" 하는 질문이 계속 머릿속에 맴돈다..
그럴 때마다 나는 Promsie<any> 대신 Promise<T> 를 고민해본다.
"이 데이터가 어떤 타입이어야 하고, 그게 함수 호출부에까지 드러나야 하지 않을까?" 라는 생각 말이다.
다음에는 타입 조작이나 조건부, 유틸리티 타입에 대해 알아보려고 한다
그러면 일주일동안 퇴근하고 공부를 해야겠지..ㅠㅠ 열심히 해보자!!