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 & 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 Worker
에 WebSocket을 하나만 두고, 각각의 탭은 워커를 통해 메시지를 주고받게 합니다.
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
핸들러가 없습니다. 메인 측에서는beforeunload
시port.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를 사용해 우리 유튜브 채널에서 함께 따라할 수 있습니다. 유튜브 채널도 확인해 주세요.