안녕하세요 오늘은 프로젝트를 진행하면서 생긴 refresh Token으로 Token을 재발급할 때
제대로 발급 안되는 문제들에 대해 어떻게 해결 했는지 트러블 슈팅을 작성 해봤어요~


원인을 모르고 refresh Token 요청이 계속 실패하면서 무한으로 요청이 계속 가는 문제가 발생했습니다
실제로 이 요청이 무한으로 가고 있는 줄도 모르고 계속 요청을 보내게 되어서 서버가 잠시 다운될 정도였습니다
이 때까지는 refresh Token 요청이 안되는 이유를 찾지는 못하였습니다

refresh Token 요청이 거의 동시에 여러 개가 보내지면서 모든 요청이 실패가 되는 상황이 벌어졌습니다
처음에는 제대로 된 이유도 파악하지 못하고 미뤄두고 있었는데 하루 날을 잡고 안되는 이유들을 찾으려고 몇가지를 나열하니
금방 해결되었던거 같습니다

이 문제는 정말 간단하게 해결 했습니다
axios retry 횟수를 정해 같은 요청이 계속 실패해도 다시 가지 않도록 3회 정도로 제한을 두었습니다
이 문제를 겪고 혹시 모를 상황을 대비하여서 항상 retry 횟수를 정해두는 습관을 가졌습니다
이 문제를 해결한 방법은 401(Unauthorized)이 발생한 에러들을 모두 큐에 적재하여 하나씩 꺼내 refresh Token 요청을 보내도록 했습니다
이렇게 하면 바로 요청을 보내는게 아니라 큐에 적재하여 하나씩 꺼내서 처리 하기 때문에 동시에 여러 요청이 가는 문제를 막을 수 있었습니다
import axios, {
AxiosError,
AxiosResponse,
InternalAxiosRequestConfig,
} from 'axios';
import { authConfig } from '../config/auth';
import {
AUTH_TOKEN_KEY,
AUTH_REFRESH_TOKEN_KEY,
getCookie,
setCookie,
clearTokens,
} from '@/entities/user';
export const baseURL = process.env.NEXT_PUBLIC_API_URL;
export const instance = axios.create({
baseURL,
timeout: 5000,
headers: {
'Content-Type': 'application/json',
},
});
const refreshClient = axios.create({
timeout: 5000,
headers: {
'Content-Type': 'application/json',
},
});
let isRefreshing = false;
let failedQueue: Array<{
resolve: (token: string) => void;
reject: (error: any) => void;
}> = [];
const processQueue = (error: any, token: string | null = null) => {
failedQueue.forEach(({ resolve, reject }) => {
if (error) reject(error);
else resolve(token!);
});
failedQueue = [];
};
instance.interceptors.request.use(
(config: InternalAxiosRequestConfig) => {
if (typeof window !== 'undefined') {
const accessToken = getCookie(AUTH_TOKEN_KEY);
if (accessToken) {
config.headers.Authorization = `Bearer ${accessToken}`;
}
}
return config;
},
(error: AxiosError) => Promise.reject(error),
);
instance.interceptors.response.use(
(response: AxiosResponse) => response,
async (error: AxiosError) => {
if (typeof window === 'undefined') return Promise.reject(error);
const originalRequest = error.config as InternalAxiosRequestConfig & {
_retry?: boolean;
};
if (error.response?.status !== 401) return Promise.reject(error);
const url = originalRequest.url ?? '';
const SKIP_PATHS = [
'/api/admin/signin',
'/api/signin',
'/api/auth/reissue',
'/auth/reissue',
];
if (SKIP_PATHS.some((p) => url.includes(p))) {
return Promise.reject(error);
}
if (originalRequest._retry) return Promise.reject(error);
originalRequest._retry = true;
if (isRefreshing) {
return new Promise((resolve, reject) => {
failedQueue.push({
resolve: (token: string) => {
originalRequest.headers.Authorization = `Bearer ${token}`;
resolve(instance(originalRequest));
},
reject,
});
});
}
isRefreshing = true;
try {
const refreshToken = getCookie(AUTH_REFRESH_TOKEN_KEY);
if (!refreshToken) throw new Error('No refresh token available');
const res = await refreshClient.patch<{ accessToken: string }>(
'/api/auth/reissue',
null,
{
headers: {
refreshtoken: refreshToken,
},
},
);
const { accessToken } = res.data;
setCookie(AUTH_TOKEN_KEY, accessToken, 86400);
processQueue(null, accessToken);
originalRequest.headers.Authorization = `Bearer ${accessToken}`;
return instance(originalRequest);
} catch (refreshError) {
processQueue(refreshError, null);
clearTokens();
const currentPath = window.location.pathname;
const isProtectedPage = authConfig.protectedPages.some((path: string) =>
currentPath.startsWith(path),
);
if (isProtectedPage) {
window.location.replace(authConfig.signInPage);
}
return Promise.reject(refreshError);
} finally {
isRefreshing = false;
}
},
);
오늘은 Token 재발급 문제를 트러블슈팅으로 간단히 정리해봤습니다
문제를 해결하는 과정에서 여러 가설을 세우고 하나씩 검증해 나가니 배운 점도 많았고 해결 속도도 빨라졌다고 느꼈습니다
읽어주셔서 감사합니당