-
Notifications
You must be signed in to change notification settings - Fork 0
[FEAT] 프록시 설정 & 리프레시 토큰 인터셉터 추가 #222
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
e122f47
8174c8f
332f594
43c12fd
34a9177
fe69125
b55bb42
c1942f2
c7312f5
887750a
a110267
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -1,16 +1,24 @@ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import axios from "axios"; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { useLocalStorage } from "../../hooks/use-local-storage"; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { LOCAL_STORAGE_KEY } from "../../constants/key"; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { postLogout, postReissue } from "../auth"; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import toast from "react-hot-toast"; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const isDev = import.meta.env.DEV; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const BASE_URL = isDev ? "" : import.meta.env.VITE_API_BASE_URL; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
| export const axiosInstance = axios.create({ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| baseURL: import.meta.env.VITE_API_BASE_URL, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| baseURL: BASE_URL, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| headers: { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "Content-Type": "application/json", | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // 요청 인터셉터: 모든 요청 전에 accessToken을 Authorization 헤더에 추가 | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| axiosInstance.interceptors.request.use( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| (config) => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const { getItem } = useLocalStorage(LOCAL_STORAGE_KEY.accessToken); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| let accessToken = getItem(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const accessToken = getItem(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // accessToken이 존재하면 Authorization 헤더에 Bearer 토큰 형식으로 추가한다 | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (accessToken) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -24,3 +32,43 @@ axiosInstance.interceptors.request.use( | |||||||||||||||||||||||||||||||||||||||||||||||||||||
| // 요청 인터셉터가 실패하면, 에러 뿜음 | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| (error) => Promise.reject(error), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // 응답 인터셉터 | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| axiosInstance.interceptors.response.use( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| (response) => response, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| async (error) => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const originalRequest = error.config; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // 401에러 & 재시도 X | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (error.response?.status === 401 && !originalRequest._retry) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| originalRequest._retry = true; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
| try { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const reissueRes = await postReissue(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
coderabbitai[bot] marked this conversation as resolved.
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const { setItem } = useLocalStorage(LOCAL_STORAGE_KEY.accessToken); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| setItem(reissueRes.result.accessToken); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| originalRequest.headers.Authorization = `Bearer ${reissueRes.result.accessToken}`; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (reissueRes.result.needAgreement) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // 약관동의 안 돼있을 경우 약관동의로 리다이렉트 | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| window.location.href = "/agreement"; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return Promise.reject(new Error("Agreement required")); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return axiosInstance(originalRequest); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+46
to
+57
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 재발급된 accessToken이 localStorage에 저장되지 않음 - 중요 버그 토큰 재발급 성공 시 새
🐛 토큰 저장 로직 추가 제안 try {
const reissueRes = await postReissue();
+ const { setItem } = useLocalStorage(LOCAL_STORAGE_KEY.accessToken);
+ setItem(reissueRes.result.accessToken);
originalRequest.headers.Authorization = `Bearer ${reissueRes.result.accessToken}`;
if (reissueRes.result.needAgreement) {📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents
coderabbitai[bot] marked this conversation as resolved.
Comment on lines
+43
to
+57
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 동시 401 응답 시 레이스 컨디션 발생 가능
🔒 재발급 요청 큐잉 패턴 제안+let isRefreshing = false;
+let failedQueue: Array<{
+ resolve: (token: string) => void;
+ reject: (error: unknown) => void;
+}> = [];
+
+const processQueue = (error: unknown, token: string | null = null) => {
+ failedQueue.forEach((prom) => {
+ if (error) {
+ prom.reject(error);
+ } else {
+ prom.resolve(token!);
+ }
+ });
+ failedQueue = [];
+};
+
axiosInstance.interceptors.response.use(
(response) => response,
async (error) => {
const originalRequest = error.config;
if (error.response?.status === 401 && !originalRequest._retry) {
+ if (isRefreshing) {
+ return new Promise((resolve, reject) => {
+ failedQueue.push({ resolve, reject });
+ }).then((token) => {
+ originalRequest.headers.Authorization = `Bearer ${token}`;
+ return axiosInstance(originalRequest);
+ });
+ }
+
originalRequest._retry = true;
+ isRefreshing = true;
try {
const reissueRes = await postReissue();
- // ... token storage and retry
+ const { setItem } = useLocalStorage(LOCAL_STORAGE_KEY.accessToken);
+ setItem(reissueRes.result.accessToken);
+ processQueue(null, reissueRes.result.accessToken);
+ // ... rest of success handling
} catch (refreshError) {
+ processQueue(refreshError, null);
// ... error handling
+ } finally {
+ isRefreshing = false;
}
}🧰 Tools🪛 Biome (2.4.4)[error] 50-50: This hook is being called conditionally, but all hooks must be called in the exact same order in every component render. (lint/correctness/useHookAtTopLevel) 🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } catch (refreshError) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // 리프레시 토큰 만료 | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| toast.error("세션이 만료되었습니다. 다시 로그인해주세요."); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
| try { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| await postLogout(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } catch (logoutError) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| toast.error("로그아웃 API 호출 실패"); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } finally { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| localStorage.removeItem(LOCAL_STORAGE_KEY.accessToken); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| window.location.href = "/login"; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+58
to
+70
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 사용되지 않는 에러 변수 수정 필요
🔧 언더스코어 접두사 사용 또는 생략- } catch (refreshError) {
+ } catch (_refreshError) {
// 리프레시 토큰 만료
toast.error("세션이 만료되었습니다. 다시 로그인해주세요.");
try {
await postLogout();
- } catch (logoutError) {
+ } catch {
toast.error("로그아웃 API 호출 실패");
} finally {📝 Committable suggestion
Suggested change
🧰 Tools🪛 ESLint[error] 56-56: 'refreshError' is defined but never used. ( [error] 62-62: 'logoutError' is defined but never used. ( 🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return Promise.reject(error); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
This file was deleted.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,8 +1,21 @@ | ||
| import { defineConfig } from "vite"; | ||
| import { defineConfig, loadEnv } from "vite"; | ||
| import react from "@vitejs/plugin-react"; | ||
| import tailwindcss from "@tailwindcss/vite"; | ||
|
|
||
| // https://vite.dev/config/ | ||
| export default defineConfig({ | ||
| plugins: [react(), tailwindcss()], | ||
| export default defineConfig(({ mode }) => { | ||
| const env = loadEnv(mode, process.cwd(), ""); | ||
|
|
||
| return { | ||
| plugins: [react(), tailwindcss()], | ||
| server: { | ||
| proxy: { | ||
| // '/api'로 시작하는 모든 요청을 프록시가 가로챔 | ||
| "/api": { | ||
| target: env.VITE_API_BASE_URL, | ||
| changeOrigin: true, | ||
| }, | ||
| }, | ||
| }, | ||
| }; | ||
|
coderabbitai[bot] marked this conversation as resolved.
|
||
| }); | ||
Uh oh!
There was an error while loading. Please reload this page.