CODE

[Angular]Web Worker 개요 및 사용법

[Angular]Web Worker 개요 및 사용법

Web Workers란?

Javascript에서 메인스레드와는 별도의 스레드를 생성할 수 있게 도와주는 기능

Web Workers 등장배경

Javascript는 Single Thread로 동작하며 코드를 순서대로 실행시키는데 이를 비동기로 작업을 할 수 있게 브라우저에서 이벤트 루프, 태스크 큐를 통해 관리할 수 있다. 하지만 이러한 비동기 작업이 많아지면 사용자의 입출력을 처리하는 메인 스레드의 실행 속도에 문제가 생길 가능성이 높아진다. 그래서 브라우저에는 자바스크립트로 작성된 어플리케이션의 멀티스레딩을 위해 Web Worker를 제공한다. Web Worker로 실행되는 스레드는 독립된 스레드(카피본)로 메인스레드의 작업을 방해하지 않으며 기기의 CPU, 메모리 리소스를 더 효율적으로 활용할 수 있게 해준다.

이해를 위한 예시

수백개의 이미지 파일을 가져오는 비동기 코드가 실행되는 동시에 사용자가 화면에서 텍스트를 입력하거나 마우스를 조작하여 이벤트를 발생시켰다고 가정하자. 이와 비슷한 시점에 브라우저의 태스크 큐에는 비동기 작업이 완료된 후에 실행할 콜백 함수가 쌓이게 될 것이다. 동시에 UI 상호작용에 의한 컴포넌트 상태를 업데이트하는 콜백 함수도 태스크 큐에 쌓이게 될 것이다. 이렇게 많은 작업을 한꺼번에 처리하게 되면 사용자는 앱이 느리게 반응한다고 느낄 것이다.

Web Worker 사용법

Web Worker 동작 방법

Web Worker 객체 생성
// main.js

if (window.Worker) {

  const worker = new Worker('worker.js')

}

메인스레드에서 Worker 객체를 생성한다. Web Worker는 별도의 JS 파일을 가지고 있기 때문에 Worker 객체를 생성할 때 참조할 JS 파일을 생성할 필요가 있다. 그리고 조금 더 통제된 오류 처리와 이전 버전과의 호환성을 위해 window.Worker 조건을 걸어주는 것이 좋다.

Message 시스템으로 데이터 송・수신
// worker.js

var data = 'send data';
postMessage(data); // 메시지 송신
// main.js

if( window.Worker ) {
        var worker = new Worker('worker.js');
        // 메시지 수신
        worker.onmessage = (event) => { 
            console.log(event.data); // result : send data
            worker.terminate(); // 워커 종료
        };
}

메인스레드와 Worker 간에 데이터를 전달하기 위해서는 Message System을 사용해야한다. 데이터를 전송하기 위해서는 postMessage(), 데이터를 수신하기 위해서는 onmessage를 사용해야한다. 이때 전달되는 데이터는 메인스레드의 데이터를 직접 건드리지 않는 복사본이다.

Worker 종료하기
// 방법1.
worker.terminate();

// 방법2.
self.close();

 

Web Worker 예제

예제 1.
// main.ts
const worker = new Worker('./worker.js') // Worker 객체를 생성할 때 worker.js 파일을 참조한다. 
worker.postMessage(['str', 'ing', 'sample'])

