typescript와 axios를 쓰며 사실 타입 범위를 좁히지 않고 그동안 사용 했었다. (사실 기능적으로나, 컴파일 에러같은 문제들은 없었다). 다만 그렇게 사용하다 보니, 타입스크립트를 쓰는데 최대한 타입을 좁히는게 맞지 않나? 라고 생각하게 되었고 간단하지만 개선을 해보았다
다만 고민되는 점은 비동기 함수의 파라미터 타입과 return 타입을 어디에 정의 하느냐 하는게 고민이 되었다. @types 폴더 아래에 interface 파일들을 몰아 넣을 수도 있지만, 가급적 자주 사용하는 곳에 가깝게 두는게 직관적이라 생각하여 API 폴더 아래에 interface 폴더를 만들고 그 곳에 type들을 정의하였다.
먼저 서버로 부터 다음과 같은 JSON 데이터를 받는다고 가정 한다.
{
name: "홍길동",
role: "admin",
thumbnail: "https://~~~",
follower: 1,
id: 1,
}
그렇다면 type은 다음과 같이 나올 수 있을 것이다.
// api > interface > user.interface.ts
type User = {
name: string;
role: "admin" | "user",
thumbnail: string;
follower: number;
id: number;
};
그렇다면 이제 가장 간단한 get api 부터 만들면 다음과 같이 만들 수 있다.
(나의 정보를 불러오는 API 함수, token에 대한 헤더는 생략한다, try catch 또한 생략)
import type { User } from "./interface/user";
export const getUser = async (): Promise<User> => {
const result = await axios.get(`backendurl:port/getme`);
return result.data;
};
이러면 기능적으로는 작동하고 react-query같은 툴을 써도 타입 추론이 가능 할 것이다. 하지만 result의 타입을 보면 AxiosResponse<any,any>
와 같이 되어 있고 data
또한 any임을 확인할 수 있다.
이 점이 상당히 불편함으로 고쳐 보자
먼저 axios의 type 선언 파일을 보면 제너릭을 사용하여 타입을 추론함을 알 수 있다. 따라서 그에 맞게 수정하면 다음과 같다
import type { User } from "./interface/user";
export const getUser = async (): Promise<User> => {
const result = await axios.get<User>(`backendurl:port/getme`);
return result.data;
};
이러면 data에도 타입이 더 이상 any가 아닌 User가 됨을 알 수 있으며, 구조 분해 할당도 가능해진다.
하지만 위에서도 불편한 점이 있다면 User type을 Promise와 axios에 각각 두번 작성 해주어야 한다는 점이다. 따라서 이 점을 개선 하기 위해서 제네릭을 또 사용하였다.
export const getUser = async <T = User>(): Promise<T> => {
const { data } = await axios.get<T>(url);
return data;
};
post같은 경우 request body가 포함되는 경우가 많다. 이러한 request body에 대한 type을 받고 싶은 경우 axios에 정의되어 있는 AxiosResponse를 사용 해볼 수 있다.
export const postUser = async <T = User, R = UserPostType>(body: R): Promise<T> => {
const { data } = await axios.post<T, AxiosResponse<T>, R>(url, body, header);
}
위와 같은 방식으로 제너릭을 활용하여 보다 DX를 높일 수 있었다.