JavaScript의 Shared Worker

JavaScript의 Shared Worker

이 문서에서는 JavaScript의 Shared Worker에 대해 설명합니다.

Shared Worker의 기본부터 실제 사용 예시까지 단계별로 모두 설명하겠습니다.

YouTube Video

javascript-shared-worker.html
  1<!DOCTYPE html>
  2<html lang="en">
  3<head>
  4  <meta charset="UTF-8">
  5  <title>JavaScript &amp; HTML</title>
  6  <style>
  7    * {
  8        box-sizing: border-box;
  9    }
 10
 11    body {
 12        margin: 0;
 13        padding: 1em;
 14        padding-bottom: 10em;
 15        font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
 16        background-color: #f7f9fc;
 17        color: #333;
 18        line-height: 1.6;
 19    }
 20
 21    .container {
 22        max-width: 800px;
 23        margin: 0 auto;
 24        padding: 1em;
 25        background-color: #ffffff;
 26        border: 1px solid #ccc;
 27        border-radius: 10px;
 28        box-shadow: 0 4px 12px rgba(0, 0, 0, 0.05);
 29    }
 30
 31    .container-flex {
 32        display: flex;
 33        flex-wrap: wrap;
 34        gap: 2em;
 35        max-width: 1000px;
 36        margin: 0 auto;
 37        padding: 1em;
 38        background-color: #ffffff;
 39        border: 1px solid #ccc;
 40        border-radius: 10px;
 41        box-shadow: 0 4px 12px rgba(0, 0, 0, 0.05);
 42    }
 43
 44    .left-column, .right-column {
 45        flex: 1 1 200px;
 46        min-width: 200px;
 47    }
 48
 49    h1, h2 {
 50        font-size: 1.2rem;
 51        color: #007bff;
 52        margin-top: 0.5em;
 53        margin-bottom: 0.5em;
 54        border-left: 5px solid #007bff;
 55        padding-left: 0.6em;
 56        background-color: #e9f2ff;
 57    }
 58
 59    button {
 60        display: block;
 61        margin: 1em auto;
 62        padding: 0.75em 1.5em;
 63        font-size: 1rem;
 64        background-color: #007bff;
 65        color: white;
 66        border: none;
 67        border-radius: 6px;
 68        cursor: pointer;
 69        transition: background-color 0.3s ease;
 70    }
 71
 72    button:hover {
 73        background-color: #0056b3;
 74    }
 75
 76    #output {
 77        margin-top: 1em;
 78        background-color: #1e1e1e;
 79        color: #0f0;
 80        padding: 1em;
 81        border-radius: 8px;
 82        min-height: 200px;
 83        font-family: Consolas, monospace;
 84        font-size: 0.95rem;
 85        overflow-y: auto;
 86        white-space: pre-wrap;
 87    }
 88
 89    .highlight {
 90        outline: 3px solid #ffc107; /* yellow border */
 91        background-color: #fff8e1;  /* soft yellow background */
 92        transition: background-color 0.3s ease, outline 0.3s ease;
 93    }
 94
 95    .active {
 96        background-color: #28a745; /* green background */
 97        color: #fff;
 98        box-shadow: 0 0 10px rgba(40, 167, 69, 0.5);
 99        transition: background-color 0.3s ease, box-shadow 0.3s ease;
100    }
101  </style>
102</head>
103<body>
104    <div class="container">
105        <h1>JavaScript Console</h1>
106        <button id="executeBtn">Execute</button>
107        <div id="output"></div>
108    </div>
109
110    <script>
111        // Override console.log to display messages in the #output element
112        (function () {
113            // Override console.log
114            const originalLog = console.log;
115            console.log = function (...args) {
116                originalLog.apply(console, args);
117                const message = document.createElement('div');
118                message.textContent = args.map(String).join(' ');
119                output.appendChild(message);
120            };
121
122            // Override console.error
123            const originalError = console.error;
124            console.error = function (...args) {
125                originalError.apply(console, args);
126                const message = document.createElement('div');
127                message.textContent = args.map(String).join(' ');
128                message.style.color = 'red'; // Color error messages red
129                output.appendChild(message);
130            };
131        })();
132
133        document.getElementById('executeBtn').addEventListener('click', () => {
134            // Prevent multiple loads
135            if (document.getElementById('externalScript')) return;
136
137            const script = document.createElement('script');
138            script.src = 'javascript-shared-worker.js';
139            script.id = 'externalScript';
140            //script.onload = () => console.log('javascript-shared-worker.js loaded and executed.');
141            //script.onerror = () => console.log('Failed to load javascript-shared-worker.js.');
142            document.body.appendChild(script);
143        });
144    </script>
145</body>
146</html>

