Skip to content

[Fix] refreshToken이 유효하지 않을 때 무한 새로고침 발생하는 버그 수정#395

Merged
Chiman2937 merged 2 commits intomainfrom
chiyoung-fix/refresh
Mar 1, 2026
Merged

[Fix] refreshToken이 유효하지 않을 때 무한 새로고침 발생하는 버그 수정#395
Chiman2937 merged 2 commits intomainfrom
chiyoung-fix/refresh

Conversation

@Chiman2937
Copy link
Member

📝 변경 사항

refreshToken 무한 새로고침 버그픽스 변경사항

브랜치: chiyoung-fix/refresh
커밋 범위: 49539f6 ~ 9113e37 (2개 커밋)
변경 파일: 2개 (64 additions, 26 deletions)


커밋 히스토리

커밋 메시지
49539f6 fix: axios interceptor - refresh 401 오류 시 /login 경로에서는 redirect 발생하지 않도록 수정
9113e37 fix: refreshToken이 유효하지 않을 때 무한 새로고침 되는 버그 수정

1. refreshToken 무한 새로고침 버그 수정

버그 원인

refreshToken이 쿠키에 존재하지만 유효하지 않은 경우, 아래 흐름이 무한 반복되는 버그가 존재했음.

요청 → proxy.ts: refresh 시도 → 실패 → refreshToken 유지
→ 다음 요청 → 또 refresh 시도 → 또 실패 → ...

refresh 실패 시 refreshToken 쿠키를 삭제하지 않아, 매 요청마다 유효하지 않은 refreshToken으로 refresh를 반복하는 구조적 문제였음.


수정 1. proxy.ts - refresh 실패 시 refreshToken 쿠키 삭제

refresh 실패 시 refreshFailed 플래그를 세우고, refreshToken 쿠키를 즉시 삭제하도록 수정.

// Before - refresh 실패 시 아무것도 하지 않음
} catch {
  hasValidToken = false;
}

// After - refresh 실패 시 refreshToken 쿠키 삭제
} catch (err) {
  isLoggedIn = false;
  refreshFailed = true;
  response.cookies.set('refreshToken', '', {
    maxAge: 0,
    domain: 'wego.monster',
    path: '/',
    httpOnly: true,
    secure: process.env.NODE_ENV === 'production',
  });
}

cookies.delete('refreshToken') 대신 cookies.set으로 maxAge: 0을 사용하는 이유: delete()domain 속성 없이 삭제 헤더를 생성하므로, 백엔드가 Domain=wego.monster로 설정한 쿠키와 매칭되지 않아 실제로 삭제되지 않음.


수정 2. proxy.ts - /login redirect 시에도 refreshToken 쿠키 삭제

member-only 경로에서 refresh 실패 시 /login으로 redirect하는 응답 객체에도 별도로 쿠키 삭제를 적용.

// Before - redirect 응답에는 refreshToken 삭제 처리 없음
if (!hasValidToken) {
  const loginUrl = getLoginUrl(request);
  return NextResponse.redirect(loginUrl);
}

// After - redirect 응답에도 refreshToken 삭제 적용
if (isMemberOnly && !isLoggedIn) {
  const redirectResponse = NextResponse.redirect(loginUrl);
  if (refreshFailed) {
    redirectResponse.cookies.set('refreshToken', '', {
      maxAge: 0,
      domain: 'wego.monster',
      path: '/',
      httpOnly: true,
      secure: process.env.NODE_ENV === 'production',
    });
  }
  return redirectResponse;
}

NextResponse.redirect()는 새 응답 객체를 생성하므로, response에 설정한 쿠키 삭제가 전달되지 않음. 따라서 redirect 응답에도 별도로 쿠키 삭제를 적용해야 함.


수정 3. proxy.ts - API.authService.refresh()fetch 직접 호출로 교체

기존에는 API.authService.refresh()를 통해 refresh를 호출했으나, createApiHelper가 응답 body만 반환하고 헤더를 버리는 구조로 인해 백엔드의 Set-Cookie: refreshToken 헤더가 브라우저에 전달되지 않는 문제가 있었음.

백엔드는 Refresh Token Rotation 방식을 사용하므로 refresh 요청마다 새로운 refreshTokenSet-Cookie로 발급함. 이 헤더를 브라우저에 포워딩하기 위해 fetch를 직접 호출하는 방식으로 변경.

