Typescript로 더 깔끔하게 Axios 사용해보기

영근·2023년 7월 14일
5
post-thumbnail

리팩토링 하라고 칼들고 협박하는 내 마음 속 마지막 양심 ..

# 배경

이직 후 시작한 첫 프로젝트는, 서비스 되고 있는 프로덕트를 처음부터 ! 끝까지 ! 리빌드하는 프로젝트이다.
입사 일주일도 안되는 시점에서 많은 업무를 담당하게 되었지만, 오히려 나같은 무지랭이에게 요런 큰 기회를 주신게 감사하게 느껴진다.
사용하는 유저가 이렇게 많은 서비스를 내 손으로 처음부터 끝까지 만들어 간다는 경험을, 나같은 주니어가 해볼 수 있다니..!
그래서 더더욱 내 코드, 내 프로젝트에 애정이 생기는 중이다.

로그인부터 하나하나 서비스를 만들어가면서, API fetching을 할 때 조금 더 편하게, 조금 더 휴먼 에러를 방지할 수 있게 코드를 짜고자 했다.
공부할 때부터 Typescript와 Axios는 꽤 사용했지만, 급하게 사용하다보니 Typescript의 이점을 전혀 살리지 못한채로 사용하고 있어 이 부분을 개선하고자 했다.

어렵지도 않고, 복잡하지도 않은 기초적인 내용이지만,
일단 내가 뿌듯하고(중요) 한 번 정리해두면 나중에 또 기억이 잘 날 것 같아 정리해둔다.


# Interceptor

Interceptor(인터셉터)는 단어의 뜻 그대로, then 또는 catch로 처리되기 전에 요청과 응답을 가로채는 역할을 한다.

특히 에러 처리, 헤더 추가, 인증 관리(토큰 등), 로깅, 데이터 가공 등에 아주 유용하게 사용되는데, 커스텀 Interceptor를 하나 만들어두면 API fetching 때 마다 자동으로 함수가 실행되기 때문이다.

create instance

const axiosInstance = axios.create({
  baseURL: process.env.NEXT_PUBLIC_API_URL,
});
  • 먼저 create 함수를 이용해 instance를 생성한다.

onRequest, onResponse, onError

onRequest

const onRequest = (
    config: InternalAxiosRequestConfig,
  ): InternalAxiosRequestConfig => {
    const { method, url } = config;
    console.log(`🛫 [API - REQUEST] ${method?.toUpperCase()} ${url}`);

    const token = getCookie(COOKIE_KEY.LOGIN_TOKEN);
    config.headers.Authorization = `Bearer ${token}`;
    return config;
  };
  • config에서 methodurl을 꺼내준 뒤, console에 로깅한다.(이후 환경이 dev일 때만 console.log 가 실행되도록 변경해 줄 예정이다.)

  • 헤더에 토큰을 넣어줘서 request마다 자동으로 인증한다.

  • 이 때 config 타입은 InternalAxiosRequestConfig로, 최신 버전에서 요 타입으로 바뀌었다고 한다.(헷갈림 주의)


onResponse

  const onResponse = (res: AxiosResponse): AxiosResponse => {
    const { method, url } = res.config;
    const { code, message } = res.data;
    if (code === 'SUCCESS') {
      console.log(
        `🛬 [API - RESPONSE] ${method?.toUpperCase()} ${url} | ${code} : ${message}`,
      );
    } else {
      openSnackbar({
        theme: 'error',
        message,
        id: `apiError-${url}-${method}-${code}`,
      });
      console.log(
        `🚨 [API - ERROR] ${method?.toUpperCase()} ${url} | ${code} : ${message}`,
      );
    }

    return res;
  };
  • 마찬가지로 config에서 method, url을 꺼내준다.

  • 백엔드에서 커스텀 된 response를 주기 때문에, 나는 data에서 code와 message를 따로 꺼내 로깅에 사용했다.

  • 에러 시에는 에러메시지를 자동으로 스낵바 형태로 띄우도록 해줬다.(여기서 openSnackbar는 전역으로 snackbar 컴포넌트를 사용할 수 있게 해주는 hook이다)

onError

 const onError = (error: AxiosError | Error): Promise<AxiosError> => {
    if (axios.isAxiosError(error)) {
      const { method, url } = error.config as InternalAxiosRequestConfig;
      if (error.response) {
        const { statusCode, message } = error.response.data;
        console.log(
          `🚨 [API - ERROR] ${method?.toUpperCase()} ${url} | ${statusCode} : ${message}`,
        );
        openSnackbar({
          theme: 'error',
          message,
          id: `axiosError-${url}-${method}-${statusCode}`,
        });
      }
    } else {
      console.log(`🚨 [API] | Error ${error.message}`);
    }
    return Promise.reject(error);
  };
  • 에러는 AxiosError 타입의 에러와 Error 타입의 에러 모두를 캐치해줬다.

  • 마찬가지로 에러일 때 에러메시지를 Snackbar 컴포넌트를 이용해 띄운다.

적용해주기

axiosInstance.interceptors.request.use(onRequest);
axiosInstance.interceptors.response.use(
      onResponse,
      onError,
    );
  • 요렇게 처음에 생성해 둔 instance에 적용해주면 된다.

Interceptor에서 Hook을?

에러가 있을 때 스낵바를 띄워주는 게 나름 욕심이 나서, 기존에 만들어 잘 사용하고 있었던 Snackbar hook을 넣어주고 싶었다.
하지만 모두가 알다시피 hook을 사용할 수 있는 위치가 한정적이라 어떻게 할지 고민하고 서치하다가, 글을 하나 발견했다.(따봉)

Axios Interceptor에서 Hook을 사용하는 방법

보자마자 이거다 ..! 를 마음속으로 외치며 급히 컴포넌트를 부랴부랴 만들었다ㅋㅋㅋ

const AxiosInterceptor = ({ children }: { children: any }) => { 

  // 위의 onRequest, onResponse, onError 함수를 선언해준다
  ...
  
  // instance에 넣어주기
  useLayoutEffect(() => {
    const reqInterceptor = axiosInstance.interceptors.request.use(onRequest);
    const resInterceptor = axiosInstance.interceptors.response.use(
      onResponse,
      onError,
    );

    // 클린업
    return () => {
      axiosInstance.interceptors.request.eject(reqInterceptor);
      axiosInstance.interceptors.response.eject(resInterceptor);
    };
}
  • 처음에 useEffect를 사용해서 했더니, refresh 직후에는 인터셉터가 적용되기 전에 API fetching이 일어나는 문제가 있었다.

  • 그래서 useLayoutEffect를 사용해 빠르게 실행했다.

  • 요 컴포넌트는 최상단 _app.tsx에서 실행된다.



# Typescript generic으로 response type 지정해주기


고백

부끄럽게도 .. 시간을 핑계로 response type을 지정해주지 않은 채로 Axios를 사용하고 있었다.
이럴거면 Typescript를 왜쓰나 하는 생각에 짬나는 시간에 response type을 generic을 사용해 지정해줬다.

백엔드에서 커스텀하신 response를 내려주시기 때문에, Axios에서 기본적으로 제공하는 AxiosResponse 타입만으로 사용할 수가 없어, 중간에 generic을 한 번 더 이용해주기로 했다.


AxiosResponse 살펴보기

export interface AxiosResponse<T = any, D = any>  {
  data: T;
  status: number;
  statusText: string;
  headers: AxiosResponseHeaders;
  config: AxiosRequestConfig<D>;
  request?: any;
}
  • 요렇게 generic으로 넘겨준 type이 data에 들어가는 구조이다.

  • 하지만 우리 백엔드에서 주시는 response는 data안에 또 커스텀 된 타입이 있는 구조이다.


다시 한 번 generic

export interface CommonResponse<T> {
  code: 'SUCCESS' | 'ERROR' | 'FAIL';
  data: T;
  message: string;
  statusCode: number;
}
  • 요렇게 다시 한 번 generic을 사용해서 커스텀 된 response 타입을 지정해준다.

  • 그렇다면 사용할 때 AxiosResponse<CommonResponse<T>> 요 형태로 사용해줘야 하는데.. 보기에 너무 깔끔하지 않아서, get, post 함수 자체를 커스텀해주는 레퍼런스를 적용하기로 했다.

import { AxiosRequestConfig, AxiosResponse } from 'axios';

import axiosInstance from '@/lib/utils/AxiosInterceptor'; // 아까 만든 interceptor가 적용된 instance

import { CommonResponse } from '../types/commonResponse';

export const Get = async <T>(
  url: string,
  config?: AxiosRequestConfig,
): Promise<AxiosResponse<CommonResponse<T>>> => {
  const response = await axiosInstance.get(url, config);
  return response;
};

export const Post = async <T>(
  url: string,
  data?: any,
  config?: AxiosRequestConfig,
): Promise<AxiosResponse<CommonResponse<T>>> => {
  const response = await axiosInstance.post(url, data, config);
  return response;
};
  • 요렇게 GET, POST 함수를 커스텀 해주면 사용할 때 아래와 같이 깔끔하게 사용할 수 있다.
export const FetchUserInfo = async () => {
  const url = `/user/user`;

  const res = await Get<UserInfo>(url);
  return res.data;
};
  • UserInfo라는 이름으로 타입을 선언해준 뒤, generic 자리에 쏙 넣어주면 끝!

후기

나 자신이 그동안 Typescript를 꽤나 소극적으로 사용하고 있었다는 생각을 했다.
단순히 TypeError를 방지하기 위해서 props의 타입을 선언해주는 등에 그쳤었는데, 이렇게 generic을 사용해서 타입을 커스텀하고, 글에는 안나와있지만 extends나 omit을 사용해서 Typescript를 꽤나 효율적으로 사용해보려 노력했다.
그리고 Interceptor에서 하는 Error 핸들링이나 로깅 등으로 좀 더 편하게 개발하고자 했다.

앞으로 Typescript를 사용하면서 내가 사용하지 않고 있던 여러 메소드를 사용해보며 익숙해지는 것도 좋을 것 같다.

역시 깔끔해진 코드를 보는 건 언제나 기분좋다..!


Reference

0개의 댓글