JavaScript의 Shared Worker

Shared Worker란 무엇인가요?

Shared Worker동일 오리진(origin) 내의 여러 페이지(탭, 창, iframe 등)에서 공유할 수 있는 워커 스레드입니다. 페이지별 Dedicated Worker와 달리, 하나의 백그라운드 프로세스를 여러 페이지에서 공유할 수 있다는 점이 주요 특징입니다. 주요 사용 예시는 다음과 같습니다:.

  • 하나의 WebSocket 연결을 여러 탭에서 공유하여 연결 수를 줄이고, 재연결을 중앙집중식으로 관리할 수 있습니다.
  • 탭 간 상태를 동기화할 수 있습니다(Pub/Sub 또는 상태 저장소의 중앙 관리 등).
  • 동시 접근을 조정하여 IndexedDB 작업을 직렬화할 수 있습니다.
  • 계산 비용이 큰 프로세스의 중복 실행을 방지할 수 있습니다.

Shared Worker는 **Service Worker**와는 다른 역할을 합니다. Service Worker는 주로 네트워크 프록시 역할을 하며, Shared Worker여러 페이지에서 공유되는 임의의 계산이나 상태 관리를 수행하는 것에 중점을 둡니다.

기본 API 및 생명주기

생성하기 (메인 스레드에서)

 1// main.js
 2// The second argument 'name' serves as an identifier to share the same SharedWorker
 3// if both the URL and name are the same.
 4const worker = new SharedWorker('/shared-worker.js', { name: 'app-core' });
 5
 6// Get the communication port (MessagePort) with this SharedWorker
 7const port = worker.port;
 8
 9// Receive messages with onmessage, send messages with postMessage
10port.onmessage = (ev) => {
11    console.log('From SharedWorker:', JSON.stringify(ev.data));
12};
13
14// When using onmessage, start() is not required
15// (start() is needed when using addEventListener instead)
16port.postMessage({ type: 'hello', from: location.pathname });
  • 이 코드는 Shared Worker를 생성하고 포트를 통해 메시지를 주고받는 방법을 보여줍니다.

Shared Worker 범위 안의 수신자

 1// shared-worker.js
 2// SharedWorkerGlobalScope has an onconnect event,
 3// and a MessagePort is provided for each connection
 4const ports = new Set();
 5
 6onconnect = (e) => {
 7    const port = e.ports[0];
 8    ports.add(port);
 9
10    port.onmessage = (ev) => {
11        // Log the received message and send a reply back to the sender
12        console.log('From page:', ev.data);
13        port.postMessage({ type: 'ack', received: ev.data });
14    };
15
16    // For handling explicit disconnection from the page (via MessagePort.close())
17    port.onmessageerror = (err) => {
18        console.error('Message error:', err);
19    };
20
21    // Greeting immediately after connection
22    port.postMessage({ type: 'welcome', clientCount: ports.size });
23};
24
25// Explicit termination of the SharedWorker is usually unnecessary
26// (It will be garbage collected when all ports are closed)
  • 이 코드는 Shared Worker 내에서 각 연결마다 MessagePort를 수신하고, 클라이언트의 메시지를 처리·응답하는 방법을 보여줍니다.

Shared Worker의 생명주기

