Axios 에는 interceptors 라는 기능이 있다.
이를 통해서 request / response 에 선행,후행 처리를 커스텀하게 할 수 있다.
const client = Axios.create({ baseURL: 'https://my-url.com/v1' });
client.interceptors.response.use(res => {
return res.data;
})
client.interceptors.request.use(req => {
req.headers.authorization = TokenManager.accessToken;
return req;
})
// 원래대로라면, response.data 에 user 가 들어있어야 하지만
// interceptor 를 통해서 response.data 를 건너뛰었다.
const user = await client.get('/users/my-uid');
그취만.. 타입 스크립트에서는 interceptor 를 통해서 한번 포맷팅을 변경했지만
client.get
의 타입은 그대로 response.data 형태로 남아있는걸 겪을 수 있다.
// user 의 타입은 AxiosResponse 로 감싸져서, 실제로는 user 객체를 받아오지만
// 타입상으로는 { data: { name: string; age: number; } } 과 같은 형태로 나온다.
const user = await client.get<{ name:string; age:number; }>('/users/my-uid');
https://github.com/axios/axios/issues/1510 이슈를 통해서 활발한 논의가 진행됐었고
https://github.com/axios/axios/pull/1605 를 통해서 커스텀한 타입을 추가할 수 있게 수정되었다. (뒤쪽 제네릭의 기본값을 AxiosResponse 로 감싼 형태로 처리하고, 해당 제네릭을 return 값으로 처리했다.)
const user = await client.get<any,{ name:string; age:number; }>('/users/my-uid');
흠.. 계속해서 사용하기에 적합한 방식은 아닌 것 같다는 생각이 들었다.
typescript 는 타입선언을 여러가지 방법으로 지원하는데 이 중 인터페이스와 상속을 사용하면 쉽게 해결할 수 있다.
interface CustomInstance extends AxiosInstance
짜잔.. 바로 인터페이스를 상속받은 뒤, 재정의해서 쓰면 된다.
타입의 근본적인 형만 일치한다면, Instance 자체에 Generic 을 할당하거나
얼마든지 원하는 형태로 가공해서 사용할 수 있다.
아래의 예제는, 기본으로 내려주는 형태가 정해진 경우를 가정해서 작성했다.
type CustomResponseFormat<T = any> = {
response: T;
refreshedToken?: string;
}
interface CustomInstance extends AxiosInstance {
interceptors: {
request: AxiosInterceptorManager<AxiosRequestConfig>;
response: AxiosInterceptorManager<AxiosResponse<CustomResponseFormat>>;
};
getUri(config?: AxiosRequestConfig): string;
request<T>(config: AxiosRequestConfig): Promise<T>;
get<T>(url: string, config?: AxiosRequestConfig): Promise<T>;
delete<T>(url: string, config?: AxiosRequestConfig): Promise<T>;
head<T>(url: string, config?: AxiosRequestConfig): Promise<T>;
options<T>(url: string, config?: AxiosRequestConfig): Promise<T>;
post<T>(url: string, data?: any, config?: AxiosRequestConfig): Promise<T>;
put<T>(url: string, data?: any, config?: AxiosRequestConfig): Promise<T>;
patch<T>(url: string, data?: any, config?: AxiosRequestConfig): Promise<T>;
}
const client:CustomInstance = Axios.create({ baseURL: 'https://my-url.com/v1' });
// res - AxiosResponse<CustomResponseFormat>
client.interceptors.response.use(res => {
if(res.data.refreshedToken) TokenManager.replace(refreshedToken);
return res.data.response;
})
client.interceptors.request.use(req => {
req.headers.authorization = TokenManager.accessToken;
return req;
})
// user - { name:string; age:number; }
const user = await client.get<{ name:string; age:number; }>('/users/my-uid');
타입을 잘 다루는 분이라면, AxiosInterceptorManager
도 재정의하고, infer 혹은 predicates 를 통해서 interceptor 에서 리턴한 타입을 자동으로 추론되게 할 수 있지 않을까 하는 생각도 해본다.
끝.
마지막 CustomInstance
타입 정의해주시는 부분을 보고 interceptor
타입 문제 해결 할 수 있었습니다! 감사합니다 ㅎㅎ
저는 extends 할 때, 직접 작성하거나 라이브러리가 업데이트 되었을 때 CustomInstance
메소드들에 적어둔 파라미터들에 누락된 것이 생길 위험이 있다고 생각되어 아래와 같이 제네릭을 사용하였습니다!
interface CustomInstance extends AxiosInstance {
get<T>(...params: Parameters<AxiosInstance["get"]>): Promise<T>;
delete<T>(...params: Parameters<AxiosInstance["delete"]>): Promise<T>;
post<T>(...params: Parameters<AxiosInstance["post"]>): Promise<T>;
put<T>(...params: Parameters<AxiosInstance["put"]>): Promise<T>;
patch<T>(...params: Parameters<AxiosInstance["patch"]>): Promise<T>;
}
나의 구원자.....❤️🔥 감사합니다