최근 진행 중인 프로젝트에서 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를 알아보고 코드를 리펙토링해보았다. 특히 토큰만료시간을 계산하지 않고 요청을 처리할 수 있게 된 부분에서 뿌듯했다ㅎㅎ
잘 읽었어요