자바스크립트는 항상 단일 스레드(Single Thread)로 동작한다. 예를 들어 HTML 페이지에서 스크립트를 실행할 때 스크립트가 완료될 때까지 기다려야 한다. 심지어 setTimeout
, setInterval
, 그리고 ajax callbacks
들조차도 다중 스레드(Multi Thread)처럼 보이지만 실제로는 단일 스레드로 구동된다(event loops 를 사용한다). 일단 실행되면 차례를 기다려야 하는 것이다.
평소에는 단일 스레드로도 충분하지만, 로딩과 실행이 오래 걸리는 집약적인 코드(매우 큰 문자열의 암호화,복호화/복잡한 연산/큰 배열 등등)가 있는 경우 다중 스레드가 필요하다.
이때 Web Worker를 사용하면 자바스크립트에서도 다중 스레드(Multi Thread) 구동이 가능하다.
Web Worker는 개발자가 다중 스레드 프로세스를 제어할 수 있는 자바스크립트 API이다. 공유 메모리에 대한 동시 접근(다른 스레드에서 값을 읽고 쓰는)을 처리하는 방법을 제공한다.
자바스크립트와 달리 브라우저는 다중 스레드로 동작한다. 네트워크 통신이나 I/O들은 서로 다른 스레드에서 동작한다.
Web Worker를 사용하면 브라우저의 메인 스레드(main thread)와 별개로 동작하는 스레드를 생성할 수 있다. 이렇게 생성한 스레드는 메인 스레드main thread에서의 렌더링 동작을 방해하지 않는다.
Web Worker를 실행할 수 있는 별도의 자바스크립트 파일(ex. worker.js)과, Web Worker를 생성할 자바스크립트 파일(ex. main.js)이 각각 필요하다.
main thread에서 worker thread를 생성하고 (const myWorker = new Worker('worker.js');
), worker thread의 메시지를 받기 위해 onmessage 이벤트 핸들러를 추가한다.
// (main.js)
if (window.Worker) { // Web Worker가 호환되는지 확인
const myWorker = new Worker('worker.js');
myWorker.onmessage = (e) => {
console.log(e.data); // 'result'
};
}
worker thread에서 작업 처리가 완료되면 postMessage()
로 데이터를 main thread의 myWorker.onmessage
에 전달한다.
// (worker.js)
let data = '';
// ... 무거운 연산 수행 후
data = 'result';
postMessage(data);
작업이 끝났거나 worker thread가 필요없다면 terminate() 을 호출한다.
myWorker.terminate();
main thread에서 window는 GlobalScope이다.
worker thread에서 window는 WorkerGlobalScope이다. 그러므로 worker thread는 main thread의 window 메서드나 DOM 조작이 불가능하다. worker thread는 Message System을 통해서만 데이터를 주고 받을 수 있다.
main thread에서는 WorkerGlobalScope에 접근할 수 있다.
self.onmessage = () => {};
worker가 더 필요하다면 자식 worker를 생성할 수 있다.
// worker.js
let subWorker = new Worker('sub_worker.js');
subWorker.onmessage = function (e) {
console.log(e.data);
}
subWorker.postMessage('test');
postMessage 메서드를 이용해 부모나 자식 thread에게 데이터를 전달할 수 있고, onmessage를 이용해 부모나 자식 thread가 전달하는 데이터를 수신할 수 있다.
main thread와 worker thread 간 주고받는 데이터는 공유되지 않고 복사된다. main thread와 worker thread는 동일한 인스턴스를 공유하지 않으므로 결과적으로 양쪽에 복제본이 생성되는 것과 같다.
worker thread는 main thread의 window scope에 접근할 수 없다. 그럼에도 외부 라이브러리를 사용하려면 importScripts()를 사용해 외부 스크립트를 가져올 수 있다.
importScripts(); /* imports nothing */
importScripts('foo.js'); /* imports just "foo.js" */
importScripts('foo.js', 'bar.js'); /* imports two scripts */
importScripts('//example.com/hello.js'); /* You can import scripts from other origins */
importScripts로 가져온 스크립트는 해당 worker thread에만 영향을 주며 전역 값 또한 worker thread의 self(WorkerGlobalScope)에 속하게 된다.
worker thread 내에서 에러가 발생하면, main thread까지 전파되지 않는다. 필요하다면 onerror 메서드를 이용해서 ErrorEvent를 수신할 수 있다.
worker thread에서 런타임 오류가 발생하면 onerror 이벤트 핸들러가 호출된다. error 이벤트에는 3가지 영역이 있다.
Web Worker 종류 2가지
const myWorker = new Worker('worker.js');
)const myWorker = new SharedWorker('worker.js');
)squareNumber.onchange = () => {
myWorker.port.postMessage([squareNumber.value, squareNumber.value]);
console.log('Message posted to worker');
}
실행이 긴 작업을 마주했을 때,
web worker를 사용해 멀티스레드를 사용하는 방법과 이벤트 루프 태스크를 잘게 쪼개서 비동기적으로 적절히 실행시키는 방법 중 어떤 게 낫지?
관련해서 아래 글을 읽어보면 좋을 것 같다.
Don't block the event loop! 매끄러운 경험을 위한 JavaScript 비동기 처리
worker thread도 WorkerGlobalScope에서 main thred의 구분되는 evnet loop를 갖는가?
The JavaScript Event Loop and Web Workers
Since JavaScript is single-threaded, how are web workers in HTML5 doing multi-threading?