Project··4 min read·

SSE와 Shared Worker — 실시간 통신의 두 가지 접근

Server-Sent Events로 서버 → 클라이언트 단방향 스트리밍을 구현하는 방법과, Shared Worker로 탭 간 상태를 공유하는 방법을 정리했다.

SSE와 Shared Worker — 실시간 통신의 두 가지 접근

왜 이 둘을 같이 보는가

실시간 기능을 구현할 때 자주 마주치는 두 가지 도구다.

  • SSE: 서버가 클라이언트에게 데이터를 밀어주는 단방향 채널
  • Shared Worker: 여러 탭이 하나의 백그라운드 스레드를 공유하는 방법

SSE로 서버 이벤트를 받고, Shared Worker로 그 이벤트를 여러 탭에 분배하면 강력한 조합이 된다.


SSE — Server-Sent Events

WebSocket과 비교

WebSocketSSE
방향양방향서버 → 클라이언트 단방향
프로토콜ws:// / wss://HTTP
자동 재연결직접 구현브라우저가 자동 처리
텍스트/바이너리둘 다텍스트만
방화벽 통과간혹 문제HTTP이므로 우수

데이터를 서버에서만 클라이언트로 보내는 경우(알림, 피드, 진행 상황)라면 SSE가 더 단순하다.

SSE 동작은 단순하다 — 자세한 건 레퍼런스로

서버는 응답을 끊지 않고 text/event-stream으로 이벤트를 흘려보내고, 클라이언트는 EventSource 하나로 받는다. data / event / id / retry 네 필드, Last-Event-ID 기반 자동 재연결, EventSource의 헤더 지정 제약, HTTP/2와의 궁합 같은 SSE의 포맷·API 동작은 개념 레퍼런스에 따로 정리했다.

Server-Sent Events — HTTP 위의 단방향 스트림

이 글은 그 SSE를 여러 탭이 공유하도록 Shared Worker와 묶는 부분에 집중한다.


Shared Worker

라이프사이클(누가 살리고 죽이나), MessagePort, onconnect, Safari iOS 미지원, 디버깅 같은 Shared Worker 자체의 동작은 Shared Worker — 여러 탭이 공유하는 워커에 자세히 정리했다. 여기서는 SSE 연결을 공유하는 데 필요한 만큼만 본다.

Worker 종류 비교

Web WorkerShared WorkerService Worker
생명주기탭과 동일모든 탭이 닫힐 때 종료브라우저가 별도 관리
탭 간 공유
주 용도무거운 연산탭 간 상태 공유오프라인/캐싱
통신 방식postMessageMessagePortpostMessage

문제 상황: 탭마다 SSE 연결이 생긴다

탭을 5개 열면 서버에 SSE 연결이 5개 생긴다. Shared Worker를 쓰면 연결이 1개로 줄어든다.

열린 탭 수:
🌐1EventSource
🌐2EventSource
🌐3EventSource
——→——→——→
🖥️서버SSE × 3연결
⚠️ 탭이 3개 열리면 서버에 SSE 연결도 3개가 생깁니다. 탭이 많아질수록 서버 부하가 증가합니다.
code
[탭1] ──┐
[탭2] ──┤── Shared Worker ── SSE → 서버
[탭3] ──┘

Shared Worker 구현

js
// public/shared-worker.js
const ports = new Set();
let es = null;

function startSSE() {
  es = new EventSource('/api/events');

  es.onmessage = (event) => {
    // 연결된 모든 탭에 브로드캐스트
    for (const port of ports) {
      port.postMessage({ type: 'sse', data: JSON.parse(event.data) });
    }
  };

  es.onerror = () => {
    for (const port of ports) {
      port.postMessage({ type: 'error' });
    }
  };
}

// 탭이 Worker에 연결될 때
self.onconnect = (event) => {
  const port = event.ports[0];
  ports.add(port);

  // 첫 탭 연결 시 SSE 시작
  if (!es) startSSE();

  port.onmessage = (e) => {
    // 탭 → Worker 메시지 처리 (양방향 통신 가능)
    if (e.data.type === 'send') {
      // 탭에서 보낸 데이터를 다른 탭에 relay
      for (const p of ports) {
        if (p !== port) p.postMessage(e.data);
      }
    }
  };

  // 탭이 닫히면 정리
  port.addEventListener('close', () => {
    ports.delete(port);
    if (ports.size === 0) {
      es?.close();
      es = null;
    }
  });

  port.start();
};

탭(클라이언트)에서 Shared Worker 사용

ts
// lib/shared-worker-client.ts
let worker: SharedWorker | null = null;

export function getSharedWorker() {
  if (!worker && typeof SharedWorker !== 'undefined') {
    worker = new SharedWorker('/shared-worker.js');
    worker.port.start();
  }
  return worker;
}

// React 훅
export function useSharedWorker() {
  const [message, setMessage] = useState<unknown>(null);

  useEffect(() => {
    const sw = getSharedWorker();
    if (!sw) return;

    const handler = (event: MessageEvent) => {
      setMessage(event.data);
    };

    sw.port.addEventListener('message', handler);
    return () => sw.port.removeEventListener('message', handler);
  }, []);

  const send = useCallback((data: unknown) => {
    getSharedWorker()?.port.postMessage(data);
  }, []);

  return { message, send };
}

타입스크립트에서 Shared Worker 타입 지정

ts
// tsconfig.json에 "lib": ["webworker.sharedworker"] 추가 필요
// 또는 /// <reference lib="webworker" /> 선언

SSE + Shared Worker 조합

code
서버 ──SSE──→ Shared Worker ──postMessage──→ 탭1
                                          ──postMessage──→ 탭2
                                          ──postMessage──→ 탭3
js
// shared-worker.js — 실전 버전
const ports = new Set();

function broadcast(msg) {
  for (const port of ports) port.postMessage(msg);
}

// Last-Event-ID 추적으로 재연결 시 누락 방지
let lastEventId = '';

function connect() {
  const url = lastEventId
    ? `/api/events?lastEventId=${lastEventId}`
    : '/api/events';

  const es = new EventSource(url);

  es.onmessage = (e) => {
    lastEventId = e.lastEventId;
    broadcast({ type: 'message', data: JSON.parse(e.data) });
  };

  es.onerror = () => {
    es.close();
    // 5초 후 재연결 (브라우저 자동 재연결 대신 수동 제어)
    setTimeout(connect, 5000);
  };
}

self.onconnect = (e) => {
  const port = e.ports[0];
  ports.add(port);
  if (ports.size === 1) connect();
  port.start();
};

정리

SSEShared Worker
역할서버 → 클라이언트 스트리밍탭 간 공유 백그라운드 스레드
연결 대상서버브라우저 내부
핵심 장점HTTP 기반, 자동 재연결, 단순 API연결 수 감소, 탭 간 상태 동기화
주의 사항단방향 (서버→클라이언트만)Safari 지원 제한적, SharedWorker 미지원 환경 폴백 필요

실시간 알림, 라이브 피드처럼 서버 → 클라이언트 단방향 데이터 흐름에는 WebSocket 대신 SSE가 더 적합하다. 여러 탭이 동일한 연결을 공유해야 한다면 Shared Worker와 조합하면 서버 부하를 크게 줄일 수 있다.

Related Posts

같이 읽으면 좋은 글