axios 모듈을 사용하는 경우에 토큰 갱신 로직을 정리합니다.

API 스펙은 다음과 같습니다.

위 로직을 axios interceptor를 이용하여 공통으로 처리하고자 합니다.

import axios, { AxiosError, InternalAxiosRequestConfig, isAxiosError } from 'axios';
import { BASE_URL, TOKEN_REFRESH_URL } from 'constants/api';

// 토큰 갱신 요청 진행중 여부
let isRefreshing = false;

// 401 에러 요청 처리를 위한 큐 { resolve, reject }의 배열
let failedQueue: any[] = [];

// 토큰 갱신 후 요청 처리를 위한 큐 처리
const processQueue = (error: any, token: string | null = null) => {

	// 쌓여있던 실패 요청들을 반복하며 토큰 갱신 결과를 전달
  failedQueue.forEach((prom) => {
  
	  // 토큰 갱신이 실패한 경우 최종 에러 처리
    if (error) {
      prom.reject(error);
      
    // 토큰 갱신이 성공하였다면 신규 토큰으로 요청 재시도
    } else {
      prom.resolve(token);
    }
  });

	// 실패 큐 초기화
  failedQueue = [];
};

/**
 * HTTP 통신을 위한 axios 인스턴스 생성
 */
const http = axios.create({
  baseURL: BASE_URL,
});

/**
 * HTTP 요청 인터셉터
 */
http.interceptors.request.use(
  (request) => {
    // 인증 토큰
    const accessToken = localStorage.getItem('accessToken');

    // API 호출시 인증 토큰을 헤더에 추가
    if (accessToken) {
      request.headers['Authorization'] = `Bearer ${accessToken}`;
    }

    return request;
  },
  (error) => {
    return Promise.reject(error);
  },
);

/**
 * HTTP 응답 인터셉터
 */
http.interceptors.response.use(
  // 응답 성공시
  (response) => response,

  // 응답 실패시
  async (error: AxiosError) => {
    const config = error.config as InternalAxiosRequestConfig;

    // 응답 상태가 401이 아닌 경우
    const isNot401Error = !isAxiosError(error) || error.response?.status !== 401;

    // 토큰 갱신 요청이 실패했거나 401에러가 아닌 경우는 즉시 에러로 종결 처리
    if (config?.url === TOKEN_REFRESH_URL || isNot401Error) {
      return Promise.reject(error);
    }

    console.log('isRefreshing', isRefreshing, config.url);

    // 토큰 갱신 요청이 이미 진행중인 경우 큐에 추가
    if (isRefreshing) {
    
	    // 프로미스 객체를 전달, 최초 API 호출부는 결과적으로 해당 프로미스가 완료되어야 종결됨
      return new Promise((resolve, reject) => {
	      // 실패 큐에 resolve, reject 푸시
        failedQueue.push({ resolve, reject });
      })
	      // 토큰 갱신이 성공하여 새로운 토큰을 받은 경우
        .then((token) => {
	        // 최초 요청 config에서 토큰만 변경하여 재호출
          config.headers.Authorization = `Bearer ${token}`;
          
          // 새로운 요청, 이 요청에 대한 Promise가 완료시 최초 호출부 종결됨
          return http(config);
        })
        // 토큰 갱신에 실패한 경우 최종 에러처리
        .catch((error) => {
          return Promise.reject(error);
        });
    }

    // 토큰 갱신요청 시작
    isRefreshing = true;

    // 기존 토큰
    const prevAccessToken = localStorage.getItem('accessToken');
    
    // 기존 토큰을 찾기 못한 경우 최종 에러처리
    if (!prevAccessToken) {
      return Promise.reject(error);
    }

    try {
      // 토큰 갱신 요청
      const response = await http.post(TOKEN_REFRESH_URL, prevAccessToken);

      // 갱신된 토큰
      const newAccessToken = response.data.data?.[0]?.accessToken;

      if (!newAccessToken) {
        return Promise.reject(error);
      }

      // 갱신된 토큰을 저장
      localStorage.setItem('accessToken', newAccessToken);
      
      // 토큰 갱신하는 사이에 실패한 API들이 있다면 재시도 하기위해 실패큐 실행
      processQueue(null, newAccessToken);

      // 갱신된 토큰을 헤더에 추가
      config.headers.Authorization = `Bearer ${newAccessToken}`;

      // 토큰 갱신 후 요청 재시도
      return http(config);
    } catch (error) {
      console.error(error);
      
      // 토큰 갱신하는 사이에 실패한 API들이 있다면 최종실패처리 하기위해 실패큐에 에러전달
      processQueue(error);

			// 최종 에러처리
      return Promise.reject(error);
    } finally {
	    // 토큰 갱신로직 종료
      isRefreshing = false;
    }
  },
);

export default http;