호진방 블로그
기술

API 공통 사항 관리를 위한, Axios 추상화 구현

# Offispace 프로젝트 API 공통 사항 관리를 위한, Axios 추상화 구현을 해봅시다

2024년 05월 19일

도입배경

Offispace 프로젝트는 로그인/회원가입, 커뮤니티, 공유 오피스, 마이페이지 등 다양한 기능을 포함한다. 이전 프로젝트에서는 각 기능 담당 팀원들이 각자의 컴포넌트에서 Axios 요청을 직접 처리 해야 했는데, 해당 방식은 다음과 같은 문제점을 가지고 있다.

또한 이번 프로젝트에서는 JWT 토큰 방식의 로그인을 구현했는데, 로그인 이후 모든 API 호출에 토큰을 헤더에 포함해야 하며, 토큰 유무 및 유효성에 따라 로그인 페이지로 Redirect 되어야 한다. 이러한 토큰 관리 과정을 각 컴포넌트에서 직접 처리하면 코드 복잡성이 증가하고 개발 효율성이 저하될 것이라고 생각했다.

따라서 위와 같은 문제점을 해결하기 위해 Axios 추상화를 도입하기로 했다.


시작하기도 전에 발생한 문제

아무리 Axios Instance를 잘 설계하고, 추상화를 진행해도 백엔드에서 내려주는 데이터가 통일되지 않은 형식으로 제공된다면 추상화된 코드에서 데이터를 제대로 사용할 수 없다는 문제가 발생했다.

이전까지 백엔드 팀 또한 각자의 개인화된 방식으로 요청 데이터를 내려줬기 때문에, 통일된 요청 데이터 형식이 필요하다는 것을 인지하게 되었고, BE팀과 회의를 통해 데이터 형식과 에러 코드 형식을 통일했다.

📎 API 명세서 / Swagger LInk

에러 Custom Code 명세서


Axios 인스턴스 생성

먼저 Axios Instance를 생성 해야하는데, Instance를 만드는 이유를 알기 위해서는 객체지향 프로그래밍과, 구성요소인 클래스, 인스턴스에 대한 개념 이해가 필요했다.

객체지향프로그래밍이란?

클래스와 인스턴스

간단히 말하면, 클래스는 객체를 만들기 위한 틀이고, 인스턴스는 그 틀을 이용하여 실제로 만들어진 객체라고 이해하면 쉽다.


❗ Axios에서 사용되는 클래스는 Axios 라이브러리 내부에 정의되어 있으며 HTTP 클라이언트의 기능과 설정을 추상화하는 역할을 한다. 이 때 인스턴스로 baseURL, header, params등이 있는데, 이러한 인스턴스 중 프로젝트에 자주 사용되는 내용들, 예를 들어 baseURL, token, Content-Type 등을 미리 지정하여, 간편하게 반복된 코드 없이 Axios 요청을 보낼 수 있다.


그럼 본격적으로 인스턴스를 만들어보자

src 아래 api 폴더를 만들고 instance 이름으로 파일을 만든다.

