Next.js + TanStack Query + JWT 조합으로 사내툴을 만들고 있었다.
테스트를 위해 accessToken 만료 시간을 10초로 줄여 놓고, 메뉴 이동과 새로고침 시점에 refresh 로직이 잘 도는지 확인했다.
흐름은 이렇게 설계했다.
기본적인 흐름은 잘 동작했다.
문제는 “직원 관리 / 라이선스 관리” 페이지에서 몇 초 있다가 브라우저 새로고침을 하면, 갑자기 로그아웃되고 /login으로 튕기는 현상이었다.
바로 새로고침하면 괜찮고, 몇 초 지나서 새로고침하는 경우에만 문제가 터졌다.
처음에는 인터셉터나 백엔드 refresh API를 의심했지만, 실제 원인은 “토큰이 있는지 판별하는 기준”이었다.
초기 구현은 다음과 같았다.
문제가 된 시나리오는 이렇다.
→ 인터셉터가 refresh 호출→ 새 accessToken 발급→ refresh 토큰은 한 번 소비된 상태가 된다정리하자면, 실제로는 localStorage에 유효한 accessToken이 있는데, 토큰의 유무를 Zustand store만 기준으로 판단하다 보니, 새로고침 직후에 두 번째 refresh 호출이 발생했고, 이게 강제 로그아웃의 직접적인 원인이었다.
토큰의 유무를 판단할 때 Zustand store가 아니라 localStorage 기준으로 바꿨다.
ProtectedPage에서 다음처럼만 변경했다.
const getStoredAccessToken = () => {
try {
return JSON.parse(localStorage.getItem('accessToken') ?? '{}')?.state?.accessToken ?? null
} catch {
return null
}
}
// 이전: store 기준
// if (!accessToken) { attemptRefresh() } else { setIsRefreshing(false) }
// 이후: localStorage 기준
const storedToken = getStoredAccessToken()
if (!storedToken) {
attemptRefresh()
} else {
setIsRefreshing(false)
}
이렇게 바꾸고 나서는, 페이지 안에서 401 → refresh 한 번 도는 것은 그대로 유지되고, 그 뒤에 몇 초 있다가 새로고침을 해도 localStorage에 accessToken이 있는 이상 새로고침 직후에는 refresh를 다시 호출하지 않아서 강제 로그아웃이 발생하지 않았다.