// Before - API 헬퍼를 통한 호출 (Set-Cookie 헤더 유실)
const data = await API.authService.refresh();
response.cookies.set('accessToken', data.accessToken, { ... });

// After - fetch 직접 호출 후 Set-Cookie 헤더 포워딩
const res = await fetch(`${process.env.NEXT_PUBLIC_API_BASE_URL}/api/v1/auth/refresh`, {
  method: 'POST',
  headers: { Cookie: `refreshToken=${refreshToken.value}` },
});
if (!res.ok) throw new Error('refresh failed');
const json = await res.json();
const data: RefreshResponse = json.data;

response.cookies.set('accessToken', data.accessToken, { ... });

// 백엔드가 발급한 새 refreshToken Set-Cookie 헤더를 브라우저에 포워딩
const setCookieHeader = res.headers.get('set-cookie');
if (setCookieHeader) {
  response.headers.append('Set-Cookie', setCookieHeader);
}

createApiHelper는 응답 body 추출을 담당하는 범용 헬퍼로, 헤더 포워딩이라는 미들웨어 특수 요구사항을 위해 변경하지 않고 proxy.ts에서 직접 fetch를 사용하는 방식을 선택.


2. axios interceptor - /login 경로에서 redirect 방지

버그 원인

axios interceptor의 401 처리에서 refresh 실패 시 /login으로 redirect하는데, 이미 /login 페이지에 있는 경우에도 redirect가 발생하여 불필요한 루프가 생길 수 있었음.

// Before - /login에서도 /login으로 redirect 발생
} catch (refreshError) {
  if (isServer) {
    throw refreshError;
  } else {
    const currentPath = window.location.pathname + window.location.search;
    window.location.href = `/login?error=unauthorized&path=${encodeURIComponent(currentPath)}`;
    return;
  }
}

// After - /login 경로에서는 redirect 없이 에러 throw
} catch (refreshError) {
  if (isServer) {
    throw refreshError;
  } else {
    if (window.location.pathname === '/login') {
      throw errorResponse;
    }
    const currentPath = window.location.pathname + window.location.search;
    window.location.href = `/login?error=unauthorized&path=${encodeURIComponent(currentPath)}`;
    return;
  }
}

수정 후 시나리오별 동작

시나리오 기존 동작 수정 후
유효하지 않은 refreshToken + member-only 경로 /login redirect (refreshToken 유지 → 무한루프) /login redirect + refreshToken 삭제
유효하지 않은 refreshToken + 일반 경로 통과 (refreshToken 유지 → 다음 요청에서 반복) 통과 + refreshToken 삭제
refresh 성공 새 accessToken만 쿠키 설정 (refreshToken 미갱신) 새 accessToken + 새 refreshToken 모두 브라우저에 반영
/login에서 API 401 발생 /login으로 재redirect 에러 throw (redirect 없음)

변경된 파일 목록

파일 변경 유형
src/proxy.ts 수정 (refresh 버그 수정, fetch 직접 호출)
src/api/core/base/index.ts 수정 (/login 경로 redirect 방지 조건 추가)

TODO

에러 유형별 사용자 안내 메시지 처리

현재 모든 401 에러(토큰 만료, 중복 로그인, 위조 토큰 등)에 대해 /login?error=unauthorized로 redirect만 할 뿐, 사용자에게 구체적인 원인을 안내하지 않음.

백엔드 명세(동시 로그인 제한 기능)에 따라 에러코드별로 다른 메시지를 노출하도록 개선 필요.

에러코드 현재 처리 개선 후 안내 메시지 (예시)
EXPIRED_TOKEN /login redirect - (refresh 후 재시도로 자동 복구)
DUPLICATE_LOGIN /login redirect (→ /login에서 재redirect 무한루프 발생 → 수정 완료) "다른 기기에서 로그인되었습니다. 다시 로그인해주세요."
INVALID_TOKEN /login redirect "인증 정보가 유효하지 않습니다. 다시 로그인해주세요."
NOT_FOUND_TOKEN /login redirect - (비로그인 상태로 자연스럽게 처리)

DUPLICATE_LOGIN 관련 이슈 (수정 완료)

