axios를 TypeScript와 함께 사용할 때는 제네릭을 사용하여 응답의 타입을 지정할 수 있다.
이를 통해 TypeScript의 타입 검사 기능을 활용하여 API 응답의 구조를 보장받을 수 있는 것이다.
예를 들어, API에서 User 타입의 데이터를 반환한다고 가정하고
interface User {
id: number;
name: string;
email: string;
}
axios.get 요청을 할 때 제네릭을 사용하여 응답의 타입을
User로 지정할 수 있다
import axios from 'axios';
async function fetchUser(userId: number): Promise<User> {
const response = await axios.get<User>(`https://api.example.com/users/${userId}`);
return response.data;
}
axios.get는 응답의 데이터가 User 인터페이스와 일치하는지 확인 한다.
만약 서버에서 예상치 못한 형태의 데이터를 반환하면, TypeScript에서는 컴파일 타임 또는 개발 시에 에러를 뱉게 된다.
대부분의 경우 api요청 메소드를 곧바로 사용하지는 않고 커스텀 훅으로 묶어서 사용하는 경우가 많다.
나같은 경우 보통 아래의 방식으로 진행한다.
api요청 함수를 생성
기능별로 분류한 객체의 메소드 형태로 묶음
커스텀 훅에서 해당 메소드를 호출
이렇게 하나의 api를 호출하기 위한 깊이가 깊어지는 패턴의 경우
제네릭을 사용하고, 최상단에서 타입을 던져주면 커스텀 훅을
보다 범용적으로 사용할 수 있다.
실제 예시는 아래와 같다
api호출은 useQuery를 통해 진행한다
최상단 (api호출하는 곳)
// 최상단에서 타입을 선언후 제네릭으로 넘겨줌
useGetProblemDetail<GetProblemDetailType>(Number(problemId));
useGetProblemDetail은 리액트쿼리의 useQuery를 사용하는 커스텀 훅이다
useGetProblemDetail 내부의 useQuery에서 api 메소드를 호출하고 반환되는 프로미스 타입인 GetProblemDetailType타입을 제네릭으로 선언 해준다
useGetProblemDetail.ts
// 호출된 메소드에서 타입을 받아와야 하므로
// queryFn에서 사용할 제네릭 타입을 여기서 선언
export const useGetProblemDetail = <T, K = any>(
problemId: number | null,
options?: K,
): UseQueryResult<T> => {
return useQuery<T>({
queryKey: ['problemDetail', { problemId }],
queryFn: async (): Promise<T> => {
// getProblemDetail함수에도 GetProblemDetailType타입 전달
const res = await problemApi.getProblemDetail<T>(problemId!);
// GetProblemDetailType타입의 값을 반환해야 하니까
// .data 를 리턴해야 함
return res.data;
},
staleTime: Infinity,
retry: 0,
enabled: problemId !== null,
...options,
});
};
useQuery에 사용되는 options타입은 제네릭 K타입이 되는데
useGetProblemDetail호출할때 딱히 선언을 하지 않았으므로
any타입이 된다
T에는 상위에서 제네릭으로 넘겨받은 GetProblemDetailType 타입이 적용된다
제네릭으로 선언한 T타입은 get요청을 하는
getProblemDetail 메소드의 제네릭으로 넘겨줌으로써
GetProblemDetailType 타입을 또 한번 넘겨주는 것이다
queryFn의 반환 타입 역시 Promise타입이 된다
getProblemDetail.ts
// instance.ts
import axios, { AxiosError } from 'axios';
export const API = axios.create({});
import axios, { AxiosResponse } from 'axios';
import { API } from './instance';
**// useGetProblemDetail 메소드에서 넘겨받은**
**// GetProblemDetailType타입이 T로 들어감**
export const problemApi = {
getProblemDetail: async <T>(id: number): Promise<AxiosResponse<T>> => {
return await API.get(`/api/problem/detail/${id}`);
},
...
};
기능별 api메소드를 모아둔 객체 problemApi에 있는
getProblemDetail 메소드가 있다.
결국은 axios.get 요청을 할 때 제네릭을 사용하여 API.get<T>
형태로
응답의 타입을 지정하는 것인데, 이렇게 되는 원인은
API.get에서 T는 AxiosResponse 객체 내의 data 필드의
타입을 나타내기 때문이다
interface AxiosResponse<T = any> {
data: T;
status: number;
statusText: string;
headers: any;
config: AxiosRequestConfig;
request?: any;
}
useGetProblemDetail**<GetProblemDetailType>**(Number(problemId));
>>
const useGetProblemDetail = <**T**, K = any>(problemId: number | null, options?: K) => {
return useQuery**<T>**({ ... });
};
>>
const problemApi = {
getProblemDetail: async (id: number): **Promise<AxiosResponse<T>>** => {
return await API.get(`/api/problem/detail/${id}`);
**// 타입 전달 최종 목적지**
},
};