Shared Worker로 단일 소스 데이터 모든 탭에서 공유하기

shared worker
shared worker

국내 국가/공공기관에 보안 솔루션 제품을 납품하려면, 제품이 일정 수준의 보안 요구사항을 충족한다는 것을 증빙하는 보안기능확인서가 사실상 필수로 따라옵니다. 이 인증서는 유효기간이 5년이고, 만료 시점마다 갱신을 진행해왔습니다.

즉, 지금 제가 유지보수하고 있는 이 레거시 코드는 최소 5년 이상 축적된 운영 경험과 요구사항이 반영된 코드입니다. 그리고 더 중요한 맥락이 하나 있습니다. 이 코드는 React / NestJS로 마이그레이션하기 전, 기존 제품 라인업에서 마지막으로 진행하는 보안기능확인서를 통과해야 하는 “마지막 레이스”의 일부였습니다.

이 글에서 소개할 기능은 그 요구사항을 만족시키기 위해 개발한 것으로, 핵심 목표는 단 하나였습니다.

  • 단일 소스의 데이터를 모든 탭에서 일관되게 공유할 것
  • 보안 이벤트(로그아웃/세션 무효화 등) 가 발생하면 열려있는 모든 탭이 즉시 동일한 행동을 하게 만들 것

웹 애플리케이션에서 여러 탭을 열어놓고 작업하는 경우가 많습니다. 이때 각 탭마다 서버와 개별 연결을 맺으면 어떤 문제가 발생할까요?

  • 서버 리소스 낭비 (동일 데이터를 탭 수만큼 요청. HTTP/1.1은 동시 커넥션이 기본 5~6개 수준)
  • 탭 간 상태 불일치
  • 보안 이벤트(로그아웃, 세션 만료) 동기화 어려움

이 글에서는 SharedWorker를 활용해 서버 연결을 탭 단위가 아니라 “브라우저(동일 origin) 단위”로 단일화하고, 그 결과를 모든 탭에서 공유하는 방법을 정리합니다.

처음부터 SharedWorker였던 건 아니다

처음에는 각 탭에서 서버와 직접 SSE 연결을 맺는 방식으로 구현했었습니다. 탭마다 다음과 같이 EventSource를 하나씩 열어지는 전형적인 패턴이었죠.

const source = new EventSource("/?mod=signin&act=procSseSignOut");
// ...

이렇게만 보면 큰 문제가 없어 보였고, QC팀 테스트 환경에서는 생각보다 심각한 이슈가 하나 튀어나왔습니다.
바로 HTTP/1.1 환경에서 브라우저가 호스트당 동시 연결 개수를 제한하고 있다는 점입니다.

SSE는 한 번 연결되면 오래 살아 있는 스트림이다 보니, 탭을 여러 개 띄우면 SSE 연결이 동시 연결 슬롯을 계속 잡아먹게 됩니다. 예를 들어 브라우저가 동일 도메인에 동시 6개 연결만 허용한다고 했을 때,

  • 그중 몇 개는 SSE가 계속 점유하고 있고
  • 남은 슬롯은 정적 리소스, API 호출 등이 나눠 써야 하다 보니

어느 순간부터는 새로 연 탭에서 HTML/CSS/JS 요청이 **pending** 상태로만 남고 화면이 끝까지 안 떠버리는 현상이 발생했습니다.
처음에는 서버나 네트워크 문제로 의심했다가, 개발자 도구(Network 탭)를 보면서 하나씩 추적해 본 끝에 “탭마다 따로 열어둔 SSE 연결”이 원인이라는 걸 확인할 수 있었습니다.

이 문제를 해결하기 위해, 결국

  • 브라우저(동일 origin) 당 SSE 연결은 1개만 유지하고
  • 그 결과를 SharedWorker → BroadcastChannel → 각 탭 순서로 뿌리는 구조로 아키텍처를 바꾸게 되었습니다.

아래에서 정리할 SharedWorker 구조는 이런 디버깅 히스토리 끝에 정착한 버전입니다.


아키텍처 개요


왜 SharedWorker인가?

Web Worker vs Shared Worker

SharedWorker는 동일 origin의 모든 탭/윈도우가 하나의 워커 인스턴스를 공유합니다. 이 특성을 활용하면 서버와의 연결을 단일화할 수 있습니다.