Shared Worker첫 번째 연결이 생성될 때 시작하고, 마지막 포트가 닫히면 종료될 수 있습니다. 페이지를 새로 고침하거나 닫으면 연결이 종료되고, 필요하면 워커가 새로 생성됩니다.

Shared Worker는 "브라우저 내에서 공유되는 장시간 실행되는 스크립트"입니다. Shared Worker의 생명주기를 신경 쓰지 않으면, 리소스 누수, 오래된 상태의 지속, 의도치 않은 재시작과 같은 문제가 발생할 수 있습니다.

실습: 탭 간 브로드캐스트

여러 탭이 동일한 Shared Worker에 연결하고, 어느 탭에서든 보낸 메시지가 모두에게 브로드캐스트되는 최소 구현 예시입니다..

main.js

 1const worker = new SharedWorker('/shared-worker.js', { name: 'bus' });
 2const port = worker.port;
 3
 4port.onmessage = (ev) => {
 5    const msg = ev.data;
 6    if (msg.type === 'message') {
 7        const li = document.createElement('li');
 8        li.textContent = `[${new Date(msg.at).toLocaleTimeString()}] ${msg.payload}`;
 9        document.querySelector('#messages').appendChild(li);
10    } else if (msg.type === 'ready') {
11        console.log(`Connected. clients=${msg.clients}`);
12    }
13};
14
15document.querySelector('#form').addEventListener('submit', (e) => {
16    e.preventDefault();
17    const input = document.querySelector('#text');
18    port.postMessage({ type: 'publish', payload: input.value });
19    input.value = '';
20});
21
22document.querySelector('#ping').addEventListener('click', () => {
23    port.postMessage({ type: 'ping' });
24});
  • 이 코드는 Shared Worker에 연결하여, 메시지 수신 및 표시, 전송 폼에서 메시지 보내기, 버튼 클릭 시 ping 전송 기능을 구현합니다.

shared-worker.js

 1// Minimal pub/sub bus in a SharedWorker
 2const subscribers = new Set();
 3
 4/** Broadcast to all connected ports */
 5function broadcast(message) {
 6    for (const port of subscribers) {
 7        try {
 8            port.postMessage(message);
 9        } catch (e) {
10            // Unsubscribe if sending fails, just in case
11            subscribers.delete(port);
12        }
13    }
14}
15
16onconnect = (e) => {
17    const port = e.ports[0];
18    subscribers.add(port);
19
20    port.onmessage = (ev) => {
21        const msg = ev.data;
22        if (msg && msg.type === 'publish') {
23            broadcast({ type: 'message', payload: msg.payload, at: Date.now() });
24        } else if (msg && msg.type === 'ping') {
25            port.postMessage({ type: 'pong', at: Date.now() });
26        }
27    };
28
29    port.postMessage({ type: 'ready', clients: subscribers.size });
30};
  • 이 코드는 Shared Worker 내에서 간단한 pub/sub 기능을 구현하여 여러 클라이언트 간에 메시지를 중계합니다.

index.html

 1<!doctype html>
 2<html>
 3    <body>
 4        <form id="form">
 5            <input id="text" placeholder="say something" />
 6            <button>Send</button>
 7        </form>
 8        <button id="ping">Ping</button>
 9        <ul id="messages"></ul>
10        <script src="/main.js" type="module"></script>
11    </body>
12</html>
  • 이 페이지를 여러 탭에서 열면, 어느 탭에서든 보낸 메시지가 다른 탭에도 안내됩니다.

메시지 설계: 요청/응답 및 Correlation ID

여러 클라이언트가 Shared Worker와 상호작용할 때, 어떤 응답이 어떤 요청에 대응하는지 식별하고 싶은 경우가 많습니다. 따라서 요청과 응답을 연결하기 위해 상관 관계 ID를 포함하는 것이 표준적인 관례입니다.

