Skip to content

Conversation

@dain823
Copy link
Contributor

@dain823 dain823 commented Aug 7, 2025

📌 변경 사항 개요

Access Token 만료 시 Refresh Token으로 재발급하는 코드를 추가했습니다.

📝 상세 내용

401 응답을 받으면(액세스 토큰 만료)
_retry 플래그로 무한 재시도를 방지
refreshToken이 없으면 Error를 던지고
브라우저 기본 경고창으로 세션 만료 안내
별도 Axios 인스턴스(refreshFetcher)를 사용해 /auth/refresh 호출
성공 시 새 accessToken, refreshToken을 localStorage에 저장
원래 요청의 Authorization 헤더만 교체하고 재시도

🔗 관련 이슈

🖼️ 스크린샷(선택사항)

💡 참고 사항

Summary by CodeRabbit

  • 버그 수정

    • 만료된 인증 토큰으로 인해 발생하는 401 오류 시, 자동으로 토큰을 갱신하고 요청을 재시도하도록 개선되었습니다.
    • 세션이 만료되었을 때 사용자에게 알림이 표시됩니다.
  • 기타

    • 인증 관련 안정성이 향상되어, 로그인 세션 관리가 보다 원활해졌습니다.

@dain823 dain823 self-assigned this Aug 7, 2025
@dain823 dain823 added the ✨ 기능 추가 새로운 코드나 기능을 추가했어요~! label Aug 7, 2025
@dain823 dain823 linked an issue Aug 7, 2025 that may be closed by this pull request
@coderabbitai
Copy link

coderabbitai bot commented Aug 7, 2025

📝 Walkthrough

Walkthrough

src/libs/api.ts 파일에 액세스 토큰과 리프레시 토큰을 안전하게 가져오는 헬퍼 함수가 추가되고, 브라우저 환경에서만 localStorage에 접근하도록 변경되었습니다. 401 에러 발생 시 자동으로 토큰을 갱신하고 요청을 재시도하는 axios 응답 인터셉터가 도입되었습니다.

Changes

Cohort / File(s) Change Summary
API 및 토큰 갱신 로직 개선
src/libs/api.ts
Axios 타입 import 확장, 브라우저 환경 체크 및 토큰 안전 조회 헬퍼 추가, refreshFetcher 인스턴스 생성, 401 에러 시 토큰 자동 갱신 및 요청 재시도 로직 구현, 기존 중복 함수 제거

Sequence Diagram(s)

sequenceDiagram
    participant Client
    participant fetcher (Axios)
    participant refreshFetcher (Axios)
    participant localStorage
    participant API

    Client->>fetcher: API 요청 (access token 포함)
    fetcher->>API: 요청 전달
    API-->>fetcher: 401 Unauthorized (만료된 토큰)
    fetcher->>localStorage: refresh token 조회
    fetcher->>refreshFetcher: /auth/refresh 요청 (refresh token)
    refreshFetcher->>API: 토큰 갱신 요청
    API-->>refreshFetcher: 새 access/refresh token 반환
    refreshFetcher-->>fetcher: 새 토큰 전달
    fetcher->>localStorage: 새 토큰 저장
    fetcher->>API: 원래 요청을 새 access token으로 재시도
    API-->>fetcher: 정상 응답
    fetcher-->>Client: 응답 반환
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~15 minutes

Possibly related issues

  • [feat] 자동 token 갱신 #237: 401 에러 발생 시 토큰 자동 갱신 및 재시도 기능 도입이 이 이슈의 요구사항과 직접적으로 일치합니다.

Possibly related PRs

  • ✨ feat: interceptor 토큰 작업 #79: 두 PR 모두 fetcher 인스턴스에 401 에러 대응 로직을 추가하지만, 본 PR은 토큰 갱신 및 재시도까지 구현한 점에서 더 확장된 기능을 포함합니다.

Poem

🐇
브라우저에서 토큰을 살짝 꺼내
401이 오면 새로고침해
새 토큰 받아 다시 한 번
요청을 보내는 토끼의 손
로그인 풀림 걱정 마세요
자동 갱신, 이제 문제 없어요!
🥕