구현하기

1. SharedWorker 파일 (sse-worker.js)

const channel = new BroadcastChannel("sse_channel");
let source;

const MESSAGES = {
  logout: "logout",
  shutdown: "shutdown",
};

onconnect = function (e) {
  const port = e.ports[0];
  port.start();

  // 이미 연결되어 있으면 중복 연결 방지
  if (source) return;

  // SSE 연결 (단 1개만 생성)
  source = new EventSource("/?mod=signin&act=procSseSignOut");

  source.addEventListener("logout", ({ data: isLogOut }) => {
    channel.postMessage(isLogOut);
    if (isLogOut == 1) {
      channel.postMessage(MESSAGES.logout);
    }
  });

  source.addEventListener("error", async () => {
    channel.postMessage(MESSAGES.shutdown);
  });
};

핵심 포인트:

  • onconnect: 새 탭이 연결될 때마다 호출
  • if (source) return: 이미 SSE 연결이 있으면 중복 생성 방지
  • BroadcastChannel: 모든 탭에 메시지 전파

2. 클라이언트 코드 (각 탭에서 실행)

// SharedWorker 연결
const sseWorker = new SharedWorker("/common/js/workers/sse-worker.js");
sseWorker.onerror = function (error) {
  console.error("SharedWorker Error:", error);
};

sseWorker.port.start();  

// BroadcastChannel로 메시지 수신

const channel = new BroadcastChannel("sse_channel");

  

channel.onmessage = async (e) => {
  const message = e.data;
  if (message === "shutdown") {
    // 서버 연결 끊김 처리
    const isShutDown = await shutdownHandler();
    if (!isShutDown) return;

    window.location.href = "?mod=signin";
  } else if (message === "logout") {
    // 다른 곳에서 로그인하여 강제 로그아웃
    window.location.href = "?mod=signin&act=procSignOut&type=2";
  }
};

3. 서버 연결 복구 로직

async function shutdownHandler() {
  const maxRetries = 3;
  let retryCount = 0;
  
  while (retryCount < maxRetries) {
    await sleep(1000);
    const res = await checkServer();
  
    if (res.ok) {
      console.log("Server is up again");
      return false; // 복구됨, 로그아웃 불필요
    }
    retryCount++;
  }
  
  return true; // 복구 실패, 로그인 페이지로 이동
}

보안기능확인서 요구사항 충족

이 구현은 특히 동시접속 세션 제한(6.2) 같은 요구사항을 “프론트엔드(브라우저 탭) 관점”에서 안정적으로 만족시키기 위해 들어갔습니다. 핵심은 중복 로그인/세션 무효화 이벤트가 발생했을 때, ‘열려있는 모든 탭’이 빠짐없이 동일한 처리를 수행하게 만드는 것입니다.

요구사항 인용: 6.2 동시접속 세션 제한

  • 제품은 동일한 사용자 계정 또는 동일 권한으로 제품에 중복 접속 하는 것을 허용하지 않아야 한다.
  • 요구항목
      1. 사용자 로그인 이후 다른 단말기에서 동일 계정으로 로그인을 수행하는 경우 신규 접속을 차단하거나 이전 접속을 종료
      2. 동일 권한으로 중복 로그인을 허용하지 않아야 한다.
      3. 중복 접속 차단시 감사기록을 생성
  • 참고 사항
      1. 모니터링만 수행하는 관리자 계정에 대해서는 중복 로그인을 허용할 수 있다.
  • 점검시 유의사항
      1. 동일 PC 또는 다른 PC에서 동시 접속 시 차단 여부 확인
      2. 제품이 지원하는 모든 로컬·관리 접속(SSH, HTTPS 등)에 대해 확인

이 구현이 요구사항을 만족시키는 방식

