안녕하세요. 이번 시간에는 웹 워커에 대해 알아보겠습니다. 자바스크립트는 싱글 쓰레드로 동작하지만 웹 워커를 사용하면 브라우저에서 멀티 쓰레드를 활용할 수 있습니다.
왜 멀티 쓰레드가 필요한지부터 생각해봅시다. 싱글 쓰레드 방식에는 한 가지 큰 단점이 있습니다. 연산량이 많은 작업을 하는 경우, 그 작업이 완료되어야 다른 작업을 수행할 수 있다는 것입니다. 예를 들어, 웹 게임에서 좌표를 계산하는데 3초가 걸리고 계산된 좌표를 받아 DOM에 반영한다고 생각해봅시다. 좌표를 계산하느라 3초간 DOM 업데이트 등의 다른 작업들을 수행할 수가 없습니다. 사용자 입장에서는 3초간 멍하니 기다릴 뿐입니다. 좌표를 동시에 여러 번 계산해야 하는 경우 더 심각해집니다. 좌표를 20번 계산하면 3 * 20 = 60초를 기다려야 하는 셈입니다.
3초의 시간은 어쩔 수 없다 치더라도, 계산하는 동안 UI 클릭같은 다른 작업은 진행할 수 있어야 원활한 서비스를 제공할 수 있습니다. 이럴 때 워커가 사용됩니다. 새로운 쓰레드의 워커를 생성해 워커에게 계산을 맡기고, 메인 쓰레드는 다른 작업을 수행합니다. 3초 후 워커가 계산된 좌표를 메인 쓰레드에게 보내주면 메인 쓰레드는 DOM 업데이트를 진행하면 됩니다. 좌표를 동시에 여러 번 계산할 경우 더 유용해집니다. 좌표를 계산해야 할 때마다 워커를 생성한 후 각각의 워커에게 계산 하나씩을 맡기면 됩니다. 단, 싱글 코어 컴퓨터의 경우 거의 의미가 없습니다. CPU 코어가 많은 컴퓨터일수록 병렬로 처리할 수 있는 것이 많아지므로 유리합니다. 머리 수 많은 CPU를 삽시다! 워커를 여러 개 써서 멀티 쓰레드처럼 사용할 수는 있지만 각각의 워커는 여전히 싱글 쓰레드라는 것을 기억합시다.
워커는 DOM에 직접 접근하지 못하기 때문에 메인 쓰레드와 서로 메시지를 주고 받아서 통신합니다. iFrame과 통신하는 것과 API가 매우 유사합니다. 워커를 활용하는 예제를 보여드리겠습니다. 먼저 문제의 상황입니다.
<div id="result"></div>
<button id="btn">run</button>
<script>
function sleep(delay) {
var start = new Date().getTime();
while (new Date().getTime() < start + delay);
}
document.querySelector('#btn').addEventListener('click', function () {
sleep(3000); // 3초가 걸림을 표현
var div = document.createElement('div');
div.textContent = Math.random();
document.querySelector('#result').appendChild(div);
});
</script>
자바스크립트에는 동기적으로 sleep하는 함수가 없기 때문에 직접 만들었습니다. (setTimeout은 비동기적이라서 사용하면 다른 결과를 낳습니다.) run 버튼을 눌러보면 모든 게 멈춰버립니다. 계산하는 3초동안 다른 작업을 할 수 없기 때문이죠. 이렇게 되면 사용자들은 3초동안 아무 것도 할 수 없게 되므로, 실제 서비스에서 이런 상황이 발생하지 않게 해야 합니다.
위의 상황을 웹 워커를 사용해서 개선해봅시다.
<div id="result"></div>
<button id="btn">run</button>
<script>
document.querySelector('#btn').addEventListener('click', function () {
var worker = new Worker('./worker.js');
worker.addEventListener('message', function(e) {
var div = document.createElement('div');
div.textContent = e.data; // 0.1238917491
document.querySelector('#result').appendChild(div);
worker.terminate();
});
worker.postMessage('일해라 워커!');
});
</script>
버튼을 누를 때, 직접 계산하는 것이 아니라 워커를 생성하고 워커에게 postMessage로 일하라고 알립니다. 워커에 message 이벤트 리스너를 붙이는 것은 워커로부터 결괏값을 받기 위함입니다. e.data로 받아올 수 있습니다. 결괏값을 DOM에 반영한 후에는 terminate 메소드로 워커를 종료합니다. 워커(worker.js)는 다음과 같습니다.
function sleep(delay) {
var start = new Date().getTime();
while (new Date().getTime() < start + delay);
}
self.addEventListener('message', function(e) {
console.log(e.data); // 일해라 워커!
sleep(3000); // 3초가 걸림을 표현
var coords = Math.random();
console.log(coords);
self.postMessage(coords);
});
계산하는 로직이 워커로 이동했습니다. 워커도 postMessage와 message 이벤트 리스너를 사용합니다. 마스터에게 메시지를 보내고 받는 것이죠. 워커는 새로운 스코프를 형성하기 때문에 self라는 키워드로 자기자신과 연결합니다(this도 가능하지만 this는 상황에 따라 값이 달라질 수 있다는 것 아시죠?). postMessage에 넣은 인자가 e.data로 연결됩니다.
크롬에서는 로컬 파일이 워커를 생성하는 것을 제한하기 때문에 실습하려면 파이어폭스에서 하시면 됩니다. 버튼이 이제 여러 번 잘 눌리고, 각각 3초 후에 DOM이 업데이트 되는 것을 확인할 수 있습니다. 복잡한 연산은 다른 쓰레드가 계산해주고 결과만 나중에 돌려주기 때문에 매우 유용합니다.
워커가 다른 워커를 불러올 수도 있습니다. worker2.js가 다음 내용이라고 해봅시다.
self.a = 'hello worker!';
worker.js에서 worker2의 self를 불러올 수 있습니다.
self.addEventListener('message', function(e) {
var a = 'ignored';
importScripts('./worker2.js');
console.log(a); // hello worker!
});
importScripts 함수로 불러오면 됩니다. self에 변수를 추가하는 것에 유의하세요. 이래야만 다른 워커에서 사용할 수 있습니다.
워커에는 DOM에 접근하지 못하는 것 외에도 사용할 수 있는 API에 제약이 있기 때문에 여기서 사용 가능한 목록을 확인하는 것이 좋습니다.
대부분의 웹에서는 워커를 쓸 정도로 복잡한 작업을 진행하지 않기 때문에 웹 워커를 사용할 일이 드뭅니다. 하지만 빅데이터 처리나 웹 게임 등의 경우에는 유용합니다. 계산은 워커에게 맡기고 메인 쓰레드는 DOM 업데이트만 담당하면 됩니다. 이러한 방식은 Node.js에서도 cluster나 child_process같은 모듈로 사용하곤 합니다.
다음 시간에는 또 다른 워커인 서비스워커를 사용하는 PWA에 대해 알아보겠습니다!
아래 댓글 '사람이 작성한 모든 코드'라는 게 Promise API, Observer API, process.nextTick등, 모든 걸 포함한 부분인가요?
궁금한 점이 하나 더 있는데, 멀티 스레드 처리를 할때, main 스레드는 오직 UI, DOM 관련 행위들만 처리할 수 있는 것 같은데 맞는지 궁금합니다.
300,000번의 console.log('A')를 처리해야 한다고 할때,
웹 워커로 작업을 던지면, UI가 먹통이 되는 것은 방지되고, 작업 처리도 잘 되는데
**-----
postMassage로 던진 직후에 300,000번의 console.log('B')를 찍으면
제 예상에는 'B', 'A'가 교차되서 동시에 계속 출력이 되어야 할 것 같은데,
'B'가 300,000번 먼저 찍힌 후, 웹 워커의 300,000번의 'A'가 나중에 출력됩니다.
답변해주시면 감사하겠습니다.
클릭 직후에 작업을 요청했는데, onmessage 밑으로 반복문을 옮겨도 동일합니다.
시간 나시면 한번 봐주시면 감사하겠습니다.