[Javascript] 쓰레드(웹 워커-Web worker)를 사용하는 방법


Study/Javascript, Jquery, CSS  2020. 3. 31. 19:48

안녕하세요. 명월입니다.


이 글은 Javascript에서 쓰레드(웹 워커-Web worker)를 사용하는 방법에 대한 글입니다.


프로그램이 실행하고 종료되기까지 작업을 우리는 프로세스라고 합니다. 그 프로세스 안에는 프로그램 코드가 있고, 코드의 순서에 따라 위에서 아래로 실행됩니다.

이 때 Javascript의 경우는 요청해서 응답받은 html에 Javascript의 링크와 코드가 태그의 순서대로 실행됩니다. 그것을 single thread 또는 main thread라고 합니다.

간혹 그 코드 순서대로 실행을 하는 것이 아니라 동시에 실행되는 병렬 처리를 원할 때가 있습니다. 하나의 처리는 for문의 루프로 특정 텍스트를 작성하고 또 다른 하나는 동시에 다른 요소의 값을 계산하는 식의 병렬 처리입니다.

우리는 2개 이상의 병렬 처리하는 방식을 멀티 쓰레드라고 합니다.

<!doctype html>
<html>
 <head>
  <title>Document</title>
 </head>
 <body>
  <script>
    // main 쓰레드에서 i가 0부터 1000까지 콘솔에 값을 출력한다.
    for(let i=0;i<1000;i++){
      // 콘솔 출력
      console.log(i);
    }  
  </script>
 </body>
</html>

위 예제가 단순히 자바스크립트에서 for문들 통해 0부터 999까지 실행했습니다. 저 부분이 메인 쓰레드입니다.

메인 쓰레드는 html이 전부 로드 되기 전까지 실행되며 중간에 script 태그를 만나거나 script 링크(script src)를 연결하는 것으로 실행됩니다.

여기서 제가 이전에 setTimeout과 async/await를 설명한 적이 있습니다.

링크 - [Javascript] 타이머 함수(setTimeout, setInterval)

링크 - [Javascript] async, await의 사용법

<!doctype html>
<html>
 <head>
  <title>Document</title>
 </head>
 <body>
  <script>
    // 0부터 99까지 반복
    for(let i=0;i<100;i++){
      // event queue에 0부터 99를 넣는다.
      (async ()=>{
        // 콘솔 출력
        console.log(i);
      })();
    }
  </script>
 </body>
</html>

여기서 async를 사용하면 이벤트 큐를 이용하면 main thread 실행이 끝나고 이벤트 속성으로 0부터 99까지 콘솔에 출력이 됩니다.

이렇게 보면 setTimeout은 main thread와 떨어져서 실행하기 때문에 멀티 쓰레드 같이 보입니다만 setTimeout은 멀티 쓰레드 환경이 아닙니다.

위 이미지처럼 main thread가 끝나고 event queue가 실행되면서 console.log(0), console.log(1)... 식으로 실행되는 것입니다.

중간에 액션 이벤트, 마우스 클릭이나 포커스 이동이 있으면 console.log가 찍히면서 동시에 액션이 실행되기 때문에 마치 멀티 쓰레드 같이 움직이는 것처럼 보입니다. 그러나 멀티 쓰레드가 아닙니다. 메시지 큐가 사람이 인지하는 속도보다 빨리 처리되어서 그렇게 보이는 것뿐입니다.


제가 위 예제에서 console.log(i)를 하나의 이벤트(setTimeout)에 넣었지만 한개의 이벤트에 for문 전체를 넣으면 어떻게 될까요?

<!doctype html>
<html>
 <head>
  <title>Document</title>
 </head>
 <body>
  <script>
    // event queue에 넣어 main thread가 종료되면 호출된다.
    (async ()=>{
      // 0부터 9999까지 반복
      for(let i=0;i<10000;i++){
        // 콘솔 출력
        console.log(i);
      }
    })();
  </script>
 </body>
</html>

이 경우는 main thread에서 실행되는 건 아니지만 하나의 event에서 for문 처리가 끝날 때까지 기다리기 때문에 브라우저가 멈추는 듯한 프리징 현상이 발생할 것입니다. 즉, main thread에서 많은 처리하는 것과 같은 현상이 일어납니다.

event queue에서 나와서 처리하는 것도 결국에는 main thread에서 실행하는 것입니다. 즉, 멀티 쓰레드가 아니기 때문입니다.


그래서 Javascript에서 멀티 쓰레드를 사용하기 위해서는 웹 워커(Web worker)를 사용합니다.

웹 워커의 경우는 별도의 script 파일을 작성해서 로드하는 방법으로 쓰레드 환경을 만듭니다.

<!doctype html>
<html>
 <head>
  <title>Document</title>
 </head>
 <body>
  <script>
    // Web worker가 실행 가능한지 체크
    // 구 IE버전이면 Web woker가 지원하지 않습니다.
    // 그러나 Web worker가 지원하지 않는 브라우저는 거의 유물. 
    if (window.Worker) {
      // Worker 쓰레드를 생성(js파일를 로드)
      let worker = new Worker("worker.js");
      // 에러가 발생할 경우 발생!
      worker.onerror = (e)=>{
        console.log("error " + e.message);
	  }
      // worker.js에서 postMessage의 값을 받는다. 
      worker.onmessage = (e)=>{
        // 콘솔 출력
        console.log(e.data);
	  }
    }
  </script>
 </body>
