Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
80 changes: 76 additions & 4 deletions src/lib/axiosInstance.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,20 @@
import axios from "axios";
import axios, { type AxiosRequestConfig } from "axios";

export const axiosInstance = axios.create({
import useAuthStore from "@/store/useAuthStore";

const axiosConfig: AxiosRequestConfig = {
baseURL: import.meta.env.VITE_API_BASE_URL,
withCredentials: true,
headers: {
"Content-Type": "application/json",
},
});
};

// 일반 API 요청용
export const axiosInstance = axios.create(axiosConfig);

// 토큰 재발급 전용
export const authInstance = axios.create(axiosConfig);

axiosInstance.interceptors.request.use(
(config) => {
Expand All @@ -22,11 +30,75 @@ 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 authInstance.post("/api/auth/reissue");

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"; // 기타 라이브러리
}
},
},
},
},
};
});