worker.onmessage = e => {
	// 주고받는 데이터는 모두 이벤트 객체의 data 속성을 통해 전달된다.
	console.log(e.data) // ['STR', 'ING', 'SAMPLE'] 
}
// worker.ts
addEventListener('message', async function (e: MessageEvent<string[]>) {
  const urls = e.data; 
  postMessage(urls.map(v => v.toUpperCase());
});
예제 2.
// main.js 

var worker = new Worker('worker.js');

worker.addEventListener('message', function(e) {
  console.log('Worker said: ', e.data);
}, false);

worker.postMessage('Hello World'); // Send data to our worker.
// worker.js

self.addEventListener('message', function(e) {
  self.postMessage(e.data);
}, false);
예제3.
<button onclick="sayHI()">Say HI</button>
<button onclick="unknownCmd()">Send unknown command</button>
<button onclick="stop()">Stop worker</button>
<output id="result"></output>

<script>
  function sayHI() {
    worker.postMessage({'cmd': 'start', 'msg': 'Hi'});
  }

  function stop() {
    // Calling worker.terminate() from this script would also stop the worker.
    worker.postMessage({'cmd': 'stop', 'msg': 'Bye'});
  }

  function unknownCmd() {
    worker.postMessage({'cmd': 'foobard', 'msg': '???'});
  }

  var worker = new Worker('worker.js');

  worker.addEventListener('message', function(e) {
    document.getElementById('result').textContent = e.data;
  }, false);
</script>
// worker.js

self.addEventListener('message', function(e) {
  var data = e.data;
  switch (data.cmd) {
    case 'start':
      self.postMessage('WORKER STARTED: ' + data.msg);
      break;
    case 'stop':
      self.postMessage('WORKER STOPPED: ' + data.msg + '. (buttons will no longer work)');
      self.close(); // Terminates the worker.
      break;
    default:
      self.postMessage('Unknown command: ' + data.msg);
  };
}, false);

 

Web Worker 종류

Web Worker에는 두가지 타입이 존재한다.

Dedicated Worker

특징
  • 처음 생성한 스크립트에서만 사용이 가능
  • 부모 자식 간의 스레드끼리만 교환 가능
  • Message System으로 데이터 전달 가능
생성방법
// main.js

const worker = new Worker('worker.js')
메시지 송신
// worker.js

var data = 'send data';
postMessage(data); // 메시지 송신
메시지 수신
// main.js

if( window.Worker ) {
        var worker = new Worker('worker.js');
        // 메시지 수신
        worker.onmessage = (event) => { 
            console.log(event.data); // result : send data
            worker.terminate(); // 워커 종료
        };
}

Shared Worker

특징
  • 동일한 도메인 내에 존재하는 여러 스레드에서 사용 가능
  • Port를 사용하여 데이터 전달 가능
생성방법
// main.js

var worker = new SharedWorker('shared_worker.js');
메시지 송신
// shared_worker.js

var count = 0;
onconnect = function (e) {
    var port = e.ports[0];
    port.onmessage = function (e) {
        port.postMessage(count++);
    }
}
메시지 수신
// main.js

if (window.SharedWorker) {
    var worker = new SharedWorker('shared_worker.js');
    worker.port.postMessage();
    worker.port.onmessage = function (e) {
        console.log(e.data);
    }
}

 

Web Worker 응용

워커의 스코프

Worker는 메인스레드가 가지고 있는 GlobalScope와는 별도의 WorkerGlobalScope를 가지고 있다. Dedicated Worker가 가지고 있는 GlobalScope를 DedicatedWorkerGlobalScope, Shared Worker가 가지고 있는 GlobalScope를 SharedWorkerGlobalScope라고 부른다.

스코프란?

변수나 함수의 유효 범위. 동일한 이름의 변수나 함수를 참조할 때 해당 변수의 유효 범위를 식별하여 유효한 값을 찾아낸다.

  • 전역 스코프 : 코드 어디에서든지 참조할 수 있다.
  • 지역 스코프 : 함수 코드 블록이 만든 스코프로 함수 자신과 하위 함수에서만 참조할 수 있다.

그렇기 때문에 메인스레드의 DOM이나 메서드를 조작할 수 없다. WorkerGlobalScope에 접근하기 위해서는 self 를 사용하여 접근이 가능하다.

self.onmessage = () => { }

여기서 this 역시 WorkerGlobalScope 참조하기 때문에 생략해도 상관이 없다.

window, this, self의 차이

window : DOM의 최상위 객체
this : 현재 컨텍스트(객체)를 참조한다.
self : window를 참조한다.

 

외부스크립트 임포트하기

메인스레드(window)의 Scope에는 직접 접근할 수 없기 때문에 외부라이브러리를 임포트할 경우에는 importScripts() 를 사용해야한다.

importScripts('script1.js'); 
importScripts('script2.js');
importScripts('script1.js, script2.js');

코드를 두개로 나누어서 작성하거나 하나로 합쳐서 작성할 수 있다.

서브 워커 사용하기

워커는 자식 워커를 생성할 수 있기 때문에 실행시에 큰 작업을 세세하게 분할할 수 있다. 단, 서브 워커를 사용할 때 다음 사항에 주의해야한다.

  • 서브 워커는 부모와 같은 곳에 생성되어야 한다.
  • 서브 워커 내의 URI는 부모 워커에서 상대 위치를 사용해야한다.
  • 사용자의 메모리 리소스를 많이 소비하지 않도록 주의해야한다.(워커는 메시지를 공유하는 것이 아닌 복사본을 사용)
  • 만약 MIMETYPE이 Javascript 가 아닐 경우 Error 가 발생한다.
예제
// worker.js
var sub_worker = new Worker('sub_worker.js');
sub_worker.onmessage = function (event) {
    console.log('worker', event.data);
}
sub_worker.postMessage('test');
// sub_worker.js
onmessage = function (event) {
    console.log('sub_worker', event.data);
    postMessage(event.data);
}

 

에러처리

Worker 자체에서 에러가 발생할 경우

Worker 내부에서 Error가 발생하면 메인스레드까지 전파되지 않는다. 필요하다면 onerror 메서드를 이용하여 ErrorEvent를 수신할 수 있다.

// err_worker.js
onmessage = (e) => {
    console.log(e.data);
}
throw new Error('error-thread');
// main.js

var err_worker = new Worker('err_worker.js');
err_worker.onerror = (err) => {
    // err: ErrorEvent { ... }
    console.err(err.message);
}
메시지 전달 중 에러가 발생할 경우
err_worker.onmessageerror = (err) => {
    // Uncaught DataCloneError: Failed to execute
    console.err(err.message);
}
err_worker.postMessage(10);

onmessageerror 를 사용하면 메시지 전달 중 에러가 발생할 경우 호출된다.

Web Worker 기능

사용할 수 있는 기능

  • navigator
  • location (읽기 전용)
  • XMLHttpRequest
  • setTimeout()/clearTimeout() 와 setInterval()/clearInterval()
  • 어플리케이션 캐시
  • importScripts() 를 사용한 외부스크립트 임포트
  • 다른 Web Worker 생성

사용할 수 없는 기능

  • DOM
  • window
  • document
  • parent

 

Web Worker 활용

Web Worker를 사용하면 좋은 예

  • 화면 업데이트를 최소화하여 성능을 향상시키고 싶은 경우
  • 로딩 작업이 다른 DOM의 업데이트와 UI 상호작용에 미치는 영향을 최소화하고 싶은 경우
  • 화면 동작에 영향을 미치는 연산 동작, 즉, 바이너리 파일 핸들링이나 복잡한 계산이 필요한 경우
  • 백그라운드에서 지속적인 작업을 해야 하거나 메인 스레드 영향을 미치지 않고 작업을 하는 경우
  • 멀티 스레드로 개발했을 때 사용자 환경 개선에 도움이 되는 경우
  • 싱글 페이지 애플리케이션으로 개발되는 경우
  • 원격지에 있는 리소스에 대한 액세스 작업을 하는 경우
  • 로컬 스토리지에 액세스 하는 경우

앵귤러에서 웹워커 사용하는 방법

Web Worker 파일 추가하기

ng generate web-worker <location>

앵귤러에서 웹 워커 파일 추가하기 위해서는 Angular CLI을 사용하여 추가합니다. 위 명령어를 실행하면 새로운 웹 워커가 추가된다. 웹 워커를 추가하면 location.worker.ts 라는 이름으로 파일이 생성됩니다. 코드를 작성하는 방법은 위에서 설명한 방법으로 동일하게 작업이 가능하다.

Web Worker 예제

웹 워커 생성

ng generate web-worker app

src/app 폴더 안에 새로운 워커를 생성합니다. 위 명령어를 실행하면 app.worker.ts 라는 이름으로 웹 워커 파일이 생성된다.

const worker = new Worker(new URL('./app.worker', import.meta.url));

Worker 객체를 new 하여 새로운 워커를 생성한다.

message 송수신

// message 수신
worker.onmessage = ({ data }) => {
  console.log(`page got message: ${data}`);
};

// message 송신
worker.postMessage('hello');

postMessage()와 onmessage()를 사용하여 데이터를 송신/수신할 수 있다.

예제 코드

// app.component.ts

if (typeof Worker !== 'undefined') {
  // 웹 워커 생성
  const worker = new Worker(new URL('./app.worker', import.meta.url));
  worker.onmessage = ({ data }) => {
    console.log(`page got message: ${data}`);
  };
  worker.postMessage('hello');
} else {
   // 이 환경에서는 웹 작업자가 지원되지 않습니다.
   // 프로그램이 계속 올바르게 실행되도록 폴백을 추가해야 합니다.
}
// app.worker.ts

addEventListener('message', ({ data }) => {
  const response = `worker response to ${data}`;
  postMessage(response);
});
최신글