axios interceptor에 대해 파헤치기

박상현·2023년 11월 4일
2
post-thumbnail

개요

최근 진행 중인 프로젝트에서 401 토큰만료 코드가 정상적으로 작동하지 않는 문제가 있었다. 사실 프로젝트 초반에 토큰만료 로직을 구성했었는데, 코드에 대한 이해없이 구글링해서 복붙한 코드가 문제였다.

간단한 문제일지도 모르나, 난 이 문제를 해결하기 위해 5시간동안 삽질을 했었다ㅎㅎ
한번 알아보자!!

기존 코드의 문제점

나는 학교 선배님의 토큰만료 재발급 코드의 글을 읽었었다. 방법으로는 요청 전처리에서 어세스 토큰만료시간과 현재시간을 비교하여 재발급을 받는 로직이였다. 난 day.js를 이용하여 그 글을 기반으로 코드를 구성했었다.

하지만 콘솔에 찍어보며 잘 동작하는지 확인을 하는 과정을 거쳤었다. 문제는 여기서 발생하였다. 어세스 토큰이 필요한 api마다 토큰을 재발급을 받아서 요청을 처리하는 것이였다. 그 당시 나는 그 이유가 뭔지 모르고 다른 작업을 하고 있었다.

시간이 지날수록 이상함을 느낀 나는 다른 사람들의 토큰만료 로직을 자세히 살펴보았다. 대부분이 응답 전처리에 토큰만료로직이 구성되어있었다. 그렇다고 그 선배님 방법이 잘못되었다는 것은 아니다. 실제로 토큰만료시간을 계산하여 재발급 받는 로직은 여러 개발자들이 사용하지만 케바케로 난 이 방법을 별로 선호하지 않을 뿐이다. 아무튼 중요한 점은...

아! 나는 interceptor 개념을 잘 모르고 있었구나!! 이다....

그래서 바로 axios interceptor를 공부하기로 했다.

요청 전처리

말 그대로 axios가 서버로 요청을 보내기 전, 요청을 가로채 특정작업을 수행할 수 있다.
아래 코드는 위에서 말한대로 작성되었던 코드이다.

// requestHandler.ts
import { InternalAxiosRequestConfig } from "axios";
import {
  ACCESS_TOKEN_KEY,
  REFRESH_TOKEN_KEY,
  REQUEST_TOKEN_KEY,
} from "@src/constants/Auth/auth.constant";
import Token from "../Token/Token";
import { rollingAxios } from "./customAxios";
import dayjs from "dayjs";
import authRepositoryImpl from "@src/repositories/Auth/auth.repositoryImpl";
import { jwtDecoding } from "@src/utils/Auth/jwtDecoding";

export const requestHandler = async (config: InternalAxiosRequestConfig) => {
  let access_token = Token.getToken(ACCESS_TOKEN_KEY);
  const refresh_token = Token.getToken(REFRESH_TOKEN_KEY);
  const expirationAt = new Date(Number(jwtDecoding("exp")) * 1000); // 1000을 곱해 밀리초 단위로 변환

  // 만료시간과 현재 시간을 비교하여 0 미만이면 어세스 토큰이 만료된 것이므로 아래 조건문 실행
  if (dayjs(expirationAt).diff(dayjs()) < 0 && refresh_token) {
    try {
      const { accessToken: newAccessToken } =
        await authRepositoryImpl.postRefreshToken({
          refreshToken: refresh_token,
        });
      Token.setToken(ACCESS_TOKEN_KEY, newAccessToken);
      access_token = newAccessToken;
    } catch (e) {
      // 요청을 실패하면 리프레쉬 토큰도 만료이므로 아래처럼 처리
      Token.clearToken();
      window.alert("토큰이 만료되었습니다!");
      window.location.href = "/";
    }
  }
  rollingAxios.defaults.headers[REQUEST_TOKEN_KEY] = `Bearer ${access_token}`;
  return config;
};

난 위 코드를 응답 전처리로 옮겼고, 기존 코드에선 요청을 보내기 전에 토큰이 존재하면 헤더에 토큰만 담아 보내도록 하는 로직으로 수정하였다.