main.js

 1function createClient(workerUrl, name) {
 2    const w = new SharedWorker(workerUrl, { name });
 3    const port = w.port;
 4    const pending = new Map(); // id -> {resolve,reject}
 5    let nextId = 1;
 6
 7    port.onmessage = (ev) => {
 8        const { id, ok, result, error } = ev.data || {};
 9        if (!id || !pending.has(id)) return;
10        const { resolve, reject } = pending.get(id);
11        pending.delete(id);
12        ok ? resolve(result) : reject(new Error(error));
13    };
14
15    function call(method, params) {
16        const id = nextId++;
17        return new Promise((resolve, reject) => {
18            pending.set(id, { resolve, reject });
19            port.postMessage({ id, method, params });
20        });
21    }
22
23    return { call };
24}
25
26// usage
27const client = createClient('/shared-worker.js', 'rpc');
28client.call('add', { a: 2, b: 3 }).then(console.log);   // -> 5
29client.call('sleep', { ms: 500 }).then(console.log);     // -> 'done'
  • 이 코드는 Shared Worker에게 비동기 방식으로 메서드를 호출할 수 있는 간단한 RPC 클라이언트를 구현한 예시입니다. 여기서 RPC 서버(원격 프로시저 호출 서버)는 다른 프로그램이나 프로세스에서 함수와 프로시저를 호출할 수 있는 메커니즘을 제공하는 서버를 의미합니다.
  • 이 예시에서는 id가 단순히 1씩 증가하지만, UUID나 임의의 문자열, 혹은 타임스탬프와 카운터를 조합하여 고유한 키를 만들 수도 있습니다.

shared-worker.js

 1// A small request/response router with correlation ids
 2onconnect = (e) => {
 3    const port = e.ports[0];
 4
 5    port.onmessage = async (ev) => {
 6        const { id, method, params } = ev.data || {};
 7        try {
 8            let result;
 9            switch (method) {
10                case 'add':
11                    result = (params?.a ?? 0) + (params?.b ?? 0);
12                    break;
13                case 'sleep':
14                    await new Promise(r => setTimeout(r, params?.ms ?? 0));
15                    result = 'done';
16                    break;
17                default:
18                    throw new Error(`Unknown method: ${method}`);
19            }
20            port.postMessage({ id, ok: true, result });
21        } catch (err) {
22            port.postMessage({ id, ok: false, error: String(err) });
23        }
24    };
25};
  • 이 코드는 method에 따라 처리를 실행하고, 그 결과를 요청 ID와 함께 다시 보내는 간단한 RPC 서버를 구현한 것입니다.

대표적 사용 패턴

Shared Worker 사용 시에는 다음과 같은 여러 실용적인 패턴이 있습니다:.

하나의 WebSocket 다중화 (여러 탭에서 공유)

각 탭이 개별적으로 WebSocket 연결을 열면 서버 부하 및 연결 제한에 영향을 줄 수 있습니다. Shared WorkerWebSocket을 하나만 두고, 각각의 탭은 워커를 통해 메시지를 주고받게 합니다.

main.js

 1const w = new SharedWorker('/shared-worker.js', { name: 'ws' });
 2const port = w.port;
 3
 4port.onmessage = (ev) => {
 5    const msg = ev.data;
 6    if (msg.type === 'data') {
 7        console.log('Server says:', msg.payload);
 8    } else if (msg.type === 'socket') {
 9        console.log('WS state:', msg.state);
10    }
11};
12
13function send(payload) {
14    port.postMessage({ type: 'send', payload });
15}
  • 이 코드는 WebSocket 서버로 메시지를 주고받고, 연결 상태 알림을 Shared Worker를 통해 전달받는 과정을 구현합니다.

