Axios Response with generic

wookhyung·2023년 6월 3일
3

⚙️ Axios의 기본 response 타입을 바꿔보자

🤔 Why?

왜 기본 response 타입을 바꿔서 사용할까요?

타입스크립트 환경의 프론트엔드에서는 서버의 응답값 을 기반으로 타입을 만들어서 사용하게 됩니다. 예를 들어, 위와 같은 API 응답값이 내려온다고 할 때 다음과 같이 응답값 interface를 만들어서 사용합니다.

interface FooResponse {
	statusCode: number;
	payload: {
		id: number;
		email: string;
	}
}

interface BarResponse {
	statusCode: number;
	payload: {
		id: number;
		role: number;
	}
}

FooResponseBarResponse 를 보면 알다시피 대부분의 API들은 다음과 같이 비슷한 형태의 응답값을 반환 합니다. 때문에 response interface를 매 번 새로 만들기 보다는 중복되는 interface를 미리 정의하고 사용하는 편이 좋다고 생각했습니다.

🔍 How?

중복되는 값들을 공통 인터페이스로 분리하여 상속받는건 어떨까요?

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>>('...');
  • 이제 FooResponseBarResponse 는 매번 statusCodepayload를 작성하지 않아도 ServerResponse제네릭을 활용하여 타입을 재활용 할 수 있게 되었습니다.
  • 다만 매번 ServerResponse 를 작성하는 건 귀찮은 일이기 때문에, axios를 그대로 사용하는 것이 아닌 axios를 래핑한 Axios 인스턴스 를 만들어서 기본 타입을 ServerResponse로 정의해봅시다.

Axios 인스턴스 | Axios Docs

Axios Response 타입은 어떻게 만들어져있을까?

  • axios의 기본 타입은 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> 입니다.

AxiosResponse 타입 살펴보기

/* 
  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();

😎 Conclusion

새로 만든 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>();
profile
Front-end Developer

2개의 댓글

comment-user-thumbnail
2023년 8월 26일

Axios 인스턴스 매서드의 제네릭값을 어떻게 주어야 할 지 고민하던 찰나에 많은 도움이 되었습니다. 감사합니다!

1개의 답글