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;