토큰 트랩(token trap)이 실제 운영 중인 사이트에서 발생했습니다. 한 사용자가 연 5개의 탭이 모두 동일한 밀리초(millisecond)에 만료된 JWT를 갱신하려고 시도하면서, 백엔드에 중복된 리프레시 요청이 폭주했고 즉시 세션이 무효화되었습니다. 모든 탭에서 사용자가 로그아웃되었으며, 이는 토큰 갱신을 위한 단일 탭 솔루션만으로는 더 이상 충분하지 않다는 것을 증명했습니다.

토큰 트랩이 중요한 이유

현대의 싱글 페이지 앱(SPA)은 수명이 짧은 access token과 수명이 긴 refresh token을 사용합니다. access token이 만료되면 클라이언트는 refresh 요청을 보내 새로운 토큰 쌍을 받고, 원래의 호출을 재시도합니다. 대부분의 개발자는 이 흐름을 인메모리 플래그(예: isRefreshing = true)나 요청 큐(request queue)로 보호하며, 이를 단일 탭 환경에서 테스트합니다. 하지만 실제 환경에서 사용자는 설정 페이지, 분석 대시보드, 몇 개의 데이터 뷰 등 여러 탭을 열어둡니다. access token이 만료되면 각 탭은 독립적으로 401 에러를 감지하고, 각자 refresh 요청을 보냅니다. 이때 백엔드(특히 refresh-token rotation을 적용하는 경우)는 두 번째 요청을 재전송(replay)으로 간주하여 전체 세션을 취소해 버립니다.

JavaScript 격리가 문제를 일으킵니다

각 브라우저 탭은 자체적인 JavaScript 컨텍스트에서 실행됩니다. 변수, 타이머, 인메모리 플래그는 동일한 origin을 공유하더라도 다른 탭에서는 보이지 않습니다. “이미 갱신이 진행 중”임을 나타내는 플래그는 해당 플래그를 설정한 탭 내부에서만 존재합니다. 다른 탭들은 다른 곳에서 토큰이 갱신되고 있다는 사실을 알 방법이 없으므로, 모두 각자의 네트워크 호출을 시작합니다. 이 문제는 인터셉터의 버그가 아니라, 클라이언트 측 상태 격리(state isolation)의 근본적인 한계 때문입니다.

Web Locks API가 해결책입니다

한 탭이 락(lock)을 보유하고 있으면, 동일한 락을 요청하는 다른 모든 탭은 락이 해제될 때까지 기다려야 합니다.

토큰 갱신 시 작동 방식

  1. 401 감지 – 권한 없음(unauthorized) 응답을 받은 모든 탭은 navigator.locks.request('auth_token_refresh_lock', async lock => { … })를 호출합니다.
  2. 락 획득 – 다른 탭이 락을 보유하고 있지 않으면 현재 탭이 진행하고, 그렇지 않으면 락이 풀릴 때까지 일시 중지합니다.
  3. 한 번만 갱신 – 락을 보유한 탭이 refresh 요청을 보내고, 새로운 access token과 타임스탬프를 localStorage에 저장한 뒤, 콜백이 완료되면 자동으로 락을 해제합니다.
  4. 중복 작업 건너뛰기 – 대기 중이던 탭이 마침내 락을 얻으면 localStorage에서 타임스탬프를 읽습니다. 만약 토큰이 설정된 시간 범위(예: 최근 몇 초 이내) 안에 갱신되었다면, 해당 탭은 네트워크 호출을 건너뛰고 localStorage에서 인메모리 토큰을 업데이트합니다.
  5. 충돌 처리 – 락을 보유한 상태에서 탭이 충돌하거나 닫히면 브라우저가 락을 해제하여 다른 탭이 갱신을 재시도할 수 있도록 합니다.

한눈에 보는 장점

  • 불필요한 네트워크 호출 제로 – 첫 번째 탭만 백엔드와 통신합니다.
  • 세션 파괴 방지 – refresh-token rotation이 단 한 번만 실행되므로 세션이 유지됩니다.
  • 유연한 복구 – 브라우저가 관리하는 락 해제 기능 덕분에 탭이 사라지더라도 데드락(deadlock)이 발생하지 않습니다.

구현 체크리스트

  • Axios(또는 fetch) 인터셉터의 갱신 로직을 락 요청으로 감싸세요.
  • 갱신된 토큰과 밀리초 단위의 타임스탬프를 localStorage에 저장하세요(sessionStorage를 사용하여 세션별 데이터를 관리할 수도 있습니다).
  • 락이 부여되면 저장된 타임스탬프를 Date.now()와 비교하세요. 차이가 임계값보다 작으면 백엔드를 호출하는 대신 저장소에서 토큰을 읽어오세요.
  • 인터셉터가 원래의 API 호출을 재시도하기 전에 저장소에서 가져온 토큰으로 요청 헤더를 업데이트하는지 확인하세요.
  • 여러 탭을 사용하여 흐름을 테스트하고, 네트워크 지연을 시뮬레이션하여 단 하나의 refresh 요청만 서버에 도달하는지 검증하세요.

발생할 수 있는 문제

다음에 주의 깊게 살펴볼 점

핵심 요약

각 브라우저 탭을 작은 분산 시스템의 노드로 취급하세요. Web Locks API를 사용하여 JWT 갱신을 직렬화(serialize)함으로써, 중복 호출을 제거하고, refresh-token rotation을 보호하며, 사용자가 열어둔 모든 탭에서 로그인 상태를 유지할 수 있습니다.