class TokenRefreshManager {
// 현재 토큰 재발급이 진행 중인지 확인하는 플래그
private isRefreshing = false;
// 토큰 재발급을 기다리는 요청들의 콜백 함수 배열
private refreshSubscribers: Array<() => void> = [];
// 대기열에 콜백 추가
public addSubscriber(callback: () => void) {
this.refreshSubscribers.push(callback);
}
// 토큰 재발급 완료 후 대기 중인 모든 요청 실행
private onRefreshed() {
this.refreshSubscribers.forEach((callback) => callback());
this.refreshSubscribers = []; // 대기열 초기화
}
async refreshToken(): Promise<boolean> {
// 이미 재발급 진행 중이면 새로운 Promise 반환
if (this.isRefreshing) {
return new Promise<boolean>((resolve) => {
// 현재 요청을 대기열에 추가
this.addSubscriber(() => resolve(true));
});
}
this.isRefreshing = true; // 재발급 시작
try {
const response = await fetch('<http://localhost:8080/token/reissue>', {
method: 'POST',
credentials: 'include', // 쿠키 포함하여 요청
});
if (!response.ok) {
throw new Error('토큰 갱신 실패');
}
this.isRefreshing = false;
this.onRefreshed(); // 대기 중인 모든 요청 실행
return true;
} catch (error) {
this.isRefreshing = false;
this.refreshSubscribers = []; // 에러 발생 시 대기열 초기화
return false;
}
}
}
export async function fetchWithToken(
url: string,
options: RequestInit = {}
): Promise<Response> {
// 기본 헤더 설정
const headers = {
...options.headers,
};
// 첫 번째 API 요청 시도
let response = await fetch(url, {
...options,
headers,
credentials: 'include', // 모든 요청에 쿠키 포함
});
// 401 에러(토큰 만료) 처리
if (response.status === 401) {
// 토큰 재발급 시도
const isRefreshSuccess = await TokenRefreshManager.refreshToken();
if (isRefreshSuccess) {
// 재발급 성공 시 원래 요청 재시도
response = await fetch(url, {
...options,
headers,
credentials: 'include',
});
} else {
// 재발급 실패 시 로그인 페이지로 강제 이동
alert('세션이 만료되었습니다. 다시 로그인해주세요.');
window.location.href = '/login';
throw new Error('인증 실패');
}
}
// 기타 에러 처리
if (!response.ok) {
throw new Error(`요청에 실패했습니다: ${response.status}`);
}
return response;
}
초기 요청 처리
fetchWithToken을 통해 처리됩니다.credentials: 'include'로 설정하여 쿠키가 자동으로 포함됩니다.토큰 만료 감지 (401 에러)
if (response.status === 401) {
const isRefreshSuccess = await TokenRefreshManager.refreshToken();
// ...
}
토큰 재발급 프로세스
async refreshToken(): Promise<boolean> {
if (this.isRefreshing) {
return new Promise<boolean>((resolve) => {
this.addSubscriber(() => resolve(true));
});
}
// ...
}
동시성 처리
private refreshSubscribers: Array<() => void> = [];
재발급 완료 후 처리
private onRefreshed() {
this.refreshSubscribers.forEach((callback) => callback());
this.refreshSubscribers = [];
}
에러 처리