</html>
// 0에서 99까지 루프
for(let i=0;i<100;i++){
  // 위 worker에서 onmessage이벤트를 발생!
  postMessage(i);
}
// test변수는 없기 때문에 에러가 발생한다.
test.val = test+1;

참고로 main thread와 worker thread는 쓰레드가 서로 다른 영역이기 때문에 변수가 공유가 되지 않습니다. 공유하는 방법은 onmessage의 이벤트를 서로 등록하고 postMessage를 호출해서 서로 간의 통신으로 데이터를 주고 받습니다.

<!doctype html>
<html>
 <head>
  <title>Document</title>
 </head>
 <body>
  <script>
    // Worker 쓰레드를 생성(js파일를 로드)
    var worker = new Worker("worker.js");
    // worker.js에서 postMessage의 값을 받는다.  
    worker.onmessage = (e)=>{
      // 콘솔 출력
      console.log(e.data);
    }
    // worker.js로 postMessage의 값을 보낸다.
    worker.postMessage("hello world");
  </script>
 </body>
</html>
// main에서 postMessage의 값을 받는다. 
onmessage = (e)=>{
  // 받은 값을 echo를 붙여서 다시 보낸다.
  postMessage("echo "+e.data);
}

main thread에서 worker.js로 hello world를 보내고 worker.js에서 echo를 붙여서 마지막으로 콘솔에는 echo hello world를 출력했습니다.


그리고 이 main thread와 worker thread는 서로 다른 영역의 쓰레드입니다. 그래서 html에서 선언한 js파일은 worker.js에서는 참조되지 않습니다.

<!doctype html>
<html>
 <head>
  <title>Document</title>
 </head>
 <body>
  <!-- test.js파일 참조 -->
  <script src="test.js"></script>
  <script>
    // Worker 쓰레드를 실행(js파일로 등록한다.)
    var worker = new Worker("worker.js");
    // test.js파일의 test함수를 호출
    test("main thread");
    // worker에서 postMessage가 호출되면 실행
    worker.onmessage = (e)=>{
      // 쓰레드를 종료한다.
      worker.terminate();
    }
  </script>
 </body>
</html>
// main page의 script src는 공유가 되지 않기 때문에 따로 참조해야 한다.
importScripts("test.js");
// test.js파일의 test함수를 호출 
test("worker thread");
// postMessage를 보내면 쓰레드를 종료한다.
postMessage(null);
// 함수 생성
function test(val){
  // 콘솔 출력
  console.log(val);
}

worker.js의 페이지가 전부 로드되면 자동으로 쓰레드는 사라지게 됩니다. 그러나 무한 루프나 특정 메시지에 반응하는 종료되지 않는 쓰레드를 만들 경우는 중단하는 시점을 만들어야 합니다. 사양에 따라 terminate를 사용해서 쓰레드를 종료할 수 있습니다.


그리고 Web worker의 종류는 두가지가 있습니다.

첫째는 Dedicated Worker 타입으로 하나의 thread에서 여러 sub thread를 파생하는 방법입니다. 지금까지 설명한 방법입니다.

두번째는 역으로 여러 js파일이나 여러 thread, 실행 스택 영역에서 하나의 쓰레드를 만들어 서로간에 공유하는 방법(Shared Worker)입니다.

<!doctype html>
<html>
 <head>
  <title>Document</title>
 </head>
 <body>
  <script>
    // 1번째 스택
    {
      // SharedWorker가 실행 가능한지 체크
      if (window.SharedWorker) {
        // SharedWorker 쓰레드를 실행(js파일로 등록한다.)
        let worker = new SharedWorker("worker.js");
        // worker.js에서 postMessage의 값을 받는다. 
        worker.port.onmessage = function (e) {
          // 콘솔 출력
          console.log(e.data);
        }
        // worker.js로 postMessage를 보낸다.
        worker.port.postMessage(null);
      }
    }
    // 2번째 스택
    {
      // SharedWorker가 실행 가능한지 체크
      if (window.SharedWorker) {ㄴ
        // SharedWorker 쓰레드를 실행(js파일로 등록한다.)
        let worker = new SharedWorker("worker.js");
        // worker.js에서 postMessage의 값을 받는다. 
        worker.port.onmessage = function (e) {
          // 콘솔 출력
          console.log(e.data);
        }
        // worker.js로 postMessage를 보낸다.
        worker.port.postMessage(null);
      }
    }
  </script>
 </body>
</html>
// 테스트 변수
var count = 0;
onconnect = function (e) {
  // SharedWorker 쓰레드로 받는다. 어떤 스택, 어떤 thread를 실행하더라도 한 개의 쓰래드로 공유된다.
  let port = e.ports[0];
  // postMessage의 값을 받는다. 
  port.onmessage = function (e) {
    // count를 증가 시켜 postMessage를 보낸다.
    port.postMessage(++count);
  }
}

위 shared thread의 경우는 멀티 쓰레드를 생성해서 실행하는 효과보다 역으로 여러 쓰레드에서 하나의 쓰레드로 데이터 등을 공유할 때 자주 사용됩니다.


여기까지 Javascript에서 쓰레드(웹 워커-Web worker)를 사용하는 방법에 대한 글이었습니다.


궁금한 점이나 잘못된 점이 있으면 댓글 부탁드립니다.