export const instance: Axios = axios.create({
  baseURL: process.env.NEXT_PUBLIC_BASE_URL,
  withCredentials: true,
  headers: {
    'Content-Type': 'application/json',
    Accept: '*/*'
  },
  timeout: 3000

인터셉터 설정

인터셉터는 요청이나 응답을 처리하기 전에 실행되는 함수다. 이를 이용하면 요청 또는 응답 데이터를 수정하거나 오류 또는 인증을 처리하는 데 사용할 수 있다.

instance.interceptors.request.use(
  (config: InternalAxiosRequestConfig): InternalAxiosRequestConfig => {
    const token = getCookie('token') as string;
    if (config && config.headers) {
      if (token) {
        config.headers.Authorization = `Bearer ${token}`;
      }
    }
    if (process.env.NODE_ENV === 'development') {
      const { method, url } = config;
      logOnDev(`🚀 [API] ${method?.toUpperCase()} ${url} | Request`);
    }
    return config;
  },
  (error: AxiosError | Error): Promise<AxiosError> => {
    return Promise.reject(error);
  }
);
 
instance.interceptors.response.use(
  (response: AxiosResponse): AxiosResponse => {
    return response;
  },
  (error: AxiosError | Error): Promise<AxiosError> => {
    if (process.env.NODE_ENV === 'development') {
      if (axios.isAxiosError(error)) {
        const { message } = error;
        const { method, url } = error.config as InternalAxiosRequestConfig;
        const { status, statusText } = error.response as AxiosResponse;
        logOnDev(
          `🚨 [API] ${method?.toUpperCase()} ${url} | Error ${status} ${statusText} | ${message}`
        );
        switch (status) {
          case 401: {
            toast.error('로그인이 필요합니다');
            break;
          }
         ,,,
        }
      } else {
        logOnDev(`🚨 [API] | Error ${error.message}`);
      }
    }
    return Promise.reject(error);
  }
);

요청 인터셉터

응답 인터셉터


API 요청 함수 생성

마지막으로, 앞서 생성한 인스턴스를 이용하여 API 요청 함수를 작성했다.

/* get 요청 */
export const getRequest = async <T,>(url: string, config?: AxiosRequestConfig): Promise<T> => {
  const response = await instance.get<T>(url, config as InternalAxiosRequestConfig);
  return response.data;
};
 
/* post 요청 */
export const postRequest = async <T, D>(
  url: string,
  data?: D,
  config?: AxiosRequestConfig
): Promise<T> => {
  const response = await instance.post<T>(url, data, config as InternalAxiosRequestConfig);
  return response.data;
};
 
/* delete 요청 */
export const deleteRequest = async <T,>(url: string, config?: AxiosRequestConfig): Promise<T> => {
  const response = await instance.delete<T>(url, config as InternalAxiosRequestConfig);
  return response.data;
};

이 함수는 제네릭 타입을 사용하여 호출 시 지정된 타입의 데이터를 반환할 수 있는데,


export interface ICommon<T> {
  //BE팀과 맞춘 데이터 형식
  status: string;
  errorCode: string;
  message: string;
  data: T;
}

사용 예시

export const userinfo = async () => {
  const response = await getRequest<UserInfoType>('members');
  return response;
};
 
type UserInfoType = ICommon<IUserInfo>;

요약 및 느낀점

1. 코드 재사용성 향상

Axios를 추상화함으로써 가장 크게 느낀 점은 코드 재사용성이 현저히 향상되었다는 것이다. 모든 API 호출에 대해 반복적으로 설정해야 하는 부분들을 하나의 폴더에서 관리할 수 있게 되면서, 각 모듈에서 코드 중복을 피할 수 있었는데, 이는 유지보수 측면에서도 큰 이점으로 다가왔다.

2. 에러 핸들링의 일관성

API 호출 시 발생할 수 있는 다양한 에러를 인터셉터를 통해 일관되게 처리할 수 있게 되었다. 이전에는 각 컴포넌트에서 개별적으로 에러 처리를 해야 했기 때문에, 에러 메시지나 처리 방식이 중구난방이였지만, 추상화된 Axios파일에서 에러를 관리함으로써 에러 핸들링과, 에러 메시지의 일관성을 유지 할 수 있었다.

3. 수정의 용이성

API 호출 시 공통적으로 필요한 헤더나 인증 토큰 등의 설정을 중앙에서 관리할 수 있게 되어, 설정 변경이 필요할 때마다 수정할 필요가 없어졌다. 이를 통해 헤더나 인증 토큰 변경이 단 한줄로 변경될만큼 훨씬 쉬워졌고, 실수로 인해 발생할 수 있는 버그를 사전에 방지할 수 있었다.

me
@banhogu
안녕하세요 배움을 나누며 함께 전진하는 1년차 주니어 개발자 방호진입니다.