DUPLICATE_LOGIN 에러 발생 시 axios interceptor가 /login으로 redirect하는데, 이미 /login 페이지에 있는 경우 다시 /login으로 redirect가 반복되는 무한루프 문제가 있었음. 2번 항목(axios interceptor - /login 경로에서 redirect 방지)에서 함께 수정됨.

구현 방향

  • axios interceptor에서 errorCode를 파싱하여 /login?error={errorCode} 형태로 redirect
  • /login 페이지에서 error query param에 따라 toast 또는 안내 문구 노출

🔗 관련 이슈

Closes #


🧪 테스트 방법

  • 수동 테스트 검증(로컬 환경)
  • 유닛 테스트 검증
  • 통합 테스트 검증

📸 스크린샷 (선택)


📋 체크리스트

  • 관련 문서를 업데이트했습니다 (필요한 경우)
  • 테스트를 추가/수정했습니다 (필요한 경우)
  • Breaking change가 있다면 명시했습니다

💬 추가 코멘트


CodeRabbit Review는 자동으로 실행되지 않습니다.

Review를 실행하려면 comment에 아래와 같이 작성해주세요

@coderabbitai review

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Mar 1, 2026

Important

Review skipped

Auto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch chiyoung-fix/refresh

Tip

Try Coding Plans. Let us write the prompt for your AI agent so you can ship faster (with fewer bugs).
Share your feedback on Discord.


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

Comment @coderabbitai help to get the list of available commands and usage tips.

@github-actions
Copy link

github-actions bot commented Mar 1, 2026

🎭 Playwright Report

E2E Test가 성공적으로 완료되었습니다.

Test 요약 내용을 확인해주세요.

Status Build Log Updated (UTC)
✅ Ready View Build 2026-03-01 12:49:53

📊 Test Summary

  • ✅ Passed: 3
  • ❌ Failed: 0
  • ⏱️ Duration: 38.5s

📜 Test Details

✅ Passed Tests (3)
  • profile.test.ts (3)
    • [chromium] 존재하지 않는 프로필 페이지로 접속 시 404 redirect 되는 지 테스트
    • [firefox] 존재하지 않는 프로필 페이지로 접속 시 404 redirect 되는 지 테스트
    • [webkit] 존재하지 않는 프로필 페이지로 접속 시 404 redirect 되는 지 테스트

@github-actions
Copy link

github-actions bot commented Mar 1, 2026

📊 Coverage Report

Status Build Log Updated (UTC)
✅ Ready View Build 2026-03-01 12:48:36

📉 #395main에 병합하면 coverage가 0.12% 감소합니다.

Coverage 요약

@@             Coverage Diff             @@
##             main     #395       +/-   ##
===========================================
- Coverage   35.36%   35.24%    -0.12%     
===========================================
  Files         264      264         0     
  Lines       12104    12142       +38     
  Branches      474      474         0     
===========================================
  Hits         4280     4280         0     
+ Misses       7824     7862       +38     

영향받은 파일

파일 Coverage 변화
/home/runner/work/WeGo_FrontEnd/WeGo_FrontEnd/src/api/core/base/index.ts 41.66% (-2.19%) ⬇️

@github-actions
Copy link

github-actions bot commented Mar 1, 2026

🎨 Storybook Report

Story가 변경되었습니다

Chromatic에서 비주얼 변경사항을 확인하세요.

Status Storybook Build Log Updated (UTC)
✅ Ready View Storybook View Build 2026-03-01 12:49:35

@github-actions
Copy link

github-actions bot commented Mar 1, 2026

🚀 PR Preview Report

Build가 성공적으로 완료되었습니다.

Preview에서 변경사항을 확인하세요.

Status Preview Build Log Updated (UTC)
✅ Ready Visit Preview View Logs 2026-03-01 12:49:36

@Chiman2937 Chiman2937 changed the title [Fix] refresh 실패 시 refreshToken 미삭제로 인한 무한 새로고침 수정 [Fix] refreshToken이 유효하지 않을 때 무한 새로고침 발생하는 버그 수정 Mar 1, 2026
@Chiman2937 Chiman2937 merged commit dd5160f into main Mar 1, 2026
7 of 8 checks passed
@Chiman2937 Chiman2937 deleted the chiyoung-fix/refresh branch March 1, 2026 13:05
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant