웹 작업자 기본사항

문제: 자바스크립트 동시 실행

관심 있는 애플리케이션이 서버 집약적인 구현에서 클라이언트 측 JavaScript로 포팅되지 못하게 하는 병목 현상은 여러 가지가 있습니다. 이러한 기능에는 브라우저 호환성, 정적 입력, 접근성, 성능이 포함됩니다. 다행히 브라우저 공급업체가 JavaScript 엔진의 속도를 빠르게 개선함에 따라 후자는 빠르게 과거가 되었습니다.

여전히 JavaScript의 한 가지 걸림돌로 남아 있는 것은 언어 자체입니다. JavaScript는 단일 스레드 환경이므로 여러 스크립트를 동시에 실행할 수 없습니다. 예를 들어 UI 이벤트를 처리하고 대량의 API 데이터를 쿼리 및 처리하고 DOM을 조작해야 하는 사이트를 가정해 보겠습니다. 꽤 흔하지요? 안타깝게도 브라우저의 JavaScript 런타임의 제한으로 인해 이 모든 작업을 동시에 수행할 수는 없습니다. 스크립트는 단일 스레드 내에서 실행됩니다.

개발자는 setTimeout(), setInterval(), XMLHttpRequest, 이벤트 핸들러와 같은 기법을 사용하여 '동시 실행'을 모방합니다. 예, 이러한 기능은 모두 비동기식으로 실행되지만 비블로킹이라고 해서 반드시 동시 실행을 의미하지는 않습니다. 비동기 이벤트는 현재 실행 중인 스크립트가 생성된 후에 처리됩니다. 좋은 소식은 HTML5가 이러한 해킹보다 더 나은 무언가를 제공한다는 것입니다.

Web Workers 소개: JavaScript에 스레딩 도입

웹 작업자 사양은 웹 애플리케이션에 백그라운드 스크립트를 생성하기 위한 API를 정의합니다. 웹 작업자를 사용하면 장기 실행 스크립트를 실행하여 계산 집약적인 작업을 처리하되, 사용자 상호작용을 처리하기 위해 UI나 다른 스크립트를 차단하지 않고도 실행할 수 있습니다. 우리 모두가 좋아하는 '반응이 없는 스크립트' 대화를 마무리하고 완성하는 데 도움을 줄 것입니다.

응답하지 않는 스크립트 대화상자
응답이 없는 일반적인 스크립트 대화상자

작업자는 스레드와 유사한 메시지 전달을 활용하여 동시 로드를 달성합니다. UI 새로고침, 성능 및 사용자 반응성을 유지하는 데 적합합니다.

웹 작업자 유형

사양에서는 두 가지 유형의 웹 작업자인 전용 작업자공유 작업자를 설명합니다. 이 문서에서는 전용 작업자만 다룹니다. 여기서는 이들을 '웹 작업자' 또는 '작업자'로 지칭하겠습니다.

시작하기

웹 작업자는 격리된 스레드에서 실행됩니다. 따라서 실행되는 코드를 별도의 파일에 포함해야 합니다. 하지만 그 전에 먼저 기본 페이지에서 새 Worker 객체를 만들어야 합니다. 생성자는 작업자 스크립트의 이름을 사용합니다.

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

지정된 파일이 있으면 브라우저는 새 작업자 스레드를 생성하며, 이 스레드는 비동기식으로 다운로드됩니다. 파일이 완전히 다운로드되고 실행될 때까지는 작업자가 시작되지 않습니다. 작업자의 경로가 404를 반환하면 작업자가 자동으로 실패합니다.

작업자를 만든 후 postMessage() 메서드를 호출하여 작업자를 시작합니다.

worker.postMessage(); // Start the worker.

메시지 전달을 통해 작업자와 통신

작업과 상위 페이지 간의 통신은 이벤트 모델과 postMessage() 메서드를 사용하여 이루어집니다. 브라우저나 버전에 따라 postMessage()는 문자열 또는 JSON 객체를 단일 인수로 허용할 수 있습니다. 최신 버전의 브라우저는 JSON 객체 전달을 지원합니다.

다음은 문자열을 사용하여 doWork.js의 작업자에게 'Hello World'를 전달하는 예입니다. 작업자는 전달된 메시지만 반환합니다.

메인 스크립트:

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

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

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

doWork.js (작업자):

self.addEventListener('message', function(e) {
self.postMessage(e.data);
}, false);

기본 페이지에서 postMessage()가 호출되면 작업자가 message 이벤트의 onmessage 핸들러를 정의하여 메시지를 처리합니다. 메시지 페이로드 (이 경우 'Hello World')는 Event.data에서 액세스할 수 있습니다. 이 특정 예는 그다지 흥미롭지는 않지만 postMessage()가 기본 스레드에 데이터를 다시 전달하는 방법이기도 함을 보여줍니다. 편리함!

기본 페이지와 작업자 간에 전달되는 메시지는 공유되지 않고 복사됩니다. 예를 들어 다음 예에서 JSON 메시지의 'msg' 속성은 두 위치 모두에서 액세스할 수 있습니다. 객체가 별도의 전용 공간에서 실행 중이지만 작업자에 직접 전달되는 것으로 보입니다. 실제로는 객체가 작업자에 전달될 때 직렬화되고, 다른 쪽 끝에서는 역직렬화됩니다. 페이지와 작업자는 동일한 인스턴스를 공유하지 않으므로 각 패스에서 중복이 생성됩니다. 대부분의 브라우저는 양쪽에서 값을 자동으로 JSON 인코딩/디코딩하여 이 기능을 구현합니다.

다음은 JSON 객체를 사용하여 메시지를 전달하는 좀 더 복잡한 예시입니다.

메인 스크립트:

<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() {
// 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('doWork2.js');

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

doWork2.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);

전송 가능한 객체

대부분의 브라우저는 구조화된 클론 알고리즘을 구현합니다. 이 알고리즘을 사용하면 File, Blob, ArrayBuffer, JSON 객체와 같은 더 복잡한 유형을 worker 안팎으로 전달할 수 있습니다. 그러나 postMessage()를 사용하여 이러한 유형의 데이터를 전달하면 여전히 사본이 생성됩니다. 따라서 대용량 파일 (예: 50MB)을 전달하는 경우 작업자와 기본 스레드 간에 파일을 가져오는 데 상당한 오버헤드가 발생합니다.

구조화된 클론은 효과적이지만 사본에 수백 밀리초가 걸릴 수 있습니다. 성능 히트에 대처하려면 전송 가능한 객체를 사용하면 됩니다.

전송 가능한 객체를 사용하면 데이터가 한 컨텍스트에서 다른 컨텍스트로 전송됩니다. 제로 카피이므로 작업자에 데이터를 전송하는 성능이 크게 향상됩니다. C/C++ 환경의 경우 이를 패스 바이 참조로 생각하면 됩니다. 그러나 패스 바이 참조와 달리 호출 컨텍스트의 '버전'은 새 컨텍스트로 전송되면 더 이상 사용할 수 없습니다. 예를 들어 기본 앱에서 Worker로 ArrayBuffer를 전송할 때 원래 ArrayBuffer가 삭제되며 더 이상 사용할 수 없습니다. 객체의 내용은 (문자 그대로 말 그대로) Worker 컨텍스트로 전송됩니다.

전송 가능한 객체를 사용하려면 약간 다른 postMessage() 서명을 사용합니다.

worker.postMessage(arrayBuffer, [arrayBuffer]);
window.postMessage(arrayBuffer, targetOrigin, [arrayBuffer]);

작업자 사례에서 첫 번째 인수는 데이터이고 두 번째 인수는 전송해야 하는 항목 목록입니다. 하지만 첫 번째 인수가 ArrayBuffer일 필요는 없습니다. 예를 들어 JSON 객체일 수 있습니다.

worker.postMessage({data: int8View, moreData: anotherBuffer},
                [int8View.buffer, anotherBuffer]);

중요한 점은 두 번째 인수는 ArrayBuffer의 배열이어야 한다는 것입니다. 이전 가능한 항목 목록입니다.

이전 가능한 항목에 관한 자세한 내용은 developer.chrome.com의 게시물을 참고하세요.

작업자 환경

작업자 범위

작업자의 컨텍스트에서 selfthis는 모두 작업자의 전역 범위를 참조합니다. 따라서 이전 예를 다음과 같이 작성할 수도 있습니다.

