다중 탭 환경에서 발생하는 웹소켓 커넥션 병목 현상을 해결하기 위한 Shared Worker 기반의 멀티플렉싱 설계와, 모바일 파편화를 극복하는 리더 선출(Leader Election) 폴백 아키텍처를 분석합니다.
현대의 웹 애플리케이션은 라이브 대시보드, 공동 편집 도구, 멀티플레이어 게임 등 끊임없는 실시간 상호작용(Real-time Communication)을 요구합니다. 이를 위해 프론트엔드 생태계는 WebSocket과 **SSE(Server-Sent Events)**를 적극적으로 채택해 왔습니다.
하지만 이 기술들은 사용자의 다중 탭(Multi-tab) 브라우징 패턴과 결합하면서 예상치 못한 거대한 아키텍처적 병목 현상을 야기했습니다.
전통적인 프론트엔드 아키텍처에서는 각각의 브라우저 탭이 완전히 독립적인 실행 컨텍스트를 가집니다. 즉, 한 사용자가 15개의 탭을 열어둔다면 15개의 독립적인 웹소켓/SSE 커넥션이 서버와 맺어집니다.
클라이언트의 중복 커넥션은 로드 밸런서의 세션 추적 부하를 가중시키고, 백엔드 노드 간 상태 동기화를 위한 내부 메시지 브로커(Redis, RabbitMQ 등)의 글로벌 통신량을 폭증시킵니다.
또한, 브라우저의 동일 도메인 동시 커넥션 제한(보통 6개)에 걸려, 일반적인 API 요청조차 무한 대기(Pending) 상태에 빠지는 현상이 발생합니다.
이를 극복하기 위해 클라이언트 단에서 커넥션을 하나로 묶어 서버로 전달하는 커넥션 일원화(Connection Centralization) 전략이 대두되었습니다.
커넥션 일원화를 논하기 전, 기반이 되는 두 프로토콜의 특성을 명확히 해야 합니다.
| 특성 | WebSocket | SSE |
|---|---|---|
| 통신 방향성 | 전이중 (Full-duplex), 양방향 | 단방향 (Server → Client) |
| 기반 프로토콜 | 초기 핸드셰이크 후 독자적 프로토콜 | 순수 HTTP/1.1, HTTP/2 활용 |
| 데이터 포맷 | 바이너리 및 텍스트 프레임 | 텍스트 기반 스트림 (UTF-8) |
| 연결 복구 | 직접 구현 필요 | 네이티브 자동 재연결 및 Last-Event-ID 지원 |
| 이상적 유스케이스 | 멀티플레이어 게임, 양방향 협업 툴 | 라이브 대시보드, 주식 시세, 단방향 피드 |
이러한 네트워크 커넥션을 메인 스레드(UI)와 격리하여 중앙 통제하기 위해 Web Worker API를 활용합니다.
전용 워커 (Dedicated Worker)
Shared Worker를 라우터로 활용하는 아키텍처는 정교한 비동기 통신 설계를 요구합니다.
단일 웹소켓에 수많은 탭이 몰려있을 때, 서버에서 온 응답이 어느 탭의 요청에 대한 결과인지 식별하기 위해 다음 패턴을 사용합니다.
특정 탭에서 요청이 발생하면 고유한 난수(callbackId)를 생성하고, 이를 키(Key)로 콜백 함수를 탭 내부 딕셔너리에 등록합니다.
생성된 ID와 페이로드가 Shared Worker로 전달되고, 워커는 단일 웹소켓을 통해 서버로 전송합니다.
서버는 로직 처리 후, 클라이언트가 보낸 callbackId를 그대로 포함시켜 결과 페이로드를 워커로 보냅니다.
워커는 연결된 모든 탭(MessagePort)에 메시지를 브로드캐스트합니다. 각 탭은 callbackId를 대조하여 자신의 딕셔너리에 일치하는 식별자가 있을 때만 UI를 업데이트합니다.
Shared Worker 아키텍처의 가장 치명적인 약점은 탭이 닫힐 때 MessagePort 객체가 해제되지 않아 발생하는 **메모리 누수(Memory Leak)**입니다.
beforeunload 이벤트를 캡처하여 워커에 포트 삭제를 지시합니다. 하지만 브라우저 크래시나 OS 강제 종료 시 이벤트 호출이 보장되지 않는 한계가 있습니다.
아키텍처적 완성도에도 불구하고, Android Chrome 브라우저는 현재까지도 Shared Worker를 지원하지 않습니다. 이를 극복하기 위해 분산 컴퓨팅 이론을 차용한 리더 선출(Leader Election) 아키텍처를 사용합니다.
모든 탭은 navigator.locks.request()를 호출하여 독점적 락(Lock)을 요청합니다. 브라우저 스케줄러에 의해 단 하나의 탭만이 리더(Leader) 로 승격되어 웹소켓 커넥션을 수립합니다.
권한을 얻지 못한 나머지 탭(팔로워)들은 직접 서버와 통신하지 않고, BroadcastChannel을 통해 리더 탭에게 모든 요청을 위임합니다.
리더 탭이 강제 종료되거나 크래시되면 브라우저는 즉시 락을 회수합니다. 대기 중이던 다음 팔로워 탭의 프로미스가 즉각 실행되며 새로운 리더로 승격, 수십 밀리초 내에 커넥션을 재건합니다.
Android Chrome 사용자가 많은 서비스라면 처음부터 Leader Election을 기본 전략으로 채택하고, Shared Worker를 점진적 향상(Progressive Enhancement)으로 제공하는 것이 안전합니다.
사용자가 마지막 탭을 닫는 순간, 워커는 즉시 종료되므로 분석 로그 전송이나 상태 지속성(IndexedDB 덤프 등) 처리가 불가능했습니다.
Chrome 139부터 도입된 Extended Lifetime Shared Workers 스펙은 이 문제를 해결합니다.
// 30초의 유예 수명을 보장받는 워커 초기화
const worker = new SharedWorker(url, { extendedLifetime: true });
이 옵션 하나로 워커는 모든 참조 탭이 닫힌 후에도 30초 동안 생존합니다. 이 유예 기간 동안 analytics 데이터 플러시, IndexedDB 동기화, 인앱 메시지 전송 등 중요한 마무리 작업을 안정적으로 수행할 수 있습니다.
Extended Lifetime은 Chrome 139+의 Origin Trial 기능으로, 아직 모든 브라우저에서 지원되지 않습니다. Production 적용 전 Chrome Platform Status에서 지원 범위를 확인하세요.