Note

⚡️ Unit Test Generation is now available in beta!

Learn more here, or try it out under "Finishing Touches" below.

✨ Finishing Touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feat/244-Token

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.

❤️ Share
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Explain this complex logic.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query. Examples:
    • @coderabbitai explain this code block.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read src/utils.ts and explain its main purpose.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.

Support

Need help? Create a ticket on our support page for assistance with any issues or questions.

CodeRabbit Commands (Invoked using PR comments)

  • @coderabbitai pause to pause the reviews on a PR.
  • @coderabbitai resume to resume the paused reviews.
  • @coderabbitai review to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
  • @coderabbitai full review to do a full review from scratch and review all the files again.
  • @coderabbitai summary to regenerate the summary of the PR.
  • @coderabbitai generate docstrings to generate docstrings for this PR.
  • @coderabbitai generate sequence diagram to generate a sequence diagram of the changes in this PR.
  • @coderabbitai generate unit tests to generate unit tests for this PR.
  • @coderabbitai resolve resolve all the CodeRabbit review comments.
  • @coderabbitai configuration to show the current CodeRabbit configuration for the repository.
  • @coderabbitai help to get help.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary or @coderabbitai 요약 to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

Documentation and Community

  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

@github-actions
Copy link

github-actions bot commented Aug 7, 2025

🧷 배포 미리보기: https://global-nomad-n01zmezg9-yun-jinwoos-projects.vercel.app
최종 업데이트: 2025년 08월 07일 13시 06분

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (3)
src/libs/api.ts (3)

16-22: 함수 스타일 일관성 개선 제안

두 토큰 가져오기 함수가 동일한 기능을 수행하지만 다른 스타일로 작성되어 있습니다. 일관성을 위해 통일하는 것이 좋겠습니다.

-const getAccessToken = (): string | null => {
-  if (typeof window === 'undefined') return null;
-  return localStorage.getItem('accessToken');
-};
+const getAccessToken = (): string | null =>
+  typeof window === 'undefined' ? null : localStorage.getItem('accessToken');

 const getRefreshToken = (): string | null =>
   typeof window === 'undefined' ? null : localStorage.getItem('refreshToken');

54-58: 리프레시 요청에 대한 타임아웃 재검토 필요

토큰 갱신은 중요한 작업이므로 3초 타임아웃이 너무 짧을 수 있습니다. 또한 공통 설정을 상수로 추출하면 유지보수가 쉬워집니다.

+const API_CONFIG = {
+  baseURL: process.env.NEXT_PUBLIC_API_BASE_URL,
+  headers: { 'Content-Type': 'application/json' },
+};
+
 const fetcher: AxiosInstance = axios.create({
-  baseURL: process.env.NEXT_PUBLIC_API_BASE_URL,
+  ...API_CONFIG,
   timeout: 3000,
-  headers: {
-    'Content-Type': 'application/json',
-  },
 });

 const refreshFetcher = axios.create({
-  baseURL: process.env.NEXT_PUBLIC_API_BASE_URL,
-  timeout: 3000,
-  headers: { 'Content-Type': 'application/json' },
+  ...API_CONFIG,
+  timeout: 5000, // 리프레시는 더 긴 타임아웃 허용
 });

16-22: 보안 강화를 위한 토큰 저장 방식 개선 권장

현재 localStorage를 사용한 토큰 저장은 XSS 공격에 취약합니다. 보안 강화를 위해 다음을 고려하세요:

  1. httpOnly 쿠키 사용: 리프레시 토큰은 httpOnly 쿠키로 저장하여 JavaScript 접근 차단
  2. 메모리 저장: 액세스 토큰은 메모리에만 저장하고 페이지 새로고침 시 리프레시로 재발급
  3. 토큰 암호화: localStorage 사용이 불가피하다면 최소한 암호화 적용

Also applies to: 83-84

📜 Review details

Configuration used: .coderabbit.yaml
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 306f1b9 and 9ab5d59.

📒 Files selected for processing (1)
  • src/libs/api.ts (3 hunks)