요구사항 ①은 “신규 접속 차단” 또는 “이전 접속 종료” 중 하나를 택할 수 있는데, 이 제품에서는 이전 접속 종료를 선택했습니다. (운영 관점에서 신규 접속을 무조건 막기보다, 최신 접속을 살리고 기존 세션을 정리하는 편이 사용자 경험이 더 자연스러운 경우가 많았습니다.

전체 흐름은 아래처럼 동작합니다.

  1. 서버가 중복 로그인(동일 계정/권한)을 판단한다.
  2. 서버는 정책에 따라 기존 세션을 무효화하고(=이전 접속 종료), 그 사실을 브라우저에 SSE 이벤트(****logout****)로 통지한다.
  3. SharedWorker는 SSE 이벤트를 수신한 뒤 BroadcastChannel"logout"을 전파한다.
  4. 각 탭은 "logout"을 받는 즉시 동일한 로그아웃/리다이렉트 처리를 수행한다.

이때 SharedWorker를 사용하지 않으면 “탭마다 SSE 연결”이 생겨 탭마다 이벤트 수신 타이밍/예외 케이스가 달라질 수 있고, 최악의 경우 “어떤 탭은 살아있고 어떤 탭은 로그아웃된 상태”가 발생합니다. 단일 SSE 연결(단일 소스) 로 만들면 이런 불일치를 크게 줄일 수 있습니다.

1. 동시 로그인 탐지/통지(브라우저 관점)

  • 서버에서 SSE로 logout 이벤트 전송
  • 모든 탭이 즉시 알림을 받아 로그아웃 처리

2. 세션 무효화 동기화(“열려있는 모든 탭” 정리)

  • 관리자가 세션을 강제 종료하면 모든 탭에서 동시 로그아웃

3. 서버 연결 상태 모니터링(가용성 이슈 대응)

  • SSE 연결 끊김 감지 → 재연결 시도 → 실패 시 로그인 페이지 이동

4. 감사기록(요구항목 ③)과 예외 계정(참고 사항)

  • 감사기록 생성(③) 은 보통 서버 측 책임입니다. “누가/언제/어떤 경로로 중복 로그인했는지”는 신뢰 가능한 원천이 서버이기 때문에, 서버가 중복 접속을 판정하는 지점에서 감사 로그를 남기는 것이 자연스럽습니다. 프론트엔드는 그 결과(세션 종료 통지)를 빠짐없이 적용하는 역할에 집중합니다.
  • 모니터링 관리자 계정 예외 역시 정책은 서버가 판단합니다. 예외 계정으로 판단되면 서버가 기존 세션을 종료하지 않거나(또는 logout 이벤트를 보내지 않거나), 다른 이벤트 타입으로 분기할 수 있습니다. SharedWorker/탭 쪽은 “서버가 내려준 정책 결과를 모든 탭에 일관되게 적용”하는 데 집중합니다.
  • “SSH/HTTPS 등 모든 접속 경로(점검 유의사항 ②)”는 제품 전체 정책으로서 서버가 강제해야 합니다. 이 글의 구현은 그중 웹(HTTPS) UI에서 ‘여러 탭’이라는 특수한 실행 환경을 안정적으로 정리하기 위한 클라이언트 측 구성입니다.

BroadcastChannel을 함께 사용하는 이유

SharedWorker만으로도 탭 간 통신이 가능하지만, BroadcastChannel을 함께 사용하면:

  1. 코드 단순화: port 관리 없이 postMessage 한 번으로 모든 탭에 전파
  2. 유연성: SharedWorker 외부에서도 같은 채널로 메시지 송수신 가능
  3. 안정성: 특정 탭의 port 연결이 끊어져도 다른 탭에 영향 없음
// SharedWorker 내부
channel.postMessage("logout");  // 모든 탭에 전파

// 모든 탭에서
channel.onmessage = (e) => { ... };  // 동시 수신

주의사항

1. 브라우저 지원

  • SharedWorker: Chrome, Firefox, Edge 지원 (Safari 부분 지원)
  • 미지원 브라우저를 위한 폴백 구현 필요

2. 디버깅

  • Chrome: chrome://inspect/#workers에서 SharedWorker 디버깅 가능

3. HTTPS 환경

  • SharedWorker는 same-origin 정책 적용
  • 프로덕션에서는 HTTPS 필수

마치며

SharedWorker를 활용하면 단일 서버 연결로 모든 탭의 상태를 동기화할 수 있습니다. 특히 보안이 중요한 시스템에서 세션 관리, 동시 로그인 탐지 등의 요구사항을 효율적으로 구현할 수 있습니다.

참고

Shared Worker (MDN)
Broadcast Channel (MDN)