Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions src/api/auth/auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import type {
ILoginResponse,
ISignUpRequest,
ISignUpResponse,
ITokenRefreshResponse,
} from "../../types/auth/auth";

import { axiosInstance } from "@/lib/axiosInstance";
Expand Down Expand Up @@ -43,6 +44,14 @@ export const signUp = async (
return responseData;
};

// 토큰 재발급
export const reissueToken = async (): Promise<
ICommonResponse<ITokenRefreshResponse>
> => {
const { data } = await axiosInstance.post("/api/auth/reissue");
return data;
};

// 로그인
export const login = async (
data: ILoginRequest,
Expand Down
72 changes: 71 additions & 1 deletion src/lib/axiosInstance.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import axios from "axios";

import useAuthStore from "@/store/useAuthStore";

export const axiosInstance = axios.create({
baseURL: import.meta.env.VITE_API_BASE_URL,
withCredentials: true,
Expand All @@ -22,11 +24,79 @@ axiosInstance.interceptors.request.use(
},
);

let isRefreshing = false;
let refreshSubscribers: ((token: string) => void)[] = [];

// 대기 요청 처리
const onRefreshed = (accessToken: string) => {
refreshSubscribers.forEach((callback) => callback(accessToken));
refreshSubscribers = [];
};

const addRefreshSubscriber = (callback: (token: string) => void) => {
refreshSubscribers.push(callback);
};

axiosInstance.interceptors.response.use(
(response) => {
return response;
},
(error) => {
async (error) => {
const originalRequest = error.config;

// 401 에러 감지
if (error.response?.status === 401) {
// 재발급 진행 중: 대기열 등록
if (isRefreshing) {
return new Promise((resolve) => {
addRefreshSubscriber((accessToken: string) => {
originalRequest.headers.Authorization = `Bearer ${accessToken}`;
resolve(axiosInstance(originalRequest));
});
});
}

// 재발급 실패: 로그아웃
if (
originalRequest.url?.includes("/api/auth/reissue") ||
originalRequest._retry
) {
useAuthStore.getState().logout();
return Promise.reject(error);
}

// 첫 401: 재발급 시도
originalRequest._retry = true;
isRefreshing = true;

try {
const { data } = await axios.post(
`${import.meta.env.VITE_API_BASE_URL}/api/auth/reissue`,
{},
{ withCredentials: true },
);

const newAccessToken = data.data.accessToken;
localStorage.setItem("accessToken", newAccessToken);

// 대기 요청 일괄 처리
onRefreshed(newAccessToken);

// 현재 요청 재시도
originalRequest.headers.Authorization = `Bearer ${newAccessToken}`;
return axiosInstance(originalRequest);
} catch (refreshError) {
// 재발급 실패: 로그아웃
console.error("Token reissue failed:", refreshError);
useAuthStore.getState().logout();
return Promise.reject(refreshError);
} finally {
// 상태 초기화
isRefreshing = false;
refreshSubscribers = [];
}
}

console.error("API Error:", error);
return Promise.reject(error);
},
Expand Down
12 changes: 12 additions & 0 deletions src/types/auth/auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,3 +42,15 @@ export interface ILoginResponse {
accessToken: string;
accessTokenExpiresIn: number;
}

// 토큰 재발급 요청 타입
export interface ITokenRefreshRequest {
refreshToken: string;
}

// 토큰 재발급 응답 타입
export interface ITokenRefreshResponse {
grantType: string;
accessToken: string;
accessTokenExpiresIn: number;
}
31 changes: 31 additions & 0 deletions vite.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ export default defineConfig(({ mode }) => {

return {
plugins: [react(), svgr({ include: "**/*.svg?react" }), tailwindcss()],

// 개발 서버 및 프록시 설정
server: {
host: "0.0.0.0",
port: 5173,
Expand All @@ -18,10 +20,39 @@ export default defineConfig(({ mode }) => {
},
},
},

// 경로 별칭
resolve: {
alias: {
"@": "/src",
},
},

// 배포 시 콘솔 로그 제거
esbuild: {
drop: mode === "production" ? ["console", "debugger"] : [],
},

// 빌드 최적화
build: {
chunkSizeWarningLimit: 1000, // 청크 크기 경고 한도 상향
rollupOptions: {
output: {
// 라이브러리 및 코드 분할
manualChunks(id) {
if (id.includes("node_modules")) {
if (
id.includes("react") ||
id.includes("react-dom") ||
id.includes("react-router-dom")
) {
return "react-vendor"; // 리액트 관련 코어
}
return "vendor"; // 기타 라이브러리
}
},
},
},
},
};
});