🧰 Additional context used
🧠 Learnings (4)
📓 Common learnings
Learnt from: two678
PR: Act-It-FE/global-nomad#122
File: src/hooks/useErrorHandler.ts:1-9
Timestamp: 2025-07-27T07:24:43.556Z
Learning: axios.isAxiosError()를 사용하여 Axios 에러를 타입 안전하게 체크하는 것이 권장되는 방법입니다. 이는 error.response?.data?.message 같은 속성에 안전하게 접근할 수 있게 해줍니다.
Learnt from: Moon-ju-young
PR: Act-It-FE/global-nomad#103
File: src/libs/MyActivities.ts:28-29
Timestamp: 2025-07-25T21:18:02.241Z
Learning: axios 에러 객체에서 HTTP 상태 코드에 접근할 때는 error.status가 아닌 error.response?.status를 사용해야 합니다. error.status는 axios의 표준 속성이 아니며 일반적으로 undefined입니다.
📚 Learning: when axios instance has a global default 'content-type': 'application/json' header, formdata uploads...
Learnt from: Moon-ju-young
PR: Act-It-FE/global-nomad#197
File: src/api/activitiesApi.ts:79-83
Timestamp: 2025-08-01T18:24:05.811Z
Learning: When axios instance has a global default 'Content-Type': 'application/json' header, FormData uploads require explicit 'Content-Type': 'multipart/form-data' header override to prevent 400 Bad Request errors. The automatic Content-Type setting by axios only works when no default Content-Type is configured globally.

Applied to files:

  • src/libs/api.ts
📚 Learning: axios.isaxioserror()를 사용하여 axios 에러를 타입 안전하게 체크하는 것이 권장되는 방법입니다. 이는 error.response?.data?.message 같은...
Learnt from: two678
PR: Act-It-FE/global-nomad#122
File: src/hooks/useErrorHandler.ts:1-9
Timestamp: 2025-07-27T07:24:43.556Z
Learning: axios.isAxiosError()를 사용하여 Axios 에러를 타입 안전하게 체크하는 것이 권장되는 방법입니다. 이는 error.response?.data?.message 같은 속성에 안전하게 접근할 수 있게 해줍니다.

Applied to files:

  • src/libs/api.ts
📚 Learning: axios 에러 객체에서 http 상태 코드에 접근할 때는 error.status가 아닌 error.response?.status를 사용해야 합니다. error.status는 ax...
Learnt from: Moon-ju-young
PR: Act-It-FE/global-nomad#103
File: src/libs/MyActivities.ts:28-29
Timestamp: 2025-07-25T21:18:02.241Z
Learning: axios 에러 객체에서 HTTP 상태 코드에 접근할 때는 error.status가 아닌 error.response?.status를 사용해야 합니다. error.status는 axios의 표준 속성이 아니며 일반적으로 undefined입니다.

Applied to files:

  • src/libs/api.ts