addEventListener('message', function(e) {
var data = e.data;
switch (data.cmd) {
case 'start':
    postMessage('WORKER STARTED: ' + data.msg);
    break;
case 'stop':
...
}, false);

또는 onmessage 이벤트 핸들러를 직접 설정할 수 있습니다. 하지만 JavaScript 닌자가 항상 addEventListener를 권장합니다.

onmessage = function(e) {
var data = e.data;
...
};

작업자가 사용할 수 있는 기능

멀티스레드 동작으로 인해 Web Workers는 JavaScript의 일부 기능에만 액세스할 수 있습니다.

  • navigator 객체
  • location 객체 (읽기 전용)
  • XMLHttpRequest
  • setTimeout()/clearTimeout()setInterval()/clearInterval()
  • 애플리케이션 캐시
  • importScripts() 메서드를 사용하여 외부 스크립트 가져오기
  • 다른 웹 작업자 생성

작업자는 다음 항목에 액세스할 수 없습니다.

  • DOM (스레드로부터 안전하지 않음)
  • window 객체
  • document 객체
  • parent 객체

외부 스크립트 로드

importScripts() 함수를 사용하여 외부 스크립트 파일이나 라이브러리를 작업자에 로드할 수 있습니다. 이 메서드는 가져올 리소스의 파일 이름을 나타내는 문자열을 0개 이상 사용합니다.

이 예에서는 작업자에 script1.jsscript2.js를 로드합니다.

worker.js:

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

단일 import 문으로 작성할 수도 있습니다.

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

하위 작업자

worker는 하위 worker를 생성할 수 있습니다. 이는 런타임에 대규모 작업을 추가로 분할하는 데 유용합니다 하지만 하위 작업자에게는 몇 가지 주의사항이 있습니다.

  • 하위 작업자는 상위 페이지와 동일한 출처 내에서 호스팅되어야 합니다.
  • 하위 작업자 내의 URI는 기본 페이지가 아닌 상위 작업자의 위치를 기준으로 확인됩니다.

대부분의 브라우저는 각 작업자에 대해 별도의 프로세스를 생성합니다. 작업자 팜을 생성하기 전에 사용자의 시스템 리소스를 너무 많이 독차지하지 않도록 주의하세요. 기본 페이지와 작업자 간에 전달되는 메시지는 공유되지 않고 복사되기 때문입니다. 메시지 전달을 통한 작업자와의 커뮤니케이션을 참조하세요.

하위 작업자를 생성하는 방법의 샘플은 사양의 를 참조하세요.

인라인 작업자

작업자 스크립트를 즉시 만들거나 별도의 작업자 파일을 만들지 않고 독립적인 페이지를 만들려면 어떻게 해야 할까요? Blob()를 사용하면 작업자 코드의 URL 핸들을 문자열로 만들어 기본 로직과 동일한 HTML 파일에서 작업자를 '인라인'으로 만들 수 있습니다.

var blob = new Blob([
"onmessage = function(e) { postMessage('msg from worker'); }"]);

// Obtain a blob URL reference to our worker 'file'.
var blobURL = window.URL.createObjectURL(blob);

var worker = new Worker(blobURL);
worker.onmessage = function(e) {
// e.data == 'msg from worker'
};
worker.postMessage(); // Start the worker.

blob URL

이 기능은 window.URL.createObjectURL() 호출에서 제공됩니다. 이 메서드는 DOM File 또는 Blob 객체에 저장된 데이터를 참조하는 데 사용할 수 있는 간단한 URL 문자열을 만듭니다. 예를 들면 다음과 같습니다.

blob:http://localhost/c745ef73-ece9-46da-8f66-ebes574789b1

blob URL은 고유하며 애플리케이션의 전체 기간 동안 (예: document가 언로드될 때까지) 지속됩니다. Blob URL을 여러 개 만드는 경우, 더 이상 필요하지 않은 참조를 해제하는 것이 좋습니다. Blob URL을 window.URL.revokeObjectURL()에 전달하여 명시적으로 해제할 수 있습니다.

window.URL.revokeObjectURL(blobURL);

Chrome에는 생성된 모든 blob URL을 볼 수 있는 멋진 페이지(chrome://blob-internals/)가 있습니다.

전체 예

한 단계 더 나아가 작업자의 JS 코드가 페이지에서 인라인되는 방식을 현명하게 만들 수 있습니다. 이 기법에서는 <script> 태그를 사용하여 작업자를 정의합니다.

<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
</head>
<body>

<div id="log"></div>

<script id="worker1" type="javascript/worker">
// This script won't be parsed by JS engines
// because its type is javascript/worker.
self.onmessage = function(e) {
    self.postMessage('msg from worker');
};
// Rest of your worker code goes here.
</script>

<script>
function log(msg) {
    // Use a fragment: browser will only render/reflow once.
    var fragment = document.createDocumentFragment();
    fragment.appendChild(document.createTextNode(msg));
    fragment.appendChild(document.createElement('br'));

    document.querySelector("#log").appendChild(fragment);
}

var blob = new Blob([document.querySelector('#worker1').textContent]);

var worker = new Worker(window.URL.createObjectURL(blob));
worker.onmessage = function(e) {
    log("Received: " + e.data);
}
worker.postMessage(); // Start the worker.
</script>
</body>
</html>

제 생각에는 이 새로운 접근 방식이 조금 더 깔끔하고 읽기 쉽습니다. id="worker1"type='javascript/worker'로 스크립트 태그를 정의합니다 (따라서 브라우저가 JS를 파싱하지 않음). 이 코드는 document.querySelector('#worker1').textContent를 사용하여 문자열로 추출되고 Blob()에 전달되어 파일을 만듭니다.

외부 스크립트 로드

이러한 기법을 사용하여 작업자 코드를 인라인으로 작성할 때 importScripts()는 절대 URI를 제공하는 경우에만 작동합니다. 상대 URI를 전달하려고 하면 브라우저에 보안 오류 메시지가 표시됩니다. 그 이유는 작업자 (이제 blob URL에서 생성됨)는 blob: 접두사로 확인되지만 앱은 다른 스키마 (예: http://)에서 실행되기 때문입니다. 따라서 교차 출처 제한으로 인해 실패합니다.

인라인 작업자에서 importScripts()를 활용하는 한 가지 방법은 실행 중인 기본 스크립트의 현재 URL을 인라인 작업자에 전달하고 절대 URL을 수동으로 구성하여 '삽입'하는 것입니다. 이렇게 하면 외부 스크립트를 동일한 출처에서 가져옵니다. 기본 앱이 http://example.com/index.html에서 실행된다고 가정합니다.

...
<script id="worker2" type="javascript/worker">
self.onmessage = function(e) {
var data = e.data;

if (data.url) {
var url = data.url.href;
var index = url.indexOf('index.html');
if (index != -1) {
    url = url.substring(0, index);
}
importScripts(url + 'engine.js');
}
...
};
</script>
<script>
var worker = new Worker(window.URL.createObjectURL(bb.getBlob()));
worker.postMessage(<b>{url: document.location}</b>);
</script>

오류 처리

모든 JavaScript 로직과 마찬가지로 웹 워커에서 발생하는 모든 오류를 처리해야 합니다. worker가 실행되는 동안 오류가 발생하면 ErrorEvent가 실행됩니다. 인터페이스에는 문제를 파악하는 데 유용한 세 가지 속성이 포함되어 있습니다. filename: 오류를 일으킨 작업자 스크립트의 이름, lineno: 오류가 발생한 줄 번호, message: 오류에 관한 의미 있는 설명입니다. 다음은 onerror 이벤트 핸들러를 설정하여 오류의 속성을 출력하는 예입니다.

<output id="error" style="color: red;"></output>
<output id="result"></output>

<script>
function onError(e) {
document.getElementById('error').textContent = [
    'ERROR: Line ', e.lineno, ' in ', e.filename, ': ', e.message
].join('');
}

function onMsg(e) {
document.getElementById('result').textContent = e.data;
}

var worker = new Worker('workerWithError.js');
worker.addEventListener('message', onMsg, false);
worker.addEventListener('error', onError, false);
worker.postMessage(); // Start worker without a message.
</script>

: workerWithError.js가 1/x를 수행하려고 하지만 x가 정의되지 않았습니다.

// TODO: DevSite - 인라인 이벤트 핸들러를 사용하여 코드 샘플 삭제됨

workerWithError.js:

self.addEventListener('message', function(e) {
postMessage(1/x); // Intentional error.
};

보안에 대한 한마디

로컬 액세스 제한

Chrome의 보안 제한사항으로 인해 작업자는 최신 버전의 브라우저에서 로컬로 (예: file://에서) 실행되지 않습니다. 오히려 조용히 실패합니다! file:// 스키마로 앱을 실행하려면 --allow-file-access-from-files 플래그를 설정하여 Chrome을 실행합니다.

다른 브라우저에는 동일한 제한이 적용되지 않습니다.

동일 출처 고려사항

작업자 스크립트는 호출 페이지와 구성표가 동일한 외부 파일이어야 합니다. 따라서 data: URL이나 javascript: URL에서 스크립트를 로드할 수 없으며 https: 페이지는 http: URL로 시작하는 작업자 스크립트를 시작할 수 없습니다.

사용 사례

그렇다면 어떤 종류의 앱이 웹 워커를 활용할까요? 다음은 머리를 흔드는 데 도움이 되는 몇 가지 아이디어입니다.

  • 나중에 사용하기 위해 데이터 미리 가져오기 및 캐싱
  • 코드 구문 강조 표시 또는 기타 실시간 텍스트 서식 지정
  • 맞춤법 검사기
  • 동영상 또는 오디오 데이터 분석
  • 백그라운드 I/O 또는 웹 서비스의 폴링입니다.
  • 큰 배열 또는 방대한 JSON 응답 처리
  • <canvas>에서 이미지 필터링
  • 로컬 웹 데이터베이스의 여러 행을 업데이트합니다.

Web Workers API와 관련된 사용 사례에 관한 자세한 내용은 Workers 개요를 참고하세요.

데모

참조