refreshToken은 살아있는데 왜 로그인은 풀렸을까? Next.js 인증 구조 디버깅 기록

#401에러해결#AccessToken#AuthProvider구조#BFF아키텍처#NextJS인증

현상 정리

어느 날 보니 분명 refreshToken은 쿠키에 살아 있다.
브라우저 Application 탭을 열어보면 멀쩡히 존재한다. 그런데 화면은 로그아웃 상태처럼 보인다. 헤더에는 Login만 덩그러니 떠 있고, 마치 인증이 완전히 끊긴 것처럼 동작한다. 심지어 페이지를 이동해도 마찬가지였다. URL은 잘 바뀌는데, 인증 상태는 복구되지 않는다.

이건 뭐지?

토큰은 있는데 로그인은 아니다?

마치 지갑에 카드가 있는데 단말기가 결제를 거부하는 느낌.


bd8a833d-b265-4080-9348-748336584823.png


인증 / 갱신 구조 요약

우리 구조는 이렇게 되어 있었다.

  • accessToken 실제 API 요청에 사용되는 단기 토큰

  • refreshToken access 만료 시 재발급을 위한 장기 토큰 (HttpOnly 쿠키)

클라이언트에서는 AuthProvider/auth/me를 호출해 로그인 여부를 판단한다.
서버 쪽에서는 BFF(프록시)가 access 만료 시 refresh API를 호출하고 성공하면 새로운 access를 세팅해준다. 이론상 완벽하다. 말로 하면 아주 그럴싸하다. 하지만 실제로는 토큰은 있는데 로그인처럼 안 보였다.


원인

1) Transient 실패

서버에서 refresh가 실패해도 쿠키를 명시적으로 지우지 않고 그냥 401만 반환하고 있었다.

즉,

  • refreshToken은 쿠키에 남아 있음

  • access는 만료됨

  • 서버는 401 반환

  • 클라이언트는 unauthenticated 상태로 전환


결과적으로 토큰은 있는데 로그아웃처럼 보이는 기묘한 상태가 만들어졌다. 이건 거의 쿠키는 살아있는데 세션은 죽은 상태 인증 좀비 상태랄까.


2) 페이지 이동 시 refetch 없음

더 큰 문제는 이거였다.

pathname이 바뀌어도 /auth/me를 다시 호출하지 않았다.

즉 한 번 unauthenticated 상태가 되면 복구 기회가 없었다. 마치 한 번 삐끗하면 다시 로그인 전까지 영원히 Login 버튼만 보이는 구조. 아이 셋 키우면서 한 번 삐끗하면 하루가 무너지는 것처럼, 인증도 한 번 무너지면 그대로 끝이었다.


적용한 조치

이제부터가 진짜다.

1) 리프레시 성공 시 accessrefresh 모두 재설정

  • Next /api/auth/refresh

  • phpFetch

  • BFF

이 세 군데 모두에서
refresh 성공 시 access + refresh 둘 다 쿠키로 재설정하도록 통일했다.

단일 소스 오브 트루스는 쿠키.
흐름은 하나로 정리.

토큰은 흩어지면 안 된다.
육아도 그렇고 인증도 그렇다.
일관성이 생명이다.


2) 클라이언트에서 401 발생 시 refresh 1회 시도

클라이언트 fetch에서 401이 오면

  1. refresh 1회 시도

  2. 성공하면 /auth/me 재호출

  3. 인증 상태 복구

한 번의 기회를 주는 구조다.
인생도, 인증도 리트라이 한 번은 있어야 한다.


3) pathname 변경 시 refetch

라우팅 변경 시 /auth/me를 다시 호출하도록 수정했다.

페이지 이동할 때마다 인증 상태를 재검증.

이제는 한 번 깨져도 복구 기회가 생겼다.


정리 (원인과 조치 요약)

refresh 실패 시 쿠키를 정리하지 않고 401만 반환하면서 access는 만료되고 refreshToken만 남는 불일치 상태가 발생했고, 클라이언트에서는 401 이후 인증 재시도 로직과 pathname 변경 시 refetch가 없어 한 번 unauthenticated 상태가 되면 복구되지 않는 구조였다. 이를 해결하기 위해 refresh 성공 시 accessrefresh를 모두 쿠키로 재설정하도록 통일하고, 클라이언트에서 401 발생 시 refresh를 1회 시도 후 /auth/me를 재호출하도록 수정했으며, pathname 변경 시에도 인증을 재확인하도록 보완했다.


결국 이 문제는 토큰 문제가 아니라 흐름의 문제였다. 나는 또 하나 배웠다. 인증은 기술이고, 복구는 설계다. 오늘도 나는

코드와 책임 사이에서 버그를 줄이고 가족을 지킨다. 완벽하진 않아도 적어도 흐름은 바로 세웠다. 그 정도면, 오늘의 나는 분명 한 단계는 성장했다.


25

댓글