Axios instance와 interceptor

김강민·2024년 11월 25일
0

개발

목록 보기
14/16
post-thumbnail

https://axios-http.com/kr/docs/instance

개발을 진행하면서 HTTP 요청을 다루게 되는데, 이때 많은 경우에 반복되는 패턴이나 코드를 발견할 수 있다.

예를 들자면,

  1. accessToken을 Header에 포함시켜서 API를 요청하거나
  2. refreshToken 만료(에러처리)를 통해 accessToken을 재발급하거나

이러한 반복적인 코드를 줄이고, 더 효과적으로 코드를 관리하기 위해 Aioxs 인스턴스와 인터셉터를 사용하는 방법에 대해 알아보자.

인스턴스란 무엇일까?

axios 의 instance는 기본적으로 axios 를 사용할 때 특정 설정을 미리 지정해놓은 별도의 “객체”를 생성해서 사용하는 것을 의미한다.

이렇게 생성된 instance는 axios.create() 메서드를 통해 만들어지며, 공통적으로 사용해야 할 설정 (예: 기본 URL, 헤더, 타임아웃 등)을 적용한 상태에서 API 호출을 할 수 있다.

Axios 인스턴스 생성

먼저, 반복적으로 사용되는 Axios 설정을 추상화하기 위해 Axios 인스턴스를 생성한다. Axios 인스턴스를 주어진 설정으로 초기화된 새로운 Axios 요청을 만들어낸다.

이렇게 생성된 인스턴스는 설정이 미리 적용되어 있으므로, 각 요청에서 이 설정을 반복하지 않아도 된다.

import axios, { AxiosInstance } from 'axios';

const API_BASE_URL = "https://some-domain.com/api/";

const createInstance = () => {
  const instance = axios.create({
    baseURL: API_BASE_URL,
    timeout: 2000,
  });

  return instance;
};

export const clientInstance = createInstance();

위의 코드에서는 기본 URL 및 타임아웃을 설정했다. 기본 URL은 모든 요청의 URL 앞에 추가되며, 타임아웃은 요청을 취소하는 데 걸리는 시간을 설정한다.

인터셉터 설정

https://axios-http.com/kr/docs/interceptors

인터셉터는 요청이나 응답을 처리하기 전에 실행되는 함수다. 이를 이용하면 중복되는 코드를 줄일 수 있으며, 요청 또는 응답을 더 세밀하게 제어할 수 있다.

  • request에서는 요청을 보내기 전에 하는 작업이 가능하고
  • response에서는 서버에서 받은 응답이 return 되기 전(then과 catch로 넘어가기 전)에 인터셉터로 가로채서 원하는 작업들을 추가할 수 있다.
  • 인터셉터를 추가했다가 삭제할 수도 있고
  • 커스텀 인스턴스에서도 인터셉터를 추가해서 사용할 수 있다.
const setInterceptors = (instance: AxiosInstance) => {
  instance.interceptors.response.use(
    (response) => response,
    (error) => {
      console.log('interceptor > error', error);
      return Promise.reject(error);
    }
  );

  instance.interceptors.request.use(
    (config) => {
      const token = '???'; // 토큰 주입 (프로젝트 정책마다 다름)

      if (token) {
        config.headers.Authorization = `Bearer ${token}`;
      }
      return config;
    },
    (error: AxiosError) => {
      console.log('interceptor > error', error);
      Promise.reject(error);
    }
  );
};

const createInstance = () => {
  const instance = axios.create({
    baseURL: API_BASE_URL,
    timeout: 2000,
  });
  setInterceptors(instance);

  return instance;
};

위의 코드에서는 요청 인터셉터와 응답 인터셉터를 설정했다.

  • 요청 인터셉터는 요청이 서버로 보내지기 전에 호출된다. 여기서는 토큰이 존재하면 이를 헤더에 추가하는 로직을 추가하였다.
  • 응답 인터셉터는 서버로부터 응답을 받은 후에 호출된다. 여기서는 오류가 발생하면 콘솔에 오류 메시지를 출력하도록 하였다.

실제로 프로젝트에서 사용한 코드

intercepter를 활용해 요청을 가로채서 요청을 하기 전 header에 token(access, refresh) 를 담아서 api 요청을 보낸다.

instance 생성

import type {
  AxiosInstance,
  AxiosRequestConfig,
  InternalAxiosRequestConfig,
} from 'axios';
import axios from 'axios';

import { authStorage } from '../../utils/storage/authStorage';
import { ERROR_STATUS } from '@/shared';
import { QueryClient } from '@tanstack/react-query';

const initInstance = (config: AxiosRequestConfig): AxiosInstance => {
  const instance = axios.create({
    timeout: 5000,
    ...config,
    headers: {
      Accept: 'application/json',
      'Content-Type': 'application/json',
      'Cross-Control-Allow-Origin': '*',

      ...config.headers,
    },
  });

  return instance;
};

export const BASE_URI = `https://sinitto.site`;

export const fetchInstance = initInstance({
  baseURL: BASE_URI,
});

interceptor를 활용하기

로그인 , 회원가입 시 localStorage에 accessToken과 refreshToken 보관.

accessToken

  • localStorage에 넣어서 보관, api 요청시 꺼내서 header에 담아서 보내기

refreshToken

  • localStorage에 넣어서 보관
  • accessToken 만료 시 refreshToken을 body에 담아 보낸 후 accessToken을 갱신하여 다시 localStorage에 보관
  • 이때 토큰 만료시 발생하는 error 코드를 확인 후 갱신할 수 있도록 구현한다. (백엔드한테 물어보기!)
fetchInstance.interceptors.request.use(
  (config: InternalAxiosRequestConfig) => {
    const accessToken = authStorage.accessToken.get();
    if (accessToken !== undefined) {
      config.headers['Content-Type'] = 'application/json';
      config.headers.Authorization = `Bearer ${accessToken}`;
    }
    return config;
  },
  (error) => {
    return Promise.reject(error);
  }
);

fetchInstance.interceptors.response.use(
  (response) => response,
  async (error) => {
    const originalRequest = error.config;
    if (
      error.response?.status === ERROR_STATUS.ACCESS_TOKEN_EXPIRATION &&
      !originalRequest._retry
    ) {
      originalRequest._retry = true;

      const refreshToken = localStorage.getItem('refreshToken');

      if (!refreshToken) {
        return Promise.reject(error);
      }
      const resp = await fetch(`${BASE_URI}/api/auth/refresh`, {
        method: 'post',
        headers: {
          'Content-Type': 'application/json',
          'Cross-Control-Allow-Origin': '*',
          Authorization: `Bearer ${refreshToken}`,
        },
        body: JSON.stringify({ refreshToken }),
      });
      if (resp.ok) {
        console.log('토큰 재발급 성공');

        const data = await resp.json();

        authStorage.accessToken.set(data.accessToken);
        authStorage.refreshToken.set(data.refreshToken);

        return fetchInstance(originalRequest);
      } else if (
        resp.status === ERROR_STATUS.REFRESH_TOKEN_EXPIRATION ||
        resp.status === ERROR_STATUS.INVALID_REFRESH_TOKEN
      ) {
        console.log('토큰 재발급 실패');

        localStorage.removeItem('accessToken');
        localStorage.removeItem('refreshToken');
      }
      return Promise.reject(error);
    }
    return Promise.reject(error);
  }
);

API 호출 코드

작성한 instance에 interceptor를 활용해 header에 token을 담았기 때문에 api 호출만을 위한 코드를 작성한다.

import { fetchInstance } from '@/shared';

export type AcceptedCallBackListResponse = {
  callbackId: number;
  seniorName: string;
  postTime: string;
  seniorId: number;
  status: 'WAITING' | 'IN_PROGRESS' | 'COMPLETE' | 'PENDING_COMPLETE';
};

export const getAcceptedCallBackListPath = '/api/callbacks/sinitto/accepted';

export const getAcceptedCallBackList =
  async (): Promise<AcceptedCallBackListResponse> => {
    const response = await fetchInstance.get<AcceptedCallBackListResponse>(
      getAcceptedCallBackListPath
    );
    return response.data;
  };

Query-hook

  • get 요청의 경우 -> useQuery 의 캐싱을 활용하기 위해
  • post, delete, put 등의 요청의 경우 -> useMutation 의 onSuccess, onError 등의 속성을 활용하기 위해

이러한 이점을 활용하기 위해 react-query 라이브러리를 활용한 hook을 만들어서 사용한다.

import { getAcceptedCallBackList } from '../apis';
import { getAcceptedCallBackListPath } from '../apis/accepted-call-back-list.api';
import { AcceptedCallBackListResponse } from '../types';
import { useQuery } from '@tanstack/react-query';

const getAcceptedCallBackListQueryKey = [getAcceptedCallBackListPath];

export const useGetAcceptedCallBackList = () => {
  return useQuery<AcceptedCallBackListResponse>({
    queryKey: getAcceptedCallBackListQueryKey,
    queryFn: () => getAcceptedCallBackList(),
  });
};

언젠간 쿠키도 사용해야겠지...?

profile
인생은 프레임워크처럼, 공부는 라이브러리처럼

0개의 댓글

관련 채용 정보