Web Worker 란?
웹 워커는 스크립트 실행을 메인 스레드와 분리된 별도의 백그라운드 스레드에서 실행할 수 있는 기술입니다. 메인 스레드에서 수행하기 버거운 무거운 연산들을 별도의 스레드에게 위임하는 방식으로 활용할 수 있습니다.
웹 워커를 생성하기 위해서는 워커가 실행할 스크립트가 기술되어 있는 자바스크립트 파일의 경로를 인자로 제공해야 합니다. 다만 Webpack, Vite, Parcel 같은 번들러를 사용하는 환경이라면 상대경로를 조금 더 명확하게 제공해주는 것이 좋습니다.
// 바닐라 환경 const myWorker = new Worker("worker.js"); // 번들러 환경 const myWorker = new Worker(new URL('worker.js', import.meta.url));
웹 워커와 통신하기
메인 스레드와 워커는 기본적으로 메세지를 이용해 통신을 합니다.
// 메인 스레드 myWorker.postMessage("Hello, World!");
postMessage
는 반드시 “직렬화된 데이터”만 인자로 받을 수 있습니다. 즉, 함수와 같이 직렬화 불가능한 값은 인자로 전달할 수 없습니다. (참고 링크)워커는 다음과 같이
e.data
를 이용해 메세지를 받을 수 있습니다.// 워커 onmessage = e => console.debug(`Message from main thread: ${e.data}`);
웹 워커 종료하기
만약 웹 워커를 종료하고 싶다면 다음과 함수를 호출하여 종료할 수 있습니다.
// 메인 스레드 myWorker.terminate();
웹 워커의 종류
웹 워커에는 크게 세 가지 종류가 있습니다.
- 전용 워커(Dedicated workers)
- 공유 워커(Shared workers)
- 서비스 워커(Service Workers)
전용 워커
전용 워커는 단일 스크립트를 실행하는 것에 최적화된 워커입니다. 전용 워커는 자신을 호출한 스레드에서만 접근이 가능합니다.
예를 들어 간단한 계산을 하는 스크립트가 있다고 가정해 볼까요?
// calculate.js function add(a, b) { return a + b; } console.debug(`3 + 4 is ${add(3, 4)}`);
위 스크립트를 별도의 스레드로 실행할 수 있습니다.
// 메인 스레드 const myWorker = new Worker('calculate.js');
이제 메인 스레드가 실행되면 다음과 같이 웹 워커에서
calculate.js
스크립트를 실행합니다.
혹은 특정 스크립트 파일을 가져와 실행하는 웹 워커를 실행할 수도 있습니다.
// worker.js importScripts("calculate.js");
// 메인 스레드 const myWorker = new Worker('worker.js');
공유 워커
공유 워커는 다양한 브라우징 컨텍스트(예를 들어 iFrame, 여러 창 혹은 워커)에서 여러가지 스크립트를 실행하는데 최적화된 워커입니다. 공유 워커는
port
를 통해 통신을 하는 것이 특징입니다.공유 워커는 전용 워커와는 다른 인터페이스를 상속하며, SharedWorkerGlobalScope 라고 하는 다른 전역 스코프를 가지고 있습니다.
다만 이 브라우징 컨텍스트들은 모두 동일한 오리진이어야 합니다.
// 메인 스레드 const myWorker = new SharedWorker('worker.js'); myWorker.port.onmessage = e => console.log(`Message received from worker: ${e.data}`); myWorker.port.start(); <input type="text" onChange={e => myWorker.port.postMessage(e.target.value)} />
// worker.js onconnect = e => { const port = e.ports[0]; port.addEventListener('message', e => { const workerResult = `Result: ${e.data}`; port.postMessage(workerResult); }); // addEventListener 를 사용하기 위해서는 다음 코드가 필수입니다. // 아니라면, onmessage 메소드에서 암시적으로 호출합니다. port.start(); };
메인 스레드의 포트와 연결되기 위해 워커에서는
onconnnect
이벤트를 이용하며, e.ports
를 통해 (공유 워커의 포트에) 큐잉된 메세지들을 메인 스레드의 포트에 전송합니다.

이렇게 서로 다른 브라우징 컨텍스트에서 동일한 웹 워커에 접근할 수 있습니다.
서비스 워커
서비스 워커는 기본적으로 프록시 서버의 역할을 수행합니다. 서비스 워커는 오프라인 경험을 개선하기 위한 목적으로 만들어졌습니다. 따라서 네트워크 요청을 가로채거나, 네트워크 연결 여부에 따라 적절한 조치를 취하는 등의 역할을 수행하는게 좋습니다. 추가로, 푸시 알림이나 백그라운드 동기화 API도 사용이 가능합니다.
이 서비스 워커를 잘 활용하는 라이브러리가 바로 MSW죠.
스레드 안정성에 대해
웹 워커 인터페이스는 실제로 OS 수준의 스레드를 생성합니다.
스레드라고 한다면 당연히 공유 메모리와 같은 동시성과 관련된 문제들이 신경 쓰이실텐데요, 다행히 스레드 불안전 컴포넌트나 DOM에 접근할 수 없고, 직렬화된 데이터만 주고받을 수 있기 때문에 안전하다고 합니다.
웹 워커에서 사용가능한 함수 및 인터페이스들
- Navigator
- fetch API
- Array, Date, Math, String
- setTimeout, setInterval
- 등등
메인 스레드에 영향을 줄 수 있는 것들은 워커에서 사용할 수 없습니다. 따라서, DOM에 접근하는 것도 불가능합니다.
개인적인 감상
저는 실무에서 MSW를 제외하고는 웹 워커를 사용해본 적이 없는데요, 이번에 갑자기 궁금해져서 이리저리 찾아보고 테스트해봤습니다.
만약 동적으로 특정 함수를 위임하고 싶다면
eval
을 활용할 수도 있는게 재밌더라구요.// 메인 스레드에서 즉시실행 함수 표현식을 문자열로 보냅니다. myWorker.postMessage(`(() => 'im stringfied function!')()`); // 웹 워커에서 해당 문자열을 받아 eval 로 실행합니다. onmessage = e => { const workerResult = `Result: ${eval(e.data)}`; };
물론
eval
은 엄청 위험한 녀석이니 조심해서 사용해야 합니다. 심지어 웹 워커는 자신만의 실행 컨텍스트를 가지므로, 워커 호출자(메인 스레드)의 Content Security Policy 도 적용받지 않으니 더욱 조심해야 합니다.이 외에더 더 많은 정보들이 있지만, 이번 기록에서는 간단하게 무슨 종류가 있는지, 어떻게 사용하는지, 대표적인 주의점은 무엇인지 간단하게 훑어보았습니다.
참고
https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API