Language/Typescript

[Typescript] Generics으로 Axios 깔끔하게 사용해보기

khakhalog 2024. 1. 4. 14:40

사이드 프로젝트를 하면서 여느때와 같이 Axios 인스턴스를 만들어서 사용하고 있다.

칸반 리팩토링을 하면서 Generics을 사용해 서버에서 만들어주는 응답값 인터페이스를 만들고, 각 api 함수를 선언시 Generics에 들어갈 타입을 지정해주도록 수정해서 pr을 올렸었다.

// apis/kanban.ts

interface IResponse<T> {
  success: boolean;
  error: any;
  data: T;
}

export const fetchKanbanList = async (): Promise<IResponse<IKabanData[]>> => {
  const data: IResponse<IKabanData[]> = await goHigerApi.get('/v1/applications/kanban');
  return data;
};

첨엔 나름 괜찮은것같다고 생각했지만... 보다보니 깔끔하지 않았다.

또, pr 리뷰 중에 응답값 인터페이스를 axios 인스턴스 부분에서 공통적으로 적용하자는 의견이 있어서 검색해보았는데 비슷한 글들이 많았다.

여러 개 글을 참고해서 인스턴스를 클래스로 캡슐화하고, http 메서드별로 타입을 지정한 커스텀 메서드를 만들었다.

// apis/index.ts
import axios, { AxiosResponse, InternalAxiosRequestConfig } from 'axios';

interface ICommonResponse<T> {
  success: boolean;
  error: any;
  data: T;
}

class ApiService {
  private api;

  constructor() {
    this.api = axios.create({
      baseURL: process.env.REACT_APP_BASE_URL,
      withCredentials: true,
    });

    this.api.interceptors.request.use(
      ...
    );

    this.api.interceptors.response.use(
      async (response: AxiosResponse) => {
        if (!response.data.success) {
          throw new Error(response.data.error);
        }
        return response;
      },
      error => {
        return Promise.reject(error);
      },
    );
  }

  public async commonRequest<T>(
    method: 'get' | 'post' | 'put' | 'patch' | 'delete',
    url: string,
    data?: any,
    config?: InternalAxiosRequestConfig,
  ): Promise<ICommonResponse<T>> {
    const response = await this.api[method](url, data, config);
    return response.data;
  }

  public Get = async <T>(url: string, config?: InternalAxiosRequestConfig) =>
    this.commonRequest<T>('get', url, undefined, config);

  public Post = async <T>(url: string, data?: any, config?: InternalAxiosRequestConfig) =>
    this.commonRequest<T>('post', url, data, config);

  public Put = async <T>(url: string, data?: any, config?: InternalAxiosRequestConfig) =>
    this.commonRequest<T>('put', url, data, config);

  public Patch = async <T>(url: string, data?: any, config?: InternalAxiosRequestConfig) =>
    this.commonRequest<T>('patch', url, data, config);

  public Delete = async <T>(url: string, config?: InternalAxiosRequestConfig) =>
    this.commonRequest<T>('delete', url, undefined, config);
}

export default new ApiService();
// apis/kanban.ts
import ApiService from 'apis';

...

export const fetchKanbanList = async () => {
  const data = await ApiService.Get<IKabanData[]>('/v1/applications/kanban');
  return data;
};

commonRequest 함수의 반환값 지정 타입인 Promise<ICommonResponse<T>>은 AxiosResponse 타입의 data에 해당하게 된다.

아래는 AxiosResponse 타입의 내용이다.

// node_modules/axios/index.d.ts
export interface AxiosResponse<T = any, D = any> {
  data: T;
  status: number;
  statusText: string;
  headers: RawAxiosResponseHeaders | AxiosResponseHeaders;
  config: InternalAxiosRequestConfig<D>;
  request?: any;
}

그간 Typescript를 소극적으로 사용해왔었는데, 제네릭을 함수 파라미터처럼 사용한다는 것을 알게되었고, 앞으로 더 적절하게 사용하면 편리하지 않을까 생각이 들었다.