// requestHandler.ts
import { AxiosRequestConfig } from "axios";
import {
  ACCESS_TOKEN_KEY,
  REFRESH_TOKEN_KEY,
  REQUEST_TOKEN_KEY,
} from "@src/constants/Auth/auth.constant";
import Token from "../Token/Token";

export const requestHandler = (config: AxiosRequestConfig) => {
  const access_token = Token.getToken(ACCESS_TOKEN_KEY);
  const refresh_token = Token.getToken(REFRESH_TOKEN_KEY);

  if (access_token || refresh_token) {
    config.headers = {
      "Content-Type": "application/json",
      [REQUEST_TOKEN_KEY]: `Bearer ${Token.getToken(ACCESS_TOKEN_KEY)}`,
    };
  }
  return config;
};

그럼 응답 전처리는 뭐고 어떻게 고쳤는지 알아보자!!

응답 전처리

응답 전처리는 서버가 주는 응답을 처리하기 전에 응답을 가로채 특정 작업을 수행하는 것이다. 난 응답 전처리를 이용하여 401 에러이면 토큰을 재발급 받은 후 밀렸던 요청을 한번에 처리할 수 있도록 하였다.

아래 코드는 위에 말한 것을 토대로 작성한 코드다.

// responseHandler.ts
import { AxiosError } from "axios";
import {
  ACCESS_TOKEN_KEY,
  REFRESH_TOKEN_KEY,
  REQUEST_TOKEN_KEY,
} from "@src/constants/Auth/auth.constant";
import Token from "../Token/Token";
import authRepositoryImpl from "@src/repositories/Auth/auth.repositoryImpl";
import { rollingAxios } from "./customAxios";

export const responseHandler = async (error: AxiosError) => {
  const access_token = Token.getToken(ACCESS_TOKEN_KEY);
  const refresh_token = Token.getToken(REFRESH_TOKEN_KEY);
  
  if (error.response) {
    const {
      config: originalRequest,
      response: { status },
    } = error;

    if (access_token && refresh_token && status === 401) {
      try {
        const { accessToken: newAccessToken } =
          await authRepositoryImpl.postRefreshToken({
            refreshToken: refresh_token,
          });

        Token.setToken(ACCESS_TOKEN_KEY, newAccessToken);

        // 새로운 토큰 헤더에 업데이트
        rollingAxios.defaults.headers.common[
          REQUEST_TOKEN_KEY
        ] = `Bearer ${newAccessToken}`;

        // 밀렸던 요청을 처리
        return rollingAxios(originalRequest);
      } catch (e) {
        Token.clearToken();
        window.alert("토큰이 만료되었습니다!");
        window.location.href = "/";
      }
    }
  }
  return Promise.reject(error);
};

이렇게 구성하면 요청을 토큰이 필요한 api마다 요청을 보내지 않고 한번만 요청할 수 있게 된다!! 또한 토큰만료시간을 계산할 필요가 없어지게 되어 보다 간편하게 코드를 작성할 수 있다.

그럼 요청, 응답 전처리 코드는 어떻게 사용할 것인가인데..
아래 코드처럼 모듈로 불러와 사용하면 된다!!

import axios from "axios";
import {
  ACCESS_TOKEN_KEY,
  REQUEST_TOKEN_KEY,
} from "@src/constants/Auth/auth.constant";
import Token from "../Token/Token";
import { responseHandler } from "./responseHandler";
import { requestHandler } from "./requestHandler";

export const rollingAxios = axios.create({
  baseURL: process.env.REACT_APP_ROLLING_API_KEY,
  headers: {
    [REQUEST_TOKEN_KEY]: `Bearer ${Token.getToken(ACCESS_TOKEN_KEY)}`,
  },
});

// 요청 전처리
rollingAxios.interceptors.request.use(requestHandler, (response) => response);
// 응답 전처리
rollingAxios.interceptors.response.use((response) => response, responseHandler);

마무리

이렇게 interceptor를 알아보고 코드를 리펙토링해보았다. 특히 토큰만료시간을 계산하지 않고 요청을 처리할 수 있게 된 부분에서 뿌듯했다ㅎㅎ

profile
Plus Ultra 👍

2개의 댓글

comment-user-thumbnail
2023년 11월 13일

잘 읽었어요

1개의 답글