게시글

5만명이 선택한 평균 별점 4.9의 제로초 프로그래밍 강좌! 로드맵만 따라오면 됩니다! 클릭
강좌21 - HTML&DOM - 6년 전 등록

웹 워커

멀티 쓰레드 활용하기

안녕하세요. 이번 시간에는 웹 워커에 대해 알아보겠습니다. 자바스크립트는 싱글 쓰레드로 동작하지만 웹 워커를 사용하면 브라우저에서 멀티 쓰레드를 활용할 수 있습니다.

왜 멀티 쓰레드가 필요한지부터 생각해봅시다. 싱글 쓰레드 방식에는 한 가지 큰 단점이 있습니다. 연산량이 많은 작업을 하는 경우, 그 작업이 완료되어야 다른 작업을 수행할 수 있다는 것입니다. 예를 들어, 웹 게임에서 좌표를 계산하는데 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에 대해 알아보겠습니다!

조회수:
0
목록
투표로 게시글에 관해 피드백을 해주시면 게시글 수정 시 반영됩니다. 오류가 있다면 어떤 부분에 오류가 있는지도 알려주세요! 잘못된 정보가 퍼져나가지 않도록 도와주세요.
Copyright 2016- . 무단 전재 및 재배포 금지. 출처 표기 시 인용 가능.
5만명이 선택한 평균 별점 4.9의 제로초 프로그래밍 강좌! 로드맵만 따라오면 됩니다! 클릭

댓글

5개의 댓글이 있습니다.
5년 전
질문있습니다.


아래 댓글 '사람이 작성한 모든 코드'라는 게 Promise API, Observer API, process.nextTick등, 모든 걸 포함한 부분인가요?
5년 전
네 포함입니다. 위에 세 개는 동시에 돌아가지 않습니다.
5년 전
답변 감사합니다.

궁금한 점이 하나 더 있는데, 멀티 스레드 처리를 할때, main 스레드는 오직 UI, DOM 관련 행위들만 처리할 수 있는 것 같은데 맞는지 궁금합니다.

300,000번의 console.log('A')를 처리해야 한다고 할때,
웹 워커로 작업을 던지면, UI가 먹통이 되는 것은 방지되고, 작업 처리도 잘 되는데

**-----
postMassage로 던진 직후에 300,000번의 console.log('B')를 찍으면
제 예상에는 'B', 'A'가 교차되서 동시에 계속 출력이 되어야 할 것 같은데,

'B'가 300,000번 먼저 찍힌 후, 웹 워커의 300,000번의 'A'가 나중에 출력됩니다.


답변해주시면 감사하겠습니다.
5년 전
전체 코드를 어떻게 짜셨는지 보여주셔야 할 것 같습니다. onmessage의 콜백함수가 나중에 실행되는 상황 아닌가요?
5년 전
https://github.com/zxczoxc125/web-worker-test

클릭 직후에 작업을 요청했는데, onmessage 밑으로 반복문을 옮겨도 동일합니다.
시간 나시면 한번 봐주시면 감사하겠습니다.
6년 전
개발시 로컬파일을 크롬으로 엑세스 해야할 경우, 크롬바로기 파일의 대상 항목 뒤에 --disable-web-security 를 붙여주시면 해결할 수 있습니다 ~ ^^;
6년 전
그렇군요! 덕분에 항상 궁금했던 부분이 풀렸습니다. 정말 감사합니다. 해당 내용에 대해서 참고하셨던 웹페이지가 있으면 알려주실 수 있나요? 저는 검색해도 안나오더라구요 ㅠㅠ
6년 전
저도 엄청나게 많이 검색했는데도 헷갈리더라고요. 결국 운영체제 대충 공부한 후에 깨닫게 되었습니다. 비동기더라도 싱글 쓰레드에서는 싱글 쓰레드가 결국 모든 것을 처리해야 하기 때문에 아무런 시간 이득이 없다는 것을요. 다만 네트워크나 파일시스템 접근만 그 시간동안 다른 작업을 할 수 있어서 시간 이득이 있습니다.
6년 전
답변 감사합니다! 그럼 하나 더 궁금한게 있습니다. 비동기라 함은 단순히 작업의 우선순위를 뒤로 미루는 것인가요? 싱글쓰레드가 메인 로직 작업을 하는 동안 비동기 작업은 누가 언제 어디서 하고 있는 건가요? 태스크 큐에는 콜백함수만 넣어진다고 알고 있습니다. 그럼 비동기 함수 자체내에서 콜백을 태스크 큐에 넣기 전에 다른 로직을 수행중이라면 이 로직을 누가 실행하는지가 궁금합니다. 브라우저나 node 엔진 자체에서 다른 쓰레드가 활용되고 있는 거라면 사실상 비동기나 웹워커나 큰 시간차가 없지 않을까요 ?
6년 전
네 작업의 우선 순위를 뒤로 미루는 겁니다. 병렬로 실행되는 게 아니라요. 로직(사람이 직접 작성한 코드)은 모두 메인쓰레드에서 처리됩니다. 네트워크 요청이나 파일시스템 정도만 운영체제에서 멀티쓰레드로 처리해주고요. 따라서 그 이외의 메인 쓰레드 로직(위에서 sleep 함수같은)이 오래 걸리면 비동기라 하더라도 시간이 오래 소요됩니다. 웹워커는 아예 다른 쓰레드를 생성해 로직을 거기서 처리하는거라서 속도에 이득을 볼 수 있습니다.
6년 전
싱글코어인 경우 워커를 사용하더라도 이득을 못 보는 것도 마찬가지로 설명가능합니다. 멀티쓰레드를 돌려도 코어가 하나라서 코어에서 쓰레드 전환 작업을 해서 한 번에 하나의 쓰레드만 처리할 수 있기 때문입니다.
6년 전
포스팅 잘봤습니다. 질문이 있는데요. 비동기(Promise)와 워커를 비교했을때의 장점이 있는건가요?
6년 전
프로미스는 싱글 쓰레드 위에서 돌아갑니다. 비동기이긴 하지만 시간적인 이득이 전혀 없습니다.