[FEAT] 프록시 설정 & 리프레시 토큰 인터셉터 추가 - #222
Conversation
[FEAT] 리프레시 토큰 인터셉터 추가& 로그아웃 로직
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
Note Reviews pausedIt looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
📝 WalkthroughWalkthrough환경별 프록시 설정이 도입되고 axios 인스턴스에 기본 헤더·withCredentials 및 리프레시 토큰 응답 인터셉터가 추가되었습니다. 인증 관련 호출 형식 일부가 변경되었고, 회원가입 페이지와 관련 라우트가 삭제되었습니다. Changes
Sequence DiagramsequenceDiagram
participant Client as 클라이언트
participant Axios as axiosInstance
participant API as API서버
participant Auth as 인증엔드포인트
Client->>Axios: 요청 (Authorization 헤더 포함)
Axios->>API: 실제 HTTP 요청
API-->>Axios: 401 Unauthorized
Axios->>Auth: postReissue({ withCredentials: true })
alt 갱신 성공
Auth-->>Axios: 새 액세스 토큰
Axios->>Axios: 로컬 토큰 갱신 및 Authorization 설정
Axios->>API: 원래 요청 재시도
API-->>Axios: 200 OK
Axios-->>Client: 응답 전달
else needAgreement 필요
Auth-->>Axios: { needAgreement: true }
Axios-->>Client: /agreement로 리다이렉트
else 갱신 실패
Axios->>Auth: postLogout({ withCredentials: true })
Auth-->>Axios: 로그아웃 완료
Axios-->>Client: 세션만료 토스트 + /login으로 리다이렉트
end
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 minutes Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 5
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@src/api/axios/axios.ts`:
- Around line 56-68: The catch blocks in src/api/axios/axios.ts declare unused
error variables refreshError and logoutError causing ESLint errors; update the
catches to either omit the identifier (use plain `catch { ... }`) or rename to a
prefixed unused variable like `_refreshError` and `_logoutError` so the linter
recognizes them as intentionally unused, making the changes in the block
handling the refresh flow (the catch after the token refresh attempt) and the
nested catch around postLogout().
- Around line 51-55: When reissueRes.result.needAgreement is true, stop the
interceptor from retrying the original request after assigning
window.location.href; change the flow in the interceptor (around reissueRes,
needAgreement, originalRequest, axiosInstance) to perform the redirect and then
immediately return a rejected Promise (e.g., Promise.reject(new
Error('redirect-to-agreement'))) or otherwise early-return so
axiosInstance(originalRequest) is not called; ensure the rejection provides
clear context for callers so the network retry is prevented.
- Around line 47-55: When handling a successful token reissue in the postReissue
flow, you're only updating originalRequest.headers.Authorization and not
persisting the new accessToken, which causes subsequent requests (and the
request interceptor that reads localStorage) to continue using the old token;
modify the post-reissue branch (where postReissue() is called and
originalRequest is updated) to save reissueRes.result.accessToken into the same
storage the request interceptor reads (e.g., localStorage.setItem with the
existing access token key) and also update any in-memory or axios default header
(e.g., axiosInstance.defaults.headers.Authorization) so future requests use the
new token and avoid repeated 401→reissue loops.
- Line 48: postReissue and postLogout call raw axios without including cookies,
so refreshToken stored in an HttpOnly cookie isn't sent and reissue handling
fails; update the implementations of postReissue and postLogout (in
src/api/auth.ts) to either call the shared axiosInstance or pass {
withCredentials: true } as the third argument to axios.post so cookies are
included (refer to the postReissue and postLogout function names and the usage
site where reissueRes = await postReissue()).
In `@vite.config.ts`:
- Around line 6-20: postReissue and postLogout currently call raw axios.post and
bypass the axiosInstance which has withCredentials:true, causing cookie-based
refresh/logout to fail; update postReissue and postLogout in src/api/auth.ts to
either call the shared axiosInstance (e.g., axiosInstance.post) or pass {
withCredentials: true } in the axios.post options so cookies are sent
(alternatively read the refresh token from localStorage and include it in the
request body if you intentionally avoid cookies); ensure the changes reference
the postReissue and postLogout functions and the axiosInstance symbol so the
credential behavior is consistent with your response interceptor.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: ASSERTIVE
Plan: Pro
Run ID: d5248f06-0813-4e65-a4ca-f5142d5f72fd
📒 Files selected for processing (5)
src/api/auth.tssrc/api/axios/axios.tssrc/pages/auth/signup-page.tsxsrc/routes/index.tsxvite.config.ts
💤 Files with no reviewable changes (3)
- src/pages/auth/signup-page.tsx
- src/api/auth.ts
- src/routes/index.tsx
| try { | ||
| const reissueRes = await postReissue(); | ||
|
|
||
| originalRequest.headers.Authorization = `Bearer ${reissueRes.result.accessToken}`; | ||
| if (reissueRes.result.needAgreement) { | ||
| // 약관동의 안 돼있을 경우 약관동의로 리다이렉트 | ||
| window.location.href = "/agreement"; | ||
| } | ||
| return axiosInstance(originalRequest); |
There was a problem hiding this comment.
재발급된 accessToken이 localStorage에 저장되지 않음 - 중요 버그
토큰 재발급 성공 시 새 accessToken을 originalRequest.headers에만 설정하고 localStorage에 저장하지 않습니다. 이로 인해:
- 해당 요청은 성공하지만, 다음 요청 시 request interceptor(line 21)가 여전히 이전/빈 토큰을 읽음
- 매 요청마다 401 → reissue 반복 또는 무한 루프 발생 가능
🐛 토큰 저장 로직 추가 제안
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
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| try { | |
| const reissueRes = await postReissue(); | |
| originalRequest.headers.Authorization = `Bearer ${reissueRes.result.accessToken}`; | |
| if (reissueRes.result.needAgreement) { | |
| // 약관동의 안 돼있을 경우 약관동의로 리다이렉트 | |
| window.location.href = "/agreement"; | |
| } | |
| return axiosInstance(originalRequest); | |
| try { | |
| const reissueRes = await postReissue(); | |
| localStorage.setItem(LOCAL_STORAGE_KEY.accessToken, reissueRes.result.accessToken); | |
| originalRequest.headers.Authorization = `Bearer ${reissueRes.result.accessToken}`; | |
| if (reissueRes.result.needAgreement) { | |
| // 약관동의 안 돼있을 경우 약관동의로 리다이렉트 | |
| window.location.href = "/agreement"; | |
| } | |
| return axiosInstance(originalRequest); |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/api/axios/axios.ts` around lines 47 - 55, When handling a successful
token reissue in the postReissue flow, you're only updating
originalRequest.headers.Authorization and not persisting the new accessToken,
which causes subsequent requests (and the request interceptor that reads
localStorage) to continue using the old token; modify the post-reissue branch
(where postReissue() is called and originalRequest is updated) to save
reissueRes.result.accessToken into the same storage the request interceptor
reads (e.g., localStorage.setItem with the existing access token key) and also
update any in-memory or axios default header (e.g.,
axiosInstance.defaults.headers.Authorization) so future requests use the new
token and avoid repeated 401→reissue loops.
| } catch (refreshError) { | ||
| // 리프레시 토큰 만료 | ||
| toast.error("세션이 만료되었습니다. 다시 로그인해주세요."); | ||
|
|
||
| try { | ||
| await postLogout(); | ||
| } catch (logoutError) { | ||
| toast.error("로그아웃 API 호출 실패"); | ||
| } finally { | ||
| localStorage.removeItem(LOCAL_STORAGE_KEY.accessToken); | ||
| window.location.href = "/login"; | ||
| } | ||
| } |
There was a problem hiding this comment.
사용되지 않는 에러 변수 수정 필요
refreshError와 logoutError가 정의되었지만 사용되지 않아 ESLint 오류가 발생합니다.
🔧 언더스코어 접두사 사용 또는 생략
- } catch (refreshError) {
+ } catch (_refreshError) {
// 리프레시 토큰 만료
toast.error("세션이 만료되었습니다. 다시 로그인해주세요.");
try {
await postLogout();
- } catch (logoutError) {
+ } catch {
toast.error("로그아웃 API 호출 실패");
} finally {📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| } catch (refreshError) { | |
| // 리프레시 토큰 만료 | |
| toast.error("세션이 만료되었습니다. 다시 로그인해주세요."); | |
| try { | |
| await postLogout(); | |
| } catch (logoutError) { | |
| toast.error("로그아웃 API 호출 실패"); | |
| } finally { | |
| localStorage.removeItem(LOCAL_STORAGE_KEY.accessToken); | |
| window.location.href = "/login"; | |
| } | |
| } | |
| } catch (_refreshError) { | |
| // 리프레시 토큰 만료 | |
| toast.error("세션이 만료되었습니다. 다시 로그인해주세요."); | |
| try { | |
| await postLogout(); | |
| } catch { | |
| toast.error("로그아웃 API 호출 실패"); | |
| } finally { | |
| localStorage.removeItem(LOCAL_STORAGE_KEY.accessToken); | |
| window.location.href = "/login"; | |
| } | |
| } |
🧰 Tools
🪛 ESLint
[error] 56-56: 'refreshError' is defined but never used.
(@typescript-eslint/no-unused-vars)
[error] 62-62: 'logoutError' is defined but never used.
(@typescript-eslint/no-unused-vars)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/api/axios/axios.ts` around lines 56 - 68, The catch blocks in
src/api/axios/axios.ts declare unused error variables refreshError and
logoutError causing ESLint errors; update the catches to either omit the
identifier (use plain `catch { ... }`) or rename to a prefixed unused variable
like `_refreshError` and `_logoutError` so the linter recognizes them as
intentionally unused, making the changes in the block handling the refresh flow
(the catch after the token refresh attempt) and the nested catch around
postLogout().
There was a problem hiding this comment.
Actionable comments posted: 1
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
src/api/axios/axios.ts (1)
21-22: 🧹 Nitpick | 🔵 Trivial
useLocalStorage네이밍이 React 훅처럼 보이지만 실제 훅이 아님Biome에서 조건부 훅 호출 경고가 발생하는 이유는
useLocalStorage가 React 훅 네이밍 컨벤션(use*)을 따르기 때문입니다. 실제로는 일반 유틸리티 함수이므로 혼란을 줄 수 있습니다.향후
createLocalStorage또는localStorageUtils와 같은 이름으로 변경하면 린트 경고도 해결되고 의도가 명확해집니다.Also applies to: 50-51
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/api/axios/axios.ts` around lines 21 - 22, The helper named useLocalStorage is misleading as it looks like a React hook; rename the utility (e.g., createLocalStorage or localStorageUtils) and update all call sites: replace useLocalStorage(LOCAL_STORAGE_KEY.accessToken) and destructuring of getItem (and any setItem) with the new name, update the import/export where useLocalStorage is defined, and adjust other occurrences (lines referenced around 50-51) so accessToken = getItem() keeps working under the new utility name.
♻️ Duplicate comments (1)
src/api/axios/axios.ts (1)
50-51:⚠️ Potential issue | 🔴 Critical
useLocalStorage에 잘못된 키가 전달됨 - 치명적 버그
useLocalStorage의 인자로LOCAL_STORAGE_KEY.accessToken(키) 대신reissueRes.result.accessToken(토큰 값)을 전달하고 있습니다. 이로 인해:
- 토큰 값이 localStorage의 키로 사용되어 잘못된 위치에 저장됨
- 다음 요청 시 request interceptor(line 21)가
LOCAL_STORAGE_KEY.accessToken키에서 토큰을 찾지 못함- 매 요청마다 401 → reissue 반복 또는 인증 실패 발생
🐛 올바른 키 사용 수정
- const { setItem } = useLocalStorage(reissueRes.result.accessToken); + const { setItem } = useLocalStorage(LOCAL_STORAGE_KEY.accessToken); setItem(reissueRes.result.accessToken);🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/api/axios/axios.ts` around lines 50 - 51, The code calls useLocalStorage with the token value instead of the storage key; change the call so useLocalStorage receives LOCAL_STORAGE_KEY.accessToken (not reissueRes.result.accessToken), then call setItem(reissueRes.result.accessToken) to persist the token; update any nearby usage that reads from localStorage (the request interceptor that expects LOCAL_STORAGE_KEY.accessToken) to rely on that key so token retrieval and reissue flow work correctly.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@src/api/axios/axios.ts`:
- Around line 44-58: Multiple concurrent 401 responses can trigger duplicate
postReissue() calls because _retry is per-request; fix by introducing a shared
reissuePromise (module-level) used in the axios response interceptor so only the
first 401 caller invokes postReissue() and other handlers await that promise,
then update local storage via useLocalStorage(reissueRes.result.accessToken),
set Authorization on originalRequest, handle needAgreement redirect, and finally
return axiosInstance(originalRequest); modify the logic around
originalRequest._retry, postReissue, axiosInstance, and useLocalStorage in the
interceptor to use this queueing/shared-promise pattern.
---
Outside diff comments:
In `@src/api/axios/axios.ts`:
- Around line 21-22: The helper named useLocalStorage is misleading as it looks
like a React hook; rename the utility (e.g., createLocalStorage or
localStorageUtils) and update all call sites: replace
useLocalStorage(LOCAL_STORAGE_KEY.accessToken) and destructuring of getItem (and
any setItem) with the new name, update the import/export where useLocalStorage
is defined, and adjust other occurrences (lines referenced around 50-51) so
accessToken = getItem() keeps working under the new utility name.
---
Duplicate comments:
In `@src/api/axios/axios.ts`:
- Around line 50-51: The code calls useLocalStorage with the token value instead
of the storage key; change the call so useLocalStorage receives
LOCAL_STORAGE_KEY.accessToken (not reissueRes.result.accessToken), then call
setItem(reissueRes.result.accessToken) to persist the token; update any nearby
usage that reads from localStorage (the request interceptor that expects
LOCAL_STORAGE_KEY.accessToken) to rely on that key so token retrieval and
reissue flow work correctly.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: ASSERTIVE
Plan: Pro
Run ID: c35377ab-3f97-413e-88ef-ba4a36ad1041
📒 Files selected for processing (2)
src/api/auth.tssrc/api/axios/axios.ts
| if (error.response?.status === 401 && !originalRequest._retry) { | ||
| originalRequest._retry = true; | ||
|
|
||
| try { | ||
| const reissueRes = await postReissue(); | ||
|
|
||
| const { setItem } = useLocalStorage(reissueRes.result.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); |
There was a problem hiding this comment.
동시 401 응답 시 레이스 컨디션 발생 가능
_retry 플래그가 요청별로 설정되므로 여러 요청이 동시에 401을 받으면 postReissue()가 중복 호출됩니다. 이로 인해:
- 서버에 불필요한 토큰 재발급 요청 다수 발생
- 토큰 저장 시점에 따라 일부 요청이 이전 토큰으로 재시도될 수 있음
🔒 재발급 요청 큐잉 패턴 제안
+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
Verify each finding against the current code and only fix it if needed.
In `@src/api/axios/axios.ts` around lines 44 - 58, Multiple concurrent 401
responses can trigger duplicate postReissue() calls because _retry is
per-request; fix by introducing a shared reissuePromise (module-level) used in
the axios response interceptor so only the first 401 caller invokes
postReissue() and other handlers await that promise, then update local storage
via useLocalStorage(reissueRes.result.accessToken), set Authorization on
originalRequest, handle needAgreement redirect, and finally return
axiosInstance(originalRequest); modify the logic around originalRequest._retry,
postReissue, axiosInstance, and useLocalStorage in the interceptor to use this
queueing/shared-promise pattern.
shooukie
left a comment
There was a problem hiding this comment.
토큰 재발급 이후 약관 동의 여부까지 체크하는 로직 너무 좋습니다! 컨벤션도 잘 지켜진 것 같아요 고생하셨습니다!!
| try { | ||
| const reissueRes = await postReissue(); | ||
|
|
||
| const { setItem } = useLocalStorage(reissueRes.result.accessToken); |
There was a problem hiding this comment.
로컬스토리지의 accessToken 값을 갱신해줘야 해서 reissueRes.result.accessToken 말고 LOCAL_STORAGE_KEY.accessToken 이 들어가야 할 것 같습니다!!
There was a problem hiding this comment.
로컬스토리지의 권한없는 accessToken 값을 postReissue에서 받아온 새로운 accessToken으로 갱신시키려고reissueRes.result.accessToken으로 넣어준건데 혹시 내가 잘못 이해하고 있는건가?
There was a problem hiding this comment.
useLocalStorage() 괄호 안에는 localStorage에 들어갈 key값, setItem()괄호 안에는 실제 저장할 value값이 들어가는 방식이야
현재 권한 없는 기존 accessToken이 LOCAL_STORAGE_KEY.accessToken 키에 저장되어 있으니 이 키 값의 setItem()을 불러와서 밑에 줄에서 setItem(reissueRes.result.accessToken); 로 value값을 교체해서 갱신!
지금 상태로 하면 key값도 실제 토큰, value값도 실제 토큰이 돼
그래서 LOCAL_STORAGE_KEY.accessToken이 들어가야 할 것 같아
There was a problem hiding this comment.
이부분은 pr전 기존 코드이긴 하지만, 이번에 파일 수정하는 김에 const로 바꿔도 괜찮을 것 같아요! 여기서 선언한 accessToken이 재할당되지 않는 것 같아서요!!
|
@coderabbitai 내가 헷갈려서 그러는데 axiosInstance안에서 postReissue로 재발급을 하는데 이때 401에러가 나면 무한루프가 일어날 수 있어? postReissue 안에서도 axiosInstance를 사용해서 무한루프가 나는지 아니면 괜찮은지 알려줘!! |
|
Tip For best results, initiate chat on the files or code changes. 🧩 Analysis chain🏁 Script executed: Length of output: 2695
🔍 문제 분석현재 코드에서 if (error.response?.status === 401 && !originalRequest._retry) {
originalRequest._retry = true; // 여기서 _retry를 세팅
// ...
await postReissue(); // 이 안에서 axiosInstance를 다시 사용!
}
무한루프 시나리오는 다음과 같습니다:
✅ 해결 방법방법 1 (권장): 인터셉터 내부에서 reissue/logout URL을 건너뛰기 if (
error.response?.status === 401 &&
!originalRequest._retry &&
!originalRequest.url?.includes("/api/auth/reissue") && // 추가
!originalRequest.url?.includes("/api/auth/logout") // 추가
) {방법 2: reissue/logout에 별도의 순수 axios 인스턴스 사용 // auth.ts
import axios from "axios"; // axiosInstance 대신 순수 axios 사용
const plainAxios = axios.create({
baseURL: ...,
withCredentials: true,
});
export const postReissue = async (): Promise<ReissueResponse> => {
const { data } = await plainAxios.post<ReissueResponse>("/api/auth/reissue");
return data;
};방법 3: 모듈 레벨의 let isRefreshing = false;
// 인터셉터 내부
if (error.response?.status === 401 && !originalRequest._retry && !isRefreshing) {
originalRequest._retry = true;
isRefreshing = true;
try {
await postReissue();
} finally {
isRefreshing = false;
}
}요약하면, 현재 코드는 |
|
코드레빗이 추천한 방법1도 괜찮은 것 같고 아니면 인스턴스를 두 개로 나누어도 괜찮을 것 같아요! |
사용할 일이 별로 없을 것 같아서 인스턴스를 두 개로 나누지는 않았고, 그냥 기존 코드대로 기존 axios를 사용하는 방법으로 다시 고쳤어 |
There was a problem hiding this comment.
Actionable comments posted: 2
♻️ Duplicate comments (3)
src/api/axios/axios.ts (3)
44-46:⚠️ Potential issue | 🟠 Major동시 401 응답 시 중복 토큰 재발급 요청 발생 가능
_retry플래그가 요청별로 설정되므로 여러 요청이 동시에 401을 받으면postReissue()가 중복 호출됩니다. 이전 리뷰에서 제안된isRefreshing플래그와 큐잉 패턴 적용을 권장합니다.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/api/axios/axios.ts` around lines 44 - 46, The current axios interceptor sets originalRequest._retry per-request which allows multiple simultaneous 401 responses to each trigger postReissue(); introduce a shared isRefreshing boolean and a request queue (e.g., array of pending promise resolvers) so that when a 401 is received and isRefreshing is true you enqueue the request and wait for the refreshed token, and when isRefreshing is false you set isRefreshing = true, call postReissue(), resolve pending queued requests with the new token, then set isRefreshing = false; update the interceptor logic that references originalRequest._retry and postReissue() to use the shared isRefreshing + queue pattern to ensure only one postReissue() runs at a time and other requests wait for its result.
50-51:⚠️ Potential issue | 🔴 Critical
useLocalStorage에 토큰 키 대신 토큰 값이 전달됨 — 토큰 저장 실패
useLocalStorage훅은 localStorage의 키 문자열을 인자로 받습니다. 현재 코드는 토큰 값(reissueRes.result.accessToken)을 키로 전달하고 있어, 실제accessToken키에 토큰이 저장되지 않습니다.결과적으로 다음 요청 시 request 인터셉터(Line 21)가 빈 토큰을 읽어 매 요청마다 401 → reissue가 반복됩니다.
🐛 수정 제안
- const { setItem } = useLocalStorage(reissueRes.result.accessToken); + const { setItem } = useLocalStorage(LOCAL_STORAGE_KEY.accessToken); setItem(reissueRes.result.accessToken);🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/api/axios/axios.ts` around lines 50 - 51, 현재 useLocalStorage 훅에 실제 토큰 값(reissueRes.result.accessToken)을 키로 넘기고 있어 로컬에 "accessToken" 키로 저장되지 않습니다; 수정은 useLocalStorage 호출에서 반드시 키 문자열 ("accessToken" 또는 프로젝트에서 사용하는 키명)을 첫 인자로 전달하고, 이후 setItem(reissueRes.result.accessToken)로 토큰 값을 저장하도록 변경하세요 (참조: useLocalStorage, setItem, reissueRes.result.accessToken).
59-71:⚠️ Potential issue | 🟡 Minor사용되지 않는 에러 변수로 인한 ESLint 오류
refreshError와logoutError가 선언되었지만 사용되지 않아 ESLint 오류가 발생합니다. 언더스코어 접두사를 사용하거나 변수를 생략하세요.🔧 수정 제안
- } catch (refreshError) { + } catch (_refreshError) { // 리프레시 토큰 만료 toast.error("세션이 만료되었습니다. 다시 로그인해주세요."); try { await postLogout(); - } catch (logoutError) { + } catch { toast.error("로그아웃 API 호출 실패"); } finally {🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/api/axios/axios.ts` around lines 59 - 71, The catch blocks declare unused error variables causing ESLint errors; update the catch clauses in the axios token refresh flow to either omit the error identifier (use "catch { ... }") or rename them with an underscore prefix (e.g., _refreshError, _logoutError) so ESLint ignores them, keeping the existing logic that calls postLogout(), removes LOCAL_STORAGE_KEY.accessToken, shows toast messages, and redirects to "/login".
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@src/api/auth.ts`:
- Around line 14-19: postLogout is passing withCredentials in the request body
(same bug as postReissue); move withCredentials out of the payload and pass it
as the axios request config (third argument) for the POST call in postLogout so
the cookie credentials are sent correctly while the request body remains the
intended payload.
- Around line 7-9: The axios.post calls are passing { withCredentials: true } as
the request body instead of the config, so cookies (refreshToken) aren't sent;
locate any axios.post usage and change the call signature to axios.post(url,
data, { withCredentials: true }) (or axios.post(url, null, { withCredentials:
true }) if no body) so withCredentials is in the config argument and the
refreshToken cookie is included; update all occurrences referencing axios.post
and the withCredentials object accordingly.
---
Duplicate comments:
In `@src/api/axios/axios.ts`:
- Around line 44-46: The current axios interceptor sets originalRequest._retry
per-request which allows multiple simultaneous 401 responses to each trigger
postReissue(); introduce a shared isRefreshing boolean and a request queue
(e.g., array of pending promise resolvers) so that when a 401 is received and
isRefreshing is true you enqueue the request and wait for the refreshed token,
and when isRefreshing is false you set isRefreshing = true, call postReissue(),
resolve pending queued requests with the new token, then set isRefreshing =
false; update the interceptor logic that references originalRequest._retry and
postReissue() to use the shared isRefreshing + queue pattern to ensure only one
postReissue() runs at a time and other requests wait for its result.
- Around line 50-51: 현재 useLocalStorage 훅에 실제 토큰
값(reissueRes.result.accessToken)을 키로 넘기고 있어 로컬에 "accessToken" 키로 저장되지 않습니다; 수정은
useLocalStorage 호출에서 반드시 키 문자열 ("accessToken" 또는 프로젝트에서 사용하는 키명)을 첫 인자로 전달하고, 이후
setItem(reissueRes.result.accessToken)로 토큰 값을 저장하도록 변경하세요 (참조: useLocalStorage,
setItem, reissueRes.result.accessToken).
- Around line 59-71: The catch blocks declare unused error variables causing
ESLint errors; update the catch clauses in the axios token refresh flow to
either omit the error identifier (use "catch { ... }") or rename them with an
underscore prefix (e.g., _refreshError, _logoutError) so ESLint ignores them,
keeping the existing logic that calls postLogout(), removes
LOCAL_STORAGE_KEY.accessToken, shows toast messages, and redirects to "/login".
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: ASSERTIVE
Plan: Pro
Run ID: b6bf991d-899c-48e2-bbe6-76a7306e9254
📒 Files selected for processing (2)
src/api/auth.tssrc/api/axios/axios.ts
There was a problem hiding this comment.
♻️ Duplicate comments (3)
src/api/axios/axios.ts (3)
58-64:⚠️ Potential issue | 🟡 Minor사용하지 않는
catch변수 때문에 린트가 깨집니다.
refreshError,logoutError를 읽지 않아서 현재 ESLint 에러가 납니다.catch {}로 바꾸거나_refreshError처럼 의도를 명시하세요.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/api/axios/axios.ts` around lines 58 - 64, The catch blocks in the token refresh flow use unused variables refreshError and logoutError causing ESLint errors; update the catch signatures in the axios token-refresh logic (the try/catch that calls postLogout) to either use anonymous catches (catch {}) or rename to intentionally unused identifiers (catch (_refreshError) and catch (_logoutError)) so the linter stops reporting unused variables while preserving the existing error-handling behavior.
49-50:⚠️ Potential issue | 🔴 Critical재발급 토큰을 잘못된 localStorage 키에 저장하고 있습니다.
Line 49는
useLocalStorage(reissueRes.result.accessToken)로 실제 토큰 값을 key로 사용합니다. 이 상태면 요청 인터셉터가 읽는LOCAL_STORAGE_KEY.accessToken은 갱신되지 않아, 다음 요청들이 계속 이전 토큰으로 나갑니다.수정 예시
- const { setItem } = useLocalStorage(reissueRes.result.accessToken); + const { setItem } = useLocalStorage(LOCAL_STORAGE_KEY.accessToken); setItem(reissueRes.result.accessToken);🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/api/axios/axios.ts` around lines 49 - 50, The code is storing the new token using the token value as the localStorage key; change the useLocalStorage call to use the correct key (LOCAL_STORAGE_KEY.accessToken) and then call setItem(reissueRes.result.accessToken) to store the token value; specifically update the call site that currently does useLocalStorage(reissueRes.result.accessToken) so it becomes useLocalStorage(LOCAL_STORAGE_KEY.accessToken) and then invoke the returned setItem with reissueRes.result.accessToken so the request interceptor reads the updated token.
43-57:⚠️ Potential issue | 🟠 Major동시 401 응답에서 재발급이 중복 호출됩니다.
_retry는 요청별 플래그라서 여러 요청이 동시에 401을 받으면 각 요청이 모두postReissue()를 호출합니다. refresh token 회전 정책이 있거나 서버가 재발급을 직렬화하지 않으면 일부 요청이 불필요하게 로그아웃 경로로 떨어질 수 있으니, 모듈 레벨reissuePromise/queue로 단일 재발급만 수행해야 합니다.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/api/axios/axios.ts` around lines 43 - 57, The 401 handler currently calls postReissue() per-request because _retry is per-request; introduce a module-level reissuePromise (e.g., let reissuePromise: Promise<any> | null = null) and use it to serialize refreshes: when encountering error.response?.status === 401 and !originalRequest._retry, set originalRequest._retry = true, then if reissuePromise is null assign reissuePromise = postReissue(); await reissuePromise for all concurrent requests, clear reissuePromise after resolution or rejection, then use the resolved reissueRes to call useLocalStorage(reissueRes.result.accessToken).setItem(...), update originalRequest.headers.Authorization, handle needAgreement by redirecting and rejecting, and finally return axiosInstance(originalRequest); ensure reissuePromise is cleared on errors so future attempts can retry.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Duplicate comments:
In `@src/api/axios/axios.ts`:
- Around line 58-64: The catch blocks in the token refresh flow use unused
variables refreshError and logoutError causing ESLint errors; update the catch
signatures in the axios token-refresh logic (the try/catch that calls
postLogout) to either use anonymous catches (catch {}) or rename to
intentionally unused identifiers (catch (_refreshError) and catch
(_logoutError)) so the linter stops reporting unused variables while preserving
the existing error-handling behavior.
- Around line 49-50: The code is storing the new token using the token value as
the localStorage key; change the useLocalStorage call to use the correct key
(LOCAL_STORAGE_KEY.accessToken) and then call
setItem(reissueRes.result.accessToken) to store the token value; specifically
update the call site that currently does
useLocalStorage(reissueRes.result.accessToken) so it becomes
useLocalStorage(LOCAL_STORAGE_KEY.accessToken) and then invoke the returned
setItem with reissueRes.result.accessToken so the request interceptor reads the
updated token.
- Around line 43-57: The 401 handler currently calls postReissue() per-request
because _retry is per-request; introduce a module-level reissuePromise (e.g.,
let reissuePromise: Promise<any> | null = null) and use it to serialize
refreshes: when encountering error.response?.status === 401 and
!originalRequest._retry, set originalRequest._retry = true, then if
reissuePromise is null assign reissuePromise = postReissue(); await
reissuePromise for all concurrent requests, clear reissuePromise after
resolution or rejection, then use the resolved reissueRes to call
useLocalStorage(reissueRes.result.accessToken).setItem(...), update
originalRequest.headers.Authorization, handle needAgreement by redirecting and
rejecting, and finally return axiosInstance(originalRequest); ensure
reissuePromise is cleared on errors so future attempts can retry.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: ASSERTIVE
Plan: Pro
Run ID: 4adbd25b-d6f9-4947-8518-bc7d6c597cf2
📒 Files selected for processing (1)
src/api/axios/axios.ts
PR 제목
[FEAT] 프록시 설정 & 리프레시 토큰 인터셉터 추가
PR을 한 이유
#️⃣연관된 이슈
📝작업 내용
💬리뷰 요구사항(선택)
Summary by CodeRabbit
새로운 기능
변경 사항