shared-worker.js

 1let ws;
 2const ports = new Set();
 3
 4function ensureSocket() {
 5    if (ws && (ws.readyState === WebSocket.OPEN || ws.readyState === WebSocket.CONNECTING)) {
 6        return;
 7    }
 8    ws = new WebSocket('wss://example.com/stream');
 9    ws.onopen = () => broadcast({ type: 'socket', state: 'open' });
10    ws.onclose = () => broadcast({ type: 'socket', state: 'closed' });
11    ws.onerror = (e) => broadcast({ type: 'socket', state: 'error', detail: String(e) });
12    ws.onmessage = (m) => broadcast({ type: 'data', payload: m.data });
13}
14
15function broadcast(msg) {
16    for (const p of ports) p.postMessage(msg);
17}
18
19onconnect = (e) => {
20    const port = e.ports[0];
21    ports.add(port);
22    ensureSocket();
23
24    port.onmessage = (ev) => {
25        const msg = ev.data;
26        if (msg?.type === 'send' && ws?.readyState === WebSocket.OPEN) {
27            ws.send(JSON.stringify(msg.payload));
28        }
29    };
30};
  • 이 코드는 Shared Worker 내부에서 하나의 WebSocket 연결을 공유, 연결 상태와 수신 데이터를 모든 클라이언트에 브로드캐스트하면서 각 클라이언트의 요청을 WebSocket을 통해 서버로 전송하는 구조를 구현합니다.

  • Shared Worker에서 재연결 전략과 전송되지 않은 메시지 큐잉을 중앙집중화함으로써, 모든 탭에서 동작을 일관되게 할 수 있습니다.

IndexedDB 중재(직렬화)

여러 탭에서 같은 DB에 접근할 때, 동시 트랜잭션으로 인한 충돌이나 락 대기를 피하려면, Shared Worker에서 작업을 큐잉해서 직렬 처리하도록 할 수 있습니다.

 1// db-worker.js (very simplified – error handling omitted)
 2let db;
 3async function openDB() {
 4    if (db) return db;
 5    db = await new Promise((resolve, reject) => {
 6        const req = indexedDB.open('appdb', 1);
 7        req.onupgradeneeded = () => req.result.createObjectStore('kv');
 8        req.onsuccess = () => resolve(req.result);
 9        req.onerror = () => reject(req.error);
10    });
11    return db;
12}
13
14async function put(key, value) {
15    const d = await openDB();
16    return new Promise((resolve, reject) => {
17        const tx = d.transaction('kv', 'readwrite');
18        tx.objectStore('kv').put(value, key);
19        tx.oncomplete = () => resolve(true);
20        tx.onerror = () => reject(tx.error);
21    });
22}
23
24async function get(key) {
25    const d = await openDB();
26    return new Promise((resolve, reject) => {
27        const tx = d.transaction('kv', 'readonly');
28        const req = tx.objectStore('kv').get(key);
29        req.onsuccess = () => resolve(req.result);
30        req.onerror = () => reject(req.error);
31    });
32}
33
34onconnect = (e) => {
35    const port = e.ports[0];
36    port.onmessage = async (ev) => {
37        const { id, op, key, value } = ev.data;
38        try {
39            const result = op === 'put' ? await put(key, value) : await get(key);
40            port.postMessage({ id, ok: true, result });
41        } catch (err) {
42            port.postMessage({ id, ok: false, error: String(err) });
43        }
44    };
45};
  • 이 코드는 IndexedDB 읽기/쓰기를 Shared Worker를 통해 큐잉하여, 여러 탭에서의 접근을 직렬화하고 충돌 및 락 경합을 방지하는 방법을 구현합니다.

모듈화 및 번들러 팁

Shared Worker 스크립트를 ES 모듈 형식으로 작성하고 싶을 때, 환경에 따라 동작 및 지원이 다를 수 있습니다. 실제 환경에서는 아래 중 한 가지 방식을 사용하는 것이 더 안전합니다:.

  • 클래식 워커 형식으로 작성하고, 필요 시 importScripts()로 의존성을 불러옵니다.
  • 번들러(Vite, Webpack, esbuild 등)의 워커 엔트리 기능을 사용해, 빌드 시 Shared Worker를 별도 번들로 생성합니다.

에러 처리, 연결 해제 감지, 견고성을 위한 팁

