타입스크립트 환경의 프론트엔드에서는 서버의 응답값
을 기반으로 타입을 만들어서 사용하게 됩니다. 예를 들어, 위와 같은 API 응답값이 내려온다고 할 때 다음과 같이 응답값 interface를 만들어서 사용합니다.
interface FooResponse {
statusCode: number;
payload: {
id: number;
email: string;
}
}
interface BarResponse {
statusCode: number;
payload: {
id: number;
role: number;
}
}
FooResponse
와 BarResponse
를 보면 알다시피 대부분의 API들은 다음과 같이 비슷한 형태의 응답값을 반환 합니다. 때문에 response interface를 매 번 새로 만들기 보다는 중복되는 interface를 미리 정의하고 사용하는 편이 좋다고 생각했습니다.
interface ServerResponse<T> {
statusCode: number;
payload: T;
}
interface FooResponse {
id: number;
email: string;
...
}
interface BarResponse {
id: number;
role: number;
...
}
/* const fooResponse: Promise<AxiosResponse<ServerResponse<FooResponse>, any>> */
const fooResponse = axios.get<ServerResponse<FooResponse>>('...');
/* /* const barResponse: Promise<AxiosResponse<ServerResponse<BarResponse>, any>> */
const barResponse = axios.get<ServerResponse<BarResponse>>('...');
FooResponse
와 BarResponse
는 매번 statusCode
와 payload
를 작성하지 않아도 ServerResponse
와 제네릭
을 활용하여 타입을 재활용 할 수 있게 되었습니다.ServerResponse
를 작성하는 건 귀찮은 일이기 때문에, axios를 그대로 사용하는 것이 아닌 axios를 래핑한 Axios 인스턴스
를 만들어서 기본 타입을 ServerResponse
로 정의해봅시다.AxiosStatic
이며, AxiosInstance
를 상속 받습니다.AxiosStatic
에는 Axios 인스턴스를 만들 때 사용되는, create
메서드에 대한 타입도 보입니다./*
https://github.com/axios/axios/blob/v1.x/index.d.ts
line 527~543
*/
export interface AxiosStatic extends AxiosInstance {
✅ create(config?: CreateAxiosDefaults): AxiosInstance;
Cancel: CancelStatic;
CancelToken: CancelTokenStatic;
Axios: typeof Axios;
AxiosError: typeof AxiosError;
HttpStatusCode: typeof HttpStatusCode;
readonly VERSION: string;
isCancel: typeof isCancel;
all: typeof all;
spread: typeof spread;
isAxiosError: typeof isAxiosError;
toFormData: typeof toFormData;
formToJSON: typeof formToJSON;
CanceledError: typeof CanceledError;
AxiosHeaders: typeof AxiosHeaders;
}
AxiosInstance
타입은 다음과 같으며, Axios
타입을 상속 받습니다./*
https://github.com/axios/axios/blob/v1.x/index.d.ts
line 494~503
*/
export interface AxiosInstance extends Axios {
<T = any, R = AxiosResponse<T>, D = any>(config: AxiosRequestConfig<D>): Promise<R>;
<T = any, R = AxiosResponse<T>, D = any>(url: string, config?: AxiosRequestConfig<D>): Promise<R>;
defaults: Omit<AxiosDefaults, 'headers'> & {
headers: HeadersDefaults & {
[key: string]: AxiosHeaderValue
}
};
}
Axios
에는 자주 사용되는 get
, post
등의 타입이 정의되어 있습니다./*
https://github.com/axios/axios/blob/v1.x/index.d.ts
line 473~492
*/
export class Axios {
constructor(config?: AxiosRequestConfig);
defaults: AxiosDefaults;
interceptors: {
request: AxiosInterceptorManager<InternalAxiosRequestConfig>;
response: AxiosInterceptorManager<AxiosResponse>;
};
getUri(config?: AxiosRequestConfig): string;
request<T = any, R = AxiosResponse<T>, D = any>(config: AxiosRequestConfig<D>): Promise<R>;
✅ get<T = any, R = AxiosResponse<T>, D = any>(url: string, config?: AxiosRequestConfig<D>): Promise<R>;
delete<T = any, R = AxiosResponse<T>, D = any>(url: string, config?: AxiosRequestConfig<D>): Promise<R>;
head<T = any, R = AxiosResponse<T>, D = any>(url: string, config?: AxiosRequestConfig<D>): Promise<R>;
options<T = any, R = AxiosResponse<T>, D = any>(url: string, config?: AxiosRequestConfig<D>): Promise<R>;
✅ post<T = any, R = AxiosResponse<T>, D = any>(url: string, data?: D, config?: AxiosRequestConfig<D>): Promise<R>;
put<T = any, R = AxiosResponse<T>, D = any>(url: string, data?: D, config?: AxiosRequestConfig<D>): Promise<R>;
patch<T = any, R = AxiosResponse<T>, D = any>(url: string, data?: D, config?: AxiosRequestConfig<D>): Promise<R>;
postForm<T = any, R = AxiosResponse<T>, D = any>(url: string, data?: D, config?: AxiosRequestConfig<D>): Promise<R>;
putForm<T = any, R = AxiosResponse<T>, D = any>(url: string, data?: D, config?: AxiosRequestConfig<D>): Promise<R>;
patchForm<T = any, R = AxiosResponse<T>, D = any>(url: string, data?: D, config?: AxiosRequestConfig<D>): Promise<R>;
}
get
, post
등의 메서드들은 3가지 제네릭 타입(T, R, D)을 매개변수로 받는데 그 중 get이 return 하는 Promise<R>
의 R의 타입은 AxiosResponse<T>
입니다./*
https://github.com/axios/axios/blob/v1.x/index.d.ts
line 381~388
*/
export interface AxiosResponse<T = any, D = any> {
data: T;
status: number;
statusText: string;
headers: RawAxiosResponseHeaders | AxiosResponseHeaders;
config: InternalAxiosRequestConfig<D>;
request?: any;
}
AxiosResponse
는 Axios의 HTTP 응답 객체인데, 기본적인 HTTP 응답 값들을 가지고 있습니다.AxiosResponse
는 2가지 제네릭 타입을 매개변수로 받는데 그 중 T
는 데이터입니다. HTTP 통신의 응답 데이터에 반환되는 유형에 따라 다른 타입을 선언할 수 있게 제네릭으로 되어있습니다.interface FooResponse {
id: number;
name: string;
...
}
/* const response: Promise<AxiosResponse<FooResponse, any>> */
const response = axios.get<FooResponse>('...')
앞서 언급했듯 서버의 응답 값은 매번 다를 수 있지만, 대부분 항상 포함되어 있는 값들이 있습니다.
interface ServerResponse<T> {
/* 항상 응답값 객체에 포함되어 있는 값들 */
statusCode: number
message: string
...
payload: T
}
응답값이 가변적이기 때문에 타입을 미리 정의할 수 없는 값인 payload
만 제네릭으로 만들고, 공통적으로 넘어오는 값을 AxiosResponse
제네릭 매개변수에 넘기게 되면 매번 공통값에 대한 타입을 선언하지 않아도 됩니다.
const api = {
/* get: <T>(url: string, config?: AxiosRequestConfig) => Promise<AxiosResponse<ServerResponse<T>, any>> */
get: <T>(url: string, config?: AxiosRequestConfig) =>
axios.get<ServerResponse<T>>(url, config)
};
/* const response: Promise<AxiosResponse<ServerResponse<T>, any>> */
const response = api.get();
새로 만든 api의 get 메서드는
Promise<AxiosResponse<ServerResponse<T>, any>>
를 return 하게 되며ServerResponse
를 기본 타입으로 가지게 되었습니다.
❌ /* Before 1 */
interface FooResponse {
statusCode: number;
payload: {
id: number;
email: string;
}
}
interface BarResponse {
statusCode: number;
payload: {
id: number;
role: number;
}
}
------------------------------------------------------------------------------
🤔 /* Before 2 */
interface ServerResponse<T> {
statusCode: number;
payload: T;
}
interface FooResponse {
id: number;
email: string;
...
}
interface BarResponse {
id: number;
role: number;
...
}
/* const fooResponse: Promise<AxiosResponse<ServerResponse<FooResponse>, any>> */
const fooResponse = axios.get<ServerResponse<FooResponse>>('...');
/* const barResponse: Promise<AxiosResponse<ServerResponse<BarResponse>, any>> */
const barResponse = axios.get<ServerResponse<BarResponse>>('...');
------------------------------------------------------------------------------
✅ /* After */
interface ServerResponse<T> {
statusCode: number;
payload: T;
}
const api = {
/* get: <T>(url: string, config?: AxiosRequestConfig) => Promise<AxiosResponse<ServerResponse<T>, any>> */
get: <T>(url: string, config?: AxiosRequestConfig) => {
return axios.get<ServerResponse<T>>(url, config);
},
};
/* const fooResponse: Promise<AxiosResponse<ServerResponse<FooResponse>, any>> */
const fooResponse = api.get<FooResponse>();
/* const barResponse: Promise<AxiosResponse<ServerResponse<BarResponse>, any>> */
const barResponse = api.get<BarResponse>();
Axios 인스턴스 매서드의 제네릭값을 어떻게 주어야 할 지 고민하던 찰나에 많은 도움이 되었습니다. 감사합니다!