Web Locks API를 사용하면 5개의 열린 탭이 동시에 인증 서버에 리프레시 토큰 요청을 퍼붓는 것을 막아, 사용자가 갑자기 로그아웃되는 상황을 방지할 수 있습니다. 탭 간의 리프레시 과정을 조정함으로써, 단 한 번의 요청만으로 Refresh Token Rotation이 사용될 때 세션 종료를 유발하는 요청 폭주를 대체할 수 있습니다.

멀티 탭 세션에 숨겨진 과부하

일반적인 싱글 페이지 앱(SPA)은 401 응답을 감시하고, Boolean isRefreshing 플래그를 전환하며, 새로운 JWT가 도착할 때까지 나가는 모든 요청을 대기열에 추가하는 Axios 인터셉터를 추가합니다. 탭 하나에서 테스트하면 이 흐름은 완벽하게 작동합니다.

동일한 앱을 5개의 탭에서 열고 액세스 토큰이 만료되게 두면, 5개의 탭 모두가 동일한 밀리초에 401을 감지합니다. 각 탭은 자신이 리프레시를 해야 한다고 생각하므로, 5개의 동일한 리프레시 토큰 요청이 인증 서버를 향해 경주하듯 달려듭니다. 새로운 토큰이 발급되는 즉시 이전 리프레시 토큰을 무효화하는 보안 조치인 Refresh Token Rotation을 사용하는 경우, 서버는 두 번째 요청을 재전송 공격(replay attack)으로 간주하여 세션이 침해된 것으로 표시하고 이를 취소합니다. 사용자는 순식간에 모든 탭에서 로그아웃됩니다.

근본 원인은 JavaScript의 격리 모델에 있습니다. isRefreshing과 같은 변수는 이를 설정한 탭에만 존재하며, 다른 탭은 리프레시가 이미 진행 중인지 알 방법이 없습니다. 그 결과 전형적인 동시성 문제가 발생하지만, 여기서 "프로세스"는 스레드가 아닌 브라우저 탭입니다.

왜 크로스 탭 락(cross-tab lock)이 적절한 도구인가

우리에게 필요한 것은 탭들이 공유 리소스(이 경우에는 새로운 JWT)에 대해 서로 통신할 수 있는 방법입니다. navigator.locks로 제공되는 Web Locks API가 바로 그 역할을 합니다. 이 API는 스크립트가 동일 출처(same origin)에 속한 모든 컨텍스트에서 브라우저가 강제하는 이름이 지정된 락을 요청할 수 있게 해줍니다. 이미 락이 점유되어 있다면, 점유자가 락을 해제하거나 브라우저가 이를 중단할 때까지(예: 탭이 충돌하는 경우) 다른 호출자들은 대기열에 추가됩니다. 외부 서버도, 폴링(polling)도 필요 없이 브라우저 자체의 조정 기능만으로 가능합니다.

락 기반 리프레시 흐름 구현하기

  1. 401 감지 – Axios 인터셉터가 이전과 같이 권한 없음 응답을 포착합니다.
  2. 독점적 락 요청 – 탭이 navigator.locks.request('auth_token_refresh_lock', async lock => { … })를 호출합니다. 한 번에 하나의 탭만 콜백에 진입할 수 있습니다.
  3. 네트워크 요청 전 이중 확인 – 락 내부에서 localStorage로부터 타임스탬프(또는 토큰 자체)를 읽습니다. 타임스탬프가 생성된 지 몇 초 지나지 않았다면, 다른 탭이 이미 토큰을 리프레시한 것입니다. 현재 탭은 네트워크 호출을 건너뛰고 저장소에서 새 JWT를 읽기만 하면 됩니다.
  4. 필요 시 리프레시 – 저장된 타임스탬프가 오래되었다면 리프레시 요청을 보내고, 새 토큰과 현재 시간을 localStorage에 저장한 다음, 콜백을 반환하여 락을 해제합니다.
  5. 대기 중인 요청 재개 – 대기 중이던 다른 모든 탭은 차례대로 락을 획득하고, 최신 타임스탬프를 확인한 뒤 추가 요청 없이 작업을 완료합니다.
async function refreshIfNeeded() {
  await navigator.locks.request('auth_token_refresh_lock', async lock => {
    const lastRefresh = Number(localStorage.getItem('token_refreshed_at') || 0);
    const now = Date.now();
    if (now - lastRefresh < 5_000) return; // another tab already refreshed

    const newToken = await callRefreshEndpoint(); // actual network call
    localStorage.setItem('jwt', newToken);
    localStorage.setItem('token_refreshed_at', now.toString());
  });
}

이 패턴은 열려 있는 탭의 수와 관계없이 단 하나의 리프레시 요청만 서버에 도달하도록 보장합니다.

측정 가능한 이점

  • 네트워크 효율성 – 하나의 요청이 다섯 개의 요청을 대체하여 대역폭과 서버 부하를 획기적으로 줄입니다.
  • 세션 안전성 – Refresh Token Rotation 환경에서 서버는 이전 리프레시 토큰의 사용을 단 한 번만 보게 되므로, 세션이 침해된 것으로 표시되지 않습니다.
  • 회복 탄력성 – 락을 보유한 탭이 충돌하더라도 브라우저가 자동으로 락을 해제하여, 모든 탭을 멈추게 할 수 있는 데드락(deadlock)을 방지합니다.
  • 확장성 – 조정 작업이 브라우저 내부에서 이루어지므로, 사용자는 연쇄 로그아웃의 위험 없이 수십 개의 탭을 열 수 있습니다.

반대 급부: 브라우저 지원 및 폴백(fallback)

Web Locks API는 비교적 새로운 기능입니다. 최신 Chromium 기반 브라우저와 최신 버전의 Firefox는 이를 구현하고 있지만, 오래된 브라우저나 Safari에는 기본 지원이 부족합니다. API를 사용할 수 없는 환경에서 개발자는 localStorage를 통해 커스텀 이벤트를 브로드캐스팅하거나 Shared Worker를 사용하는 등, 크로스 탭 신호를 흉내 내기 위해 덜 신뢰할 수 있는 기술로 폴백해야 합니다. 이러한 우회 방법들은 navigator.locks가 제공하는 자동 데드락 방지 기능이 없으므로 주의해서 사용해야 합니다.

다음에 살펴볼 내용

  • 표준화 진행 상황 – API의 채택 곡선을 주시하십시오. 더 폭넓은 지원이 이루어지면 락(lock) 기반 방식이 모든 탭 간 조정(cross-tab coordination)의 기본값이 될 것입니다.
  • 라이브러리 래퍼 – 몇몇 오픈 소스 유틸리티가 이미 락 요청 패턴을 추상화하고 있어, 기존 Axios 인터셉터에 더 쉽게 통합할 수 있습니다.
  • 보안 감사 – 락이 동시성 문제를 해결하더라도, 리프레시 엔드포인트는 여전히 적절한 토큰 로테이션과 속도 제한(rate limiting)을 강제해야 합니다. 락이 우회될 경우 단 하나의 악성 탭이 서버에 요청을 폭주시킬 수 있기 때문입니다.

핵심은 간단합니다. 열려 있는 탭 세트를 분산 시스템으로 취급하고, 이들에게 네이티브 동기화 프리미티브(synchronization primitive)를 제공하십시오. Web Locks API를 토큰 리프레시 흐름에 연결함으로써, 개발자는 “브라우저 탭 토큰 트랩(browser tab token trap)”을 제거하고 사용자가 아무리 많은 탭을 사용하더라도 로그인 상태를 유지할 수 있습니다.