에러 처리와 견고성을 위해 다음 항목들을 고려하세요:.

  • 연결이 확립되기 전의 메시지 전송 처리 포트가 준비되기 전에 도착한 메시지는 큐에 저장하세요.

  • 연결 해제 감지 MessagePort에는 표준 onclose 핸들러가 없습니다. 메인 측에서는 beforeunloadport.postMessage{type: 'bye'} 메시지를 보내거나, 그렇지 않을 경우에는 프로토콜에서 명확하게 지정하여 워커가 정리되도록 해야 합니다.

  • 재연결 페이지를 새로 고침하거나 탭을 다시 열면 새로운 포트가 생성됩니다. 전체 상태를 한 번에 보낼 수 있도록 초기 동기화 메시지를 준비합니다.

  • 백프레셔(Backpressure) 브로드캐스트가 빈번할 경우, 쓰로틀링/디바운싱 또는 스냅샷 전송으로 전환하세요.

  • 보안(Security) Shared Worker같은 오리진 내에서만 원칙적으로 공유됩니다. 워커 내부에 비밀 정보를 둘 경우, 호출 측에서 토큰 등으로 부가 인증 절차를 설계하는 것을 고려하세요.

Dedicated Worker, Shared Worker, Service Worker를 적절하게 사용하는 방법

각 워커 유형은 다음과 같은 특징을 가지고 있습니다:.

  • Dedicated Worker Dedicated Worker는 단일 페이지에서만 사용하도록 설계되었습니다. UI와 연산을 1:1로 분리할 수 있습니다.

  • Shared Worker Shared Worker는 동일 출처의 여러 페이지에서 공유될 수 있습니다. 탭 간 통신단일 연결 공유에 최적입니다.

  • Service Worker Service Worker는 네트워크 프록시, 캐싱, 오프라인 작업, 푸시 알림, 백그라운드 동기화 등에 사용할 수 있습니다. 가장 큰 장점은 fetch 요청을 가로채는 기능입니다.

실무 팁: 탭 간의 '정보 공유 및 임의 처리'에는 Shared Worker, '네트워크 제어'에는 Service Worker, UI에서 무거운 작업만 분리하고 싶을 경우에는 Dedicated Worker를 사용하세요.

일반적인 실수

Shared Worker를 사용할 때는 다음 사항에 주의해야 합니다.

  • start() 호출을 깜빡하거나, 불필요하게 여러 번 호출하는 경우 port.addEventListener('message', ...)를 사용할 때는 반드시 port.start()를 호출해야 합니다. port.onmessage = ...을 사용할 경우에는 이 호출이 필요하지 않습니다.

  • 무제한 브로드캐스팅 탭 수가 늘어나면 부하도 증가합니다. 차등 전달 또는 구독 주제(토픽별 필터링)를 도입하여 부하를 줄일 수 있습니다.

  • 객체 복사 비용 postMessage는 데이터를 복제합니다. 대용량 데이터는 Transferable(ArrayBuffer 등)로 전송하거나, Atomics와 함께 **공유 메모리(SharedArrayBuffer)**를 사용하는 것을 고려하세요.

  • 수명 및 재초기화 마지막 클라이언트가 연결을 끊으면 Shared Worker는 종료될 수 있습니다. 부작용 및 버그를 방지하기 위해 최초 연결 및 재시작 시의 초기화를 제대로 설계하세요.

요약

  • Shared Worker여러 페이지에서 공유되는 장기 실행 맞춤 로직을 구현하는 장소입니다.
  • 메시지 계약(타입/프로토콜)을 명확히 하고 상관 관계 ID를 붙여 요청/응답 설계를 견고하게 하세요.
  • WebSocket 연결 멀티플렉싱 또는 IndexedDB 접근 직렬화 등, 모든 탭에서 통합할 때 더 잘 동작하는 프로세스에 최적입니다.
  • 번들러, 타입 정의, 재접속 등 세부 조정은 유지 보수성 및 사용자 경험을 크게 향상시킬 수 있습니다.

위의 기사를 보면서 Visual Studio Code를 사용해 우리 유튜브 채널에서 함께 따라할 수 있습니다. 유튜브 채널도 확인해 주세요.

YouTube Video