Comment on lines +73 to +94
try {
const refreshToken = getRefreshToken();
if (!refreshToken) throw new Error('No refresh token available');
window.alert('세션이 만료되었습니다. 다시 로그인해주세요.');

const { data } = await refreshFetcher.post<{
accessToken: string;
refreshToken: string;
}>('/auth/refresh', { refreshToken });

localStorage.setItem('accessToken', data.accessToken);
localStorage.setItem('refreshToken', data.refreshToken);

if (originalReq.headers) {
const headers = originalReq.headers as AxiosRequestHeaders;
headers['Authorization'] = `Bearer ${data.accessToken}`;
}
return fetcher(originalReq);
} catch (refreshError) {
console.warn('토큰 갱신 실패:', refreshError);
window.alert('실패했습니다.');
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

토큰 갱신 로직의 치명적인 문제점들

현재 구현에 여러 심각한 문제가 있습니다:

  1. 리프레시 시도 전에 세션 만료 알림이 표시되어 사용자 혼란 야기
  2. localStorage 직접 접근 시 브라우저 체크 누락 (SSR 환경 오류 가능)
  3. 리프레시 실패 시 에러를 throw하지 않아 원본 요청이 재시도됨
  4. 모호한 실패 메시지
 try {
   const refreshToken = getRefreshToken();
   if (!refreshToken) throw new Error('No refresh token available');
-  window.alert('세션이 만료되었습니다. 다시 로그인해주세요.');

   const { data } = await refreshFetcher.post<{
     accessToken: string;
     refreshToken: string;
   }>('/auth/refresh', { refreshToken });

-  localStorage.setItem('accessToken', data.accessToken);
-  localStorage.setItem('refreshToken', data.refreshToken);
+  if (typeof window !== 'undefined') {
+    localStorage.setItem('accessToken', data.accessToken);
+    localStorage.setItem('refreshToken', data.refreshToken);
+  }

   if (originalReq.headers) {
     const headers = originalReq.headers as AxiosRequestHeaders;
     headers['Authorization'] = `Bearer ${data.accessToken}`;
   }
   return fetcher(originalReq);
 } catch (refreshError) {
   console.warn('토큰 갱신 실패:', refreshError);
-  window.alert('실패했습니다.');
+  if (typeof window !== 'undefined') {
+    window.alert('세션이 만료되었습니다. 다시 로그인해주세요.');
+  }
+  // 로그인 페이지로 리다이렉트 또는 로그아웃 처리
+  throw refreshError;
 }
📝 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.

Suggested change
try {
const refreshToken = getRefreshToken();
if (!refreshToken) throw new Error('No refresh token available');
window.alert('세션이 만료되었습니다. 다시 로그인해주세요.');
const { data } = await refreshFetcher.post<{
accessToken: string;
refreshToken: string;
}>('/auth/refresh', { refreshToken });
localStorage.setItem('accessToken', data.accessToken);
localStorage.setItem('refreshToken', data.refreshToken);
if (originalReq.headers) {
const headers = originalReq.headers as AxiosRequestHeaders;
headers['Authorization'] = `Bearer ${data.accessToken}`;
}
return fetcher(originalReq);
} catch (refreshError) {
console.warn('토큰 갱신 실패:', refreshError);
window.alert('실패했습니다.');
}
try {
const refreshToken = getRefreshToken();
if (!refreshToken) throw new Error('No refresh token available');
const { data } = await refreshFetcher.post<{
accessToken: string;
refreshToken: string;
}>('/auth/refresh', { refreshToken });
if (typeof window !== 'undefined') {
localStorage.setItem('accessToken', data.accessToken);
localStorage.setItem('refreshToken', data.refreshToken);
}
if (originalReq.headers) {
const headers = originalReq.headers as AxiosRequestHeaders;
headers['Authorization'] = `Bearer ${data.accessToken}`;
}
return fetcher(originalReq);
} catch (refreshError) {
console.warn('토큰 갱신 실패:', refreshError);
if (typeof window !== 'undefined') {
window.alert('세션이 만료되었습니다. 다시 로그인해주세요.');
}
// 로그인 페이지로 리다이렉트 또는 로그아웃 처리
throw refreshError;
}
🤖 Prompt for AI Agents
In src/libs/api.ts lines 73 to 94, the token refresh logic has multiple issues:
move the session expiration alert to after a successful token refresh to avoid
confusing users; add a check to ensure localStorage is accessible (e.g., confirm
window is defined) before accessing it to prevent SSR errors; on refresh
failure, throw the error to stop retrying the original request; and replace the
vague failure alert with a clearer, more informative message. Implement these
changes to improve user experience and error handling.

Comment on lines +78 to +81
const { data } = await refreshFetcher.post<{
accessToken: string;
refreshToken: string;
}>('/auth/refresh', { refreshToken });
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

❗ 아마 이부분이 refresh 토큰을 받아오는 부분으로 예상이 가는데 경로는 /auth/tokens이고요 body를 통해 refresh token을 보내는 게 아니라 header에 넣어서 보내는 것 같더라고요 현재 요청에서 404오류가 발생하니 수정 부탁드립니다!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

✨ 기능 추가 새로운 코드나 기능을 추가했어요~!

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[feat] 토큰 만료 시 재요청

6 participants