Web Workers API

김동현·2026년 3월 22일

Web Workers 사용하기

Web Workers는 웹 콘텐츠가 백그라운드 스레드에서 스크립트를 실행할 수 있는 간단한 방법이에요. Worker 스레드는 사용자 인터페이스를 방해하지 않고 작업을 수행할 수 있어요. 또한, fetch()XMLHttpRequest API를 사용하여 네트워크 요청을 할 수 있어요. 일단 생성되면, Worker는 해당 코드가 지정한 이벤트 핸들러에 메시지를 게시하여 자신을 생성한 JavaScript 코드에 메시지를 보낼 수 있어요 (그 반대도 가능해요).

이 글에서는 Web Workers 사용에 대한 자세한 소개를 제공해요.

💡 강사 팁: Web Workers는 JavaScript의 싱글 스레드 한계를 극복할 수 있는 정말 강력한 도구예요!

JavaScript는 기본적으로 싱글 스레드라서 복잡한 계산을 하면 UI가 멈추거나 버벅거리는 현상이 발생하죠. 예를 들어 대용량 데이터 정렬, 이미지 처리, 암호화 작업 같은 CPU 집약적인 작업을 메인 스레드에서 하면 사용자가 버튼을 클릭해도 반응이 없는 것처럼 느껴져요. Web Workers를 사용하면 이런 무거운 작업을 별도의 스레드에서 처리할 수 있어서 UI가 항상 부드럽게 유지돼요.

제 경험상 가장 많이 사용되는 케이스는:

  • 대용량 JSON 파싱: 수십 MB 크기의 JSON 데이터를 파싱할 때
  • 이미지/비디오 처리: Canvas를 이용한 필터 적용이나 썸네일 생성
  • 복잡한 계산: 차트 데이터 계산, 통계 분석 등
  • 실시간 데이터 처리: WebSocket으로 받은 대량의 데이터 처리

다만 주의할 점이 있어요! Worker에서는 DOM에 접근할 수 없어요. documentwindow 객체를 사용할 수 없다는 뜻이에요. 그래서 Worker에서는 계산만 하고, 결과를 메인 스레드로 보내서 DOM 업데이트는 메인 스레드에서 처리하는 패턴을 사용해야 해요.

또한 Worker와 메인 스레드 간의 데이터 전송은 복사(structured clone)로 이루어지기 때문에, 아주 큰 데이터를 자주 주고받으면 오히려 성능이 나빠질 수 있어요. 이럴 때는 Transferable 객체(예: ArrayBuffer)를 사용해서 소유권을 이전하는 방식을 고려해보세요!

Web Workers API (웹 워커 API)

안녕하세요, 미래의 프론트엔드 개발자 여러분! 👋 이번에 우리가 함께 정복할 MDN 문서는 바로 Web Workers API입니다.

자바스크립트를 공부하다 보면 한 번쯤 "자바스크립트는 싱글 스레드(Single-threaded) 언어다"라는 말을 들어보셨을 텐데요. 이 말은 즉, 한 번에 하나의 작업밖에 처리하지 못한다는 뜻입니다. 그렇다면 무거운 계산을 돌릴 때 화면이 멈춰버리는 현상은 어떻게 해결할까요? 바로 이 '웹 워커(Web Workers)'가 그 구원투수 역할을 합니다. 문서의 내용을 하나도 빠짐없이, 알기 쉽게 풀어서 설명해 드릴 테니 잘 따라와 주세요!


웹 워커(Web Workers)는 웹 애플리케이션의 메인 실행 스레드(Main execution thread)와는 별개로, 분리된 백그라운드 스레드에서 스크립트 연산을 실행할 수 있게 해주는 기술입니다. 이 기술의 가장 큰 장점은, 아주 무겁고 고된 처리 작업(연산)을 별도의 스레드에서 수행할 수 있다는 것입니다. 덕분에 메인 스레드(보통 UI를 담당하는 스레드)는 블로킹(막힘)이나 속도 저하 없이 아주 매끄럽게 돌아갈 수 있게 됩니다.

💡 강사의 팁: 현업에서 웹 워커는 대용량 배열 데이터를 정렬하거나, 브라우저 단에서 복잡한 이미지/비디오 처리를 하거나, 암호화 해시를 계산할 때 주로 사용합니다. 메인 스레드는 오직 "사용자가 화면을 클릭하고, 스크롤하고, 애니메이션을 보는 것"에만 집중하게 놔두고, 더러운(?) 일은 백그라운드의 워커에게 던져주는 거죠!


개념 및 사용법 (Concepts and usage)

워커(Worker)는 Worker()와 같은 생성자(constructor)를 사용하여 만들어진 객체입니다. 이 생성자는 이름이 지정된 자바스크립트 파일을 실행하는데, 이 파일 안에는 워커 스레드(백그라운드)에서 실행될 코드들이 들어있게 됩니다.

표준 JavaScript 내장 함수들(String, Array, Object, JSON 등)을 포함하여, 여러분은 워커 스레드 내부에서 원하는 거의 모든 코드를 실행할 수 있습니다.

하지만 몇 가지 중요한 예외 사항이 있습니다:
예를 들어, 워커 내부에서는 DOM(문서 객체 모델)을 직접 조작할 수 없습니다. 또한 Window 객체의 일부 기본 메서드나 속성들도 사용할 수 없죠. 워커 안에서 실행 가능한 코드가 무엇인지 더 자세히 알고 싶다면 워커에서 지원하는 함수들워커에서 지원하는 Web API들 문서를 참고해 보세요.

💡 강사의 부연 설명: 왜 DOM에 접근할 수 없을까요? DOM 조작은 반드시 "메인 스레드(UI 스레드)"에서만 일어나야(Thread-safe) 하기 때문입니다. 만약 백그라운드 스레드와 메인 스레드가 동시에 화면의 같은 <div>를 수정하려고 든다면 화면이 완전히 꼬여버리겠죠? 그래서 워커 안에서는 오직 '데이터 계산'만 하고, 그 결과를 메인 스레드로 넘겨서 메인 스레드가 화면을 그리도록 설계된 것입니다.

워커와 메인 스레드 사이의 데이터 전송은 메시지(messages) 시스템을 통해 이루어집니다.
양쪽 모두 postMessage() 메서드를 사용하여 메시지를 전송하고, onmessage 이벤트 핸들러를 통해 수신된 메시지에 응답합니다. (실제 데이터는 message 이벤트 객체의 data 속성 안에 들어있습니다). 여기서 중요한 점은, 데이터는 양쪽이 상태를 '공유(shared)'하는 것이 아니라 복사(copied)되어 전달된다는 것입니다.

워커는 부모 페이지와 동일한 출처(origin) 내에 호스팅 되어있기만 하다면, 자기 안에서 또 다른 새로운 워커들을 파생(spawn)시킬 수도 있습니다.

또한, 워커는 fetch()XMLHttpRequest API를 사용하여 네트워크 요청을 보낼 수 있습니다. (다만, XMLHttpRequestresponseXML 속성은 워커 내에서 항상 null을 반환한다는 점은 기억해 두세요.)


워커의 종류 (Worker types)

워커에는 몇 가지 서로 다른 유형이 존재합니다.

  • 전용 워커 (Dedicated workers): 단일 스크립트(한 페이지)에서만 독점적으로 사용되는 워커입니다. 이 워커의 실행 컨텍스트는 DedicatedWorkerGlobalScope 객체로 표현됩니다.
  • 공유 워커 (Shared workers): 여러 개의 스크립트(다른 윈도우, 탭, iframe 등)에서 공통으로 접근하고 활용할 수 있는 워커입니다. 단, 워커와 동일한 도메인 내에 있어야 합니다. 전용 워커보다는 조금 더 복잡하며, 스크립트들은 활성화된 포트(active port)를 통해 통신해야 합니다.
  • 서비스 워커 (Service Workers): 웹 애플리케이션, 브라우저, 그리고 네트워크(네트워크가 연결된 경우) 사이에 위치하는 '프록시 서버' 역할을 하는 특별한 워커입니다. 서비스 워커의 주된 목적은 훌륭한 오프라인 경험을 만들고, 네트워크 요청을 가로채서 네트워크 연결 상태에 따라 적절한 조치를 취하거나, 서버에 있는 에셋(이미지, 스크립트 등)을 업데이트하는 것입니다. 또한 푸시 알림(push notifications)이나 백그라운드 동기화(background sync) API에도 접근할 수 있게 해줍니다.

💡 강사의 팁: 요즘 프론트엔드 생태계에서 PWA(Progressive Web App)를 만들 때 빼놓을 수 없는 핵심 기술이 바로 세 번째에 있는 '서비스 워커'입니다. 오프라인 상태에서도 웹사이트가 접속되도록 캐싱(Caching)을 담당하는 아주 중요한 녀석이니 꼭 기억해 두세요!


워커 컨텍스트 (Worker contexts)

워커 내부에서는 Window 객체를 직접 사용할 수 없지만, WindowOrWorkerGlobalScope라는 공유 믹스인(mixin)에 정의된 많은 메서드들은 워커 내에서도 똑같이 쓸 수 있습니다. 이러한 기능들은 WorkerGlobalScope에서 파생된 각각의 자체 컨텍스트를 통해 워커에게 제공됩니다.


인터페이스 (Interfaces)

워커를 다루기 위한 주요 객체(인터페이스)들을 정리해 보겠습니다.

Worker

실행 중인 워커 스레드를 나타냅니다. 이 객체를 통해 실행 중인 워커 코드에 메시지를 보낼 수 있습니다.

WorkerLocation

Worker가 실행 중인 스크립트의 절대 경로(absolute location)를 정의합니다.

SharedWorker

여러 개의 브라우징 컨텍스트(예: 창, 탭, iframe)나 심지어 다른 워커들에서도 접근할 수 있는 특별한 종류의 워커를 나타냅니다.

WorkerGlobalScope

모든 워커의 가장 기본이 되는 범용 스코프(scope)를 나타냅니다. 일반 웹 문서에서 Window 객체가 하는 역할과 같습니다. 다양한 종류의 워커들은 이 인터페이스를 상속받아 더 구체적인 기능들을 추가한 각자의 스코프 객체를 가집니다.

DedicatedWorkerGlobalScope

전용 워커(Dedicated worker)의 스코프를 나타냅니다. WorkerGlobalScope를 상속받고, 전용 워커만을 위한 기능들을 추가합니다.

SharedWorkerGlobalScope

공유 워커(Shared worker)의 스코프를 나타냅니다. WorkerGlobalScope를 상속받고, 공유 워커 전용 기능들을 추가합니다.

WorkerNavigator

사용자 에이전트(클라이언트 브라우저)의 신원 정보와 상태를 나타냅니다.


예제 (Examples)

웹 워커를 어떻게 사용하는지 직접 확인할 수 있도록 몇 가지 데모를 만들어 두었습니다. (링크를 통해 소스코드와 실제 실행 화면을 확인해 보세요!)

이 데모들이 내부적으로 어떻게 작동하는지 상세한 설명이 필요하다면, 웹 워커 사용하기 (Using Web Workers) 문서를 확인해 보세요.

웹 워커 API (Web Workers API)

안녕하세요! 프론트엔드 개발의 핵심, 자바스크립트의 한계를 넘어설 수 있게 해주는 웹 워커(Web Workers)에 대해 계속해서 알아보겠습니다. 저번 시간에 이어 본격적인 API 사용법과 개념들을 아주 꼼꼼하게 파헤쳐 볼게요. 공식 문서의 내용을 하나도 빠짐없이, 여러분이 실무에서 바로 써먹을 수 있는 꿀팁들과 함께 구어체로 친절하게 설명해 드리겠습니다. 준비되셨나요? 출발해 봅시다! 🚀


웹 워커 API (Web Workers API)

워커(Worker)는 Worker()와 같은 생성자를 사용하여 만들어진 객체로, 이름이 지정된 자바스크립트 파일을 실행합니다. 이 파일에는 워커 스레드(백그라운드)에서 실행될 코드가 들어있습니다. 여기서 아주 중요한 점이 하나 있는데요! 워커는 현재의 window와는 완전히 다른 전역 컨텍스트(global context)에서 실행됩니다.

따라서, Worker 내부에서 현재의 전역 스코프를 가져오기 위해 (self 대신) window 단축어를 사용하면 에러가 발생하게 됩니다.

워커의 컨텍스트는 전용 워커(Dedicated workers)의 경우 DedicatedWorkerGlobalScope 객체로 표현됩니다. (전용 워커는 단일 스크립트에 의해서만 활용되는 표준 워커입니다. 반면, 공유 워커는 SharedWorkerGlobalScope를 사용하죠.) 전용 워커는 자신을 처음 생성(spawn)한 스크립트에서만 접근할 수 있는 반면, 공유 워커는 여러 스크립트에서 다 같이 접근할 수 있습니다.

참고: 워커에 대한 레퍼런스 문서와 추가 가이드는 웹 워커 API 랜딩 페이지 (The Web Workers API landing page)를 확인해 보세요.

여러분은 몇 가지 예외 사항만 제외하면 워커 스레드 내부에서 원하는 코드를 마음껏 실행할 수 있습니다. 예를 들어, 워커 내부에서는 DOM을 직접 조작할 수 없고, window 객체의 일부 기본 메서드나 속성들을 사용할 수 없습니다. 하지만 window 아래에 있는 수많은 항목들은 사용할 수 있는데요! 여기에는 WebSockets이나 IndexedDB 같은 데이터 저장 메커니즘도 포함됩니다. 더 자세한 내용은 워커에서 사용 가능한 함수와 클래스들 문서를 참고해 주세요.

워커와 메인 스레드 사이의 데이터는 메시지 시스템을 통해 전송됩니다. 양쪽 모두 postMessage() 메서드를 사용해서 메시지를 보내고, onmessage 이벤트 핸들러를 통해 메시지에 응답합니다. (실제 메시지 데이터는 message 이벤트의 data 속성 안에 들어있습니다.) 데이터는 공유되는 것이 아니라 복사(copied)되어 전달됩니다.

워커는 부모 페이지와 동일한 출처(origin) 내에 호스팅 되어있기만 하다면, 새로운 워커를 또다시 생성(spawn)할 수도 있습니다.

게다가, 워커는 fetch()XMLHttpRequest API를 사용해서 네트워크 요청도 보낼 수 있습니다! (단, XMLHttpRequestresponseXML 속성은 항상 null을 반환한다는 점은 주의하세요.)

💡 강사의 팁: 왜 DOM 조작이 안 되고 window 객체가 없을까요? 워커는 화면(UI)을 그리는 스레드와 물리적으로 분리되어 있기 때문입니다. 만약 백그라운드에서 화면 요소의 색상을 바꾸려고 하면 메인 스레드와 충돌(동기화 문제)이 일어날 수 있어요. 그래서 UI 업데이트는 메인 스레드에게 온전히 맡기고, 워커는 그저 뒤에서 묵묵히 '계산기' 역할만 충실히 수행하도록 설계된 것이랍니다!


전용 워커 (Dedicated workers)

앞서 언급했듯이, 전용 워커는 오직 자신을 호출한 단일 스크립트에서만 접근할 수 있습니다. 이 섹션에서는 저희가 준비한 기본 전용 워커 예제 (Basic dedicated worker example)에 사용된 자바스크립트를 살펴볼 텐데요 (전용 워커 실행해보기). 이 데모는 두 개의 숫자를 입력받아 서로 곱할 수 있게 해줍니다. 숫자들은 전용 워커로 전송되어 서로 곱해지고, 그 결과가 다시 메인 페이지로 반환되어 화면에 표시됩니다.

이 예제는 꽤나 단순해 보이지만, 기본 워커 개념을 소개하기 위해 일부러 간단하게 유지했습니다. 더 고급 세부 사항들은 문서 후반부에서 다루겠습니다.

워커 기능 감지 (Worker feature detection)

오류 처리를 조금 더 세밀하게 통제하고 이전 브라우저와의 하위 호환성(backwards compatibility)을 유지하기 위해, 워커에 접근하는 코드를 다음과 같이 감싸주는 것이 좋은 습관입니다 (main.js):

if (window.Worker) {
  // …
}

💡 강사의 팁: 요즘 대부분의 모던 브라우저는 웹 워커를 완벽하게 지원합니다. 하지만 실무에서는 사내 정책상 구형 IE 버전을 지원해야 하는 레거시 프로젝트를 만날 수도 있죠. 이렇게 기능이 존재하는지 먼저 확인(Feature Detection)하는 방어적 프로그래밍 습관은 어떤 API를 사용하든 항상 훌륭한 자세입니다!

전용 워커 생성하기 (Spawning a dedicated worker)

새로운 워커를 만드는 것은 아주 간단합니다. 그저 워커 스레드에서 실행할 스크립트의 URI를 지정하여 Worker() 생성자를 호출하기만 하면 됩니다 (main.js):

const myWorker = new Worker("worker.js");

참고: webpack, Vite, Parcel과 같은 번들러(Bundlers)들은 Worker() 생성자에 import.meta.url을 기준으로 상대적으로 해석(resolved)되는 URL을 전달할 것을 권장합니다. 예를 들면 다음과 같습니다:

const myWorker = new Worker(new URL("worker.js", import.meta.url));

이렇게 하면 경로가 현재 HTML 페이지 대신 현재 스크립트 파일을 기준으로 맞춰집니다. 이를 통해 번들러가 파일 이름 변경(renaming) 같은 최적화 작업을 안전하게 수행할 수 있습니다. (만약 이렇게 하지 않으면 worker.js URL이 번들러가 제어하지 않는 다른 파일을 가리킬 수도 있어서, 번들러가 섣불리 어떤 가정도 할 수 없게 되거든요.)

💡 강사의 부연 설명: 이 부분은 현대 프론트엔드 개발(React, Vue 등)에서 정말정말 중요합니다! 빌드 도구를 쓰면 난독화 때문에 빌드 후 파일명이 worker.a8f23.js 처럼 무작위로 바뀝니다. import.meta.url을 써서 넘겨주면, Vite나 Webpack이 "아! 이건 워커 파일이구나!" 하고 알아차리고 빌드할 때 파일 경로를 알맞게 척척 변환해 줍니다.

전용 워커와 메시지 주고받기 (Sending messages to and from a dedicated worker)

워커의 마법은 바로 postMessage() 메서드와 onmessage 이벤트 핸들러를 통해 일어납니다. 워커에게 메시지를 보내고 싶다면, 아래처럼 메시지를 전송(post)하면 됩니다 (main.js):

[first, second].forEach((input) => {
  input.onchange = () => {
    myWorker.postMessage([first.value, second.value]);
    console.log("Message posted to worker");
  };
});

여기를 보시면 firstsecond라는 두 개의 <input> 요소가 있습니다. 둘 중 하나의 값이 변경될 때마다, myWorker.postMessage([first.value, second.value])가 호출되어 두 값을 배열 형태로 워커에게 보냅니다. 메시지에는 여러분이 원하는 거의 모든 종류의 데이터를 담아서 보낼 수 있어요.

이제 워커 내부에서는, 다음과 같이 이벤트 핸들러 블록을 작성하여 메시지를 받았을 때 응답하도록 만들 수 있습니다 (worker.js):

onmessage = (e) => {
  console.log("Message received from main script");
  const workerResult = `Result: ${e.data[0] * e.data[1]}`;
  console.log("Posting message back to main script");
  postMessage(workerResult);
};

onmessage 핸들러를 사용하면 메시지가 도착할 때마다 특정 코드를 실행할 수 있습니다. 이때 전송된 메시지 자체는 message 이벤트 객체의 data 속성 안에 들어있습니다. 여기서는 두 숫자를 곱한 다음, 다시 postMessage()를 사용해서 계산 결과를 메인 스레드로 돌려보내고 있습니다.

다시 메인 스레드로 돌아와서, 이번엔 워커가 돌려보낸 메시지에 응답하기 위해 똑같이 onmessage를 사용합니다:

myWorker.onmessage = (e) => {
  result.textContent = e.data;
  console.log("Message received from worker");
};

여기서는 메시지 이벤트의 데이터를 가져와서 결과 문단(paragraph)의 textContent에 할당하여, 사용자가 계산된 결과를 볼 수 있도록 해줍니다.

참고: 메인 스크립트 스레드에서 사용할 때는 onmessagepostMessage()가 반드시 Worker 객체(여기선 myWorker)에 매달려(hung off) 있어야 합니다. 하지만 워커 내부에서 사용할 때는 그럴 필요가 없습니다. 워커 내부에서는 워커 자신이 곧 전역 스코프(global scope)이기 때문입니다.

참고: 메인 스레드와 워커 사이에 메시지가 전달될 때, 데이터는 공유(shared)되는 것이 아니라 복사(copied)되거나 "이전(transferred, 소유권 이동)"됩니다. 이에 대한 훨씬 더 자세한 설명은 워커와 데이터 주고받기: 추가 세부 사항을 읽어보세요.

워커 종료하기 (Terminating a worker)

메인 스레드에서 실행 중인 워커를 즉시 강제 종료해야 할 필요가 있다면, 워커의 terminate 메서드를 호출하면 됩니다:

myWorker.terminate();

이렇게 하면 워커 스레드가 즉시 죽게 됩니다(killed immediately).

💡 강사의 팁: 불필요한 워커를 방치하면 브라우저의 메모리와 CPU 자원을 낭비하게 됩니다. 특히 SPA(Single Page Application) 환경에서 컴포넌트가 마운트 해제(Unmount) 될 때는, 반드시 terminate()를 호출해서 열일하던 워커를 퇴근시켜(메모리 해제) 주셔야 합니다!

오류 처리하기 (Handling errors)

워커 내에서 런타임 에러(runtime error)가 발생하면, 워커의 onerror 이벤트 핸들러가 호출됩니다. 이 핸들러는 ErrorEvent 인터페이스를 구현하는 error라는 이름의 이벤트를 받게 됩니다.

이 이벤트는 버블링(bubble)되지 않으며 취소할 수(cancelable) 있습니다. 브라우저의 기본 동작이 일어나는 것을 막으려면, 워커에서 에러 이벤트의 preventDefault() 메서드를 호출하면 됩니다.

에러 이벤트는 우리가 관심 있게 볼 만한 다음 세 가지 필드를 가지고 있습니다:

  • message: 사람이 읽을 수 있는(human-readable) 형태의 에러 메시지입니다.
  • filename: 에러가 발생한 스크립트 파일의 이름입니다.
  • lineno: 에러가 발생한 스크립트 파일의 줄 번호(line number)입니다.

서브워커 생성하기 (Spawning subworkers)

워커는 원한다면 또 다른 워커를 더 만들어낼 수 있습니다. 이렇게 생성된 소위 하위 워커(sub-workers)들은 반드시 부모 페이지와 동일한 출처(same origin) 내에 호스팅 되어야 합니다. 또한, 서브워커의 URI는 소유 페이지(owning page)가 아니라 부모 워커(parent worker)의 위치를 기준으로 상대적으로 해석됩니다. 이렇게 함으로써 워커들이 자신의 의존성 파일들이 어디에 있는지 쉽게 추적할 수 있도록 해줍니다.

스크립트 및 라이브러리 가져오기 (Importing scripts and libraries)

워커 스레드는 importScripts()라는 전역 함수에 접근할 수 있는데, 이 함수를 사용하면 다른 스크립트를 워커 내부로 가져올(import) 수 있습니다. 이 함수는 가져올 리소스의 URI를 매개변수로 0개 이상 받을 수 있습니다. 다음 예제들은 모두 유효한 코드입니다:

importScripts(); /* 아무것도 가져오지 않음 */
importScripts("foo.js"); /* "foo.js"만 가져옴 */
importScripts("foo.js", "bar.js"); /* 두 개의 스크립트를 가져옴 */
importScripts(
  "//[example.com/hello.js](https://example.com/hello.js)",
); /* 다른 출처(origins)의 스크립트도 가져올 수 있음 */

브라우저는 나열된 각각의 스크립트를 다운로드하고 실행합니다. 그러고 나면 각 스크립트에 있는 전역 객체들을 워커에서 바로 사용할 수 있게 되죠. 만약 스크립트를 로드할 수 없다면 NETWORK_ERROR가 던져지고, 그 이후의 코드는 실행되지 않습니다. 하지만 이전에 이미 실행되었던 코드(예: setTimeout()을 사용해 지연시킨 코드 포함)들은 계속해서 정상적으로 작동합니다. 또한 importScripts() 메서드 이후에 작성된 함수 선언(Function declarations)들도 유지되는데, 함수 선언은 항상 다른 코드들보다 먼저 평가(호이스팅, hoisted)되기 때문입니다.

참고: 스크립트들은 무작위 순서로 다운로드될 수 있지만, 실행만큼은 여러분이 importScripts()에 전달한 파일 이름 순서대로 정확히 실행됩니다. 이 과정은 동기적으로(synchronously) 진행되므로, importScripts()는 모든 스크립트가 로드되고 실행되기 전까지는 값을 반환하지 않고 멈춰있게 됩니다.

💡 강사의 팁: 요즘은 최신 브라우저 환경에서 ES 모듈(ES Modules)을 워커에 도입할 수도 있습니다. 워커를 생성할 때 new Worker('worker.js', { type: 'module' }) 이라고 옵션을 주면, importScripts() 대신 우리가 흔히 쓰는 import / export 문법을 워커 안에서도 아주 깔끔하게 사용할 수 있답니다!


공유 워커 (Shared workers)

공유 워커(Shared worker)는 여러 개의 스크립트에서 접근할 수 있는 워커입니다. 심지어 그 스크립트들이 서로 다른 윈도우, iframe, 또는 다른 워커에 의해 접근되고 있더라도 말이죠! 이 섹션에서는 저희의 기본 공유 워커 예제 (Basic shared worker example)에 사용된 자바스크립트를 살펴볼 텐데요 (공유 워커 실행해보기). 이 예제는 앞서 본 전용 워커 예제와 매우 비슷하지만, 서로 다른 스크립트 파일에서 처리되는 두 가지 기능(하나는 두 숫자를 곱하기, 다른 하나는 숫자를 제곱하기)을 가지고 있다는 점이 다릅니다. 이 두 스크립트는 실제 계산을 수행하기 위해 단 하나의 똑같은 공유 워커를 사용합니다.

여기서는 전용 워커와 공유 워커 사이의 차이점에 집중해 보겠습니다. 이 예제에는 두 개의 HTML 페이지가 있고, 각 페이지의 자바스크립트가 동일한 단일 워커 파일을 가져다 쓴다는 점을 기억해 주세요.

참고: 만약 SharedWorker가 여러 브라우징 컨텍스트(창, 탭 등)에서 접근 가능하다면, 그 모든 브라우징 컨텍스트들은 반드시 정확히 동일한 출처(동일한 프로토콜, 호스트, 포트)를 공유해야만 합니다.

참고: Firefox 브라우저에서는 공유 워커를 사생활 보호 창(private window)과 일반 창에 로드된 문서들끼리는 서로 공유할 수 없습니다. (Firefox 버그 1177621 참고).

공유 워커 생성하기 (Spawning a shared worker)

새로운 공유 워커를 생성하는 방법은 전용 워커와 거의 똑같지만, 생성자 이름이 다릅니다. (index.htmlindex2.html을 확인해보세요.) 각 페이지는 다음과 같은 코드를 사용해 워커를 띄워야 합니다.

const myWorker = new SharedWorker("worker.js");

가장 큰 차이점 중 하나는, 공유 워커와 통신할 때는 반드시 port 객체를 경유해야 한다는 것입니다. 스크립트들이 워커와 대화하기 위해 사용할 수 있는 명시적인 포트(port)가 열리는 셈이죠. (전용 워커의 경우에는 이 과정이 암묵적으로 숨겨져서 알아서 처리됩니다.)

포트 연결(port connection)은 메시지를 전송하기 전에 onmessage 이벤트 핸들러를 사용해 암묵적으로 시작하거나, start() 메서드를 통해 명시적으로 시작해야 합니다. start()를 호출하는 방식은 오직 addEventListener() 메서드를 사용해 message 이벤트를 연결할 때만 필요합니다.

참고: 포트 연결을 열기 위해 start() 메서드를 사용하는 경우, 양방향 통신이 필요하다면 부모 스레드와 워커 스레드 양쪽 모두에서 이 메서드를 호출해 주어야 합니다.

공유 워커와 메시지 주고받기 (Sending messages to and from a shared worker)

이제 이전과 마찬가지로 워커에 메시지를 보낼 수 있습니다. 하지만 이번에는 postMessage() 메서드를 반드시 port 객체를 통해서 호출해야 합니다. (이와 비슷한 구조를 multiply.jssquare.js 양쪽 모두에서 보실 수 있을 거예요.)

squareNumber.onchange = () => {
  myWorker.port.postMessage([squareNumber.value, squareNumber.value]);
  console.log("Message posted to worker");
};

자, 이제 워커 쪽을 볼까요? 여기도 살짝 복잡해집니다. (worker.js)

onconnect = (e) => {
  const port = e.ports[0];

  port.onmessage = (e) => {
    const workerResult = `Result: ${e.data[0] * e.data[1]}`;
    port.postMessage(workerResult);
  };
};

우선, 포트에 연결이 발생했을 때(즉, 부모 스레드에서 onmessage 이벤트 핸들러가 세팅되거나 start() 메서드가 명시적으로 호출되었을 때) 코드를 실행하기 위해 onconnect 핸들러를 사용합니다.

우리는 이 이벤트 객체의 ports 속성을 사용해 해당 포트를 잡은 다음 변수에 저장해 둡니다.

그다음, 계산을 수행하고 메인 스레드로 결과를 돌려보내기 위해 그 포트 위에 onmessage 핸들러를 추가합니다. 이렇게 워커 스레드에서 onmessage 핸들러를 세팅하는 작업은 부모 스레드로 향하는 포트 연결을 알아서 암묵적으로 열어주기 때문에, 아까 언급했던 port.start() 호출이 워커 쪽에서는 사실상 필요하지 않게 됩니다.

마지막으로, 다시 메인 스크립트로 돌아와서 메시지를 어떻게 처리하는지 보겠습니다 (multiply.jssquare.js):

myWorker.port.onmessage = (e) => {
  result2.textContent = e.data;
  console.log("Message received from worker");
};

워커로부터 포트를 통해 메시지가 다시 돌아오면, 적절한 결과 표시 문단(paragraph) 안에 계산된 결과값을 쏙 집어넣어 줍니다.

💡 강사의 팁: 공유 워커는 여러 탭을 열어두는 유저를 위해 설계할 때 최고입니다! 예를 들어 여러 탭에서 각각 똑같은 웹소켓(WebSocket) 서버에 연결을 맺으면 서버 자원이 낭비되겠죠? 이때 '공유 워커' 하나에만 웹소켓을 연결해두고, 열려있는 모든 탭이 이 하나의 공유 워커를 통해 데이터를 주고받도록 만들면 네트워크 자원을 엄청나게 절약할 수 있습니다!


스레드 안전성에 대하여 (About thread safety)

Worker 인터페이스는 실제로 진짜 운영체제(OS) 수준의 스레드를 생성합니다. 그래서 동시성 프로그래밍에 조심스러운 프로그래머들은 "조심하지 않으면 이 동시성(concurrency) 때문에 내 코드에 '기상천외한' 부작용이 생기는 건 아닐까?" 하고 걱정할 수 있습니다.

하지만 너무 걱정하지 않으셔도 됩니다! 웹 워커는 다른 스레드들과 통신하는 지점들이 아주 철저하게 통제되어 있기 때문에, 동시성 문제를 일으키는 것 자체가 오히려 굉장히 어렵습니다. 워커는 스레드 안전성이 보장되지 않는 컴포넌트나 DOM에 아예 접근할 수 없게 막혀있습니다. 게다가 스레드 안팎으로 데이터를 넘길 때는 오로지 직렬화된(serialized) 객체를 통해 특정한 데이터만 복사해서 넘겨주어야 하죠. 즉, 내 코드에 동시성 문제를 일으키려면 일부러 정말 엄청난 노력을 기울여야 할 정도입니다!


콘텐츠 보안 정책 (Content security policy)

워커는 자신을 생성한 문서(document)와는 확연히 분리된 고유의 실행 컨텍스트(execution context)를 가진 것으로 간주됩니다. 이러한 이유 때문에 워커는 일반적으로 자신을 생성한 문서(또는 부모 워커)의 콘텐츠 보안 정책(Content Security Policy, CSP)의 지배를 받지 않습니다.

예를 들어, 어떤 문서가 서버로부터 다음과 같은 HTTP 헤더를 달고 제공되었다고 가정해 봅시다:

Content-Security-Policy: script-src 'self'

이 헤더는 해당 문서 안에 포함된 모든 스크립트가 eval() 같은 위험한 함수를 사용하지 못하도록 꽉 막아버립니다. 하지만, 만약 이 스크립트가 새로운 워커를 생성했다면, 그 워커의 컨텍스트 안에서 도는 코드는 eval() 사용이 허용됩니다.

워커에게도 별도의 콘텐츠 보안 정책을 지정하고 싶다면, 워커 스크립트 파일을 제공하는 네트워크 요청(request)에 대한 응답 헤더로 Content-Security-Policy 헤더를 따로 설정해 주어야 합니다.

물론 예외도 존재합니다. 워커 스크립트의 출처(origin)가 전역적으로 고유한 식별자인 경우(예를 들어, 워커의 URL이 data:blob: 스킴인 경우)입니다. 이 경우에는 워커가 자신을 생성한 부모 문서나 부모 워커의 CSP를 그대로 상속받게 됩니다.


워커와 데이터 주고받기: 추가 세부 사항 (Transferring data to and from workers: further details)

메인 페이지와 워커 사이를 오가는 데이터는 공유되는 것이 아니라 복사(copied)됩니다 (물론 명시적으로 공유(shared) 가능한 특정 객체들은 예외입니다).

객체들은 워커로 넘겨질 때 직렬화(serialized) 과정을 거치고, 그 후에 반대편에 도착해서 다시 역직렬화(de-serialized) 됩니다. 페이지와 워커는 동일한 인스턴스(메모리 주소)를 공유하지 않으므로, 최종적인 결과는 양쪽 끝에 똑같은 복제본(duplicate)이 하나씩 생기는 것입니다. 대부분의 브라우저들은 이 기능을 구현하기 위해 구조적 클로닝 알고리즘(Structured cloning algorithm)을 사용합니다.

이제 여러분도 잘 아시겠지만, 이 두 스레드 사이의 데이터 교환은 postMessage()를 사용한 메시지를 통해 이루어지며, message 이벤트의 data 속성이 워커로부터 돌려받은 데이터를 품고 있습니다.

example.html (메인 페이지):

const myWorker = new Worker("my_task.js");

myWorker.onmessage = (event) => {
  console.log(`Worker said : "${event.data}"`);
};

myWorker.postMessage({ lastUpdate: new Date() });

my_task.js (워커):

self.onmessage = (event) => {
  postMessage(`Last updated: ${event.data.lastUpdate.toDateString()}`);
};

구조적 클로닝(Structured cloning) 알고리즘은 순수 JSON 데이터뿐만 아니라, JSON이 처리하지 못하는 복잡한 데이터 구조 — 예를 들면 Date 객체나 순환 참조(circular references) 같은 것들 — 도 거뜬하게 복사해서 넘겨받을 수 있게 해주는 아주 똑똑한 녀석이랍니다.

데이터 전달 예제 (Passing data examples)

예제 1: 고급 JSON 데이터 전달 및 스위칭 시스템 만들기 (Example 1: Advanced passing JSON Data and creating a switching system)

만약 여러분이 꽤나 복잡한 데이터를 주고받아야 하고, 메인 페이지와 워커 양쪽에서 아주 다양한 함수들을 여러 개 호출해야 하는 상황이라면 어떨까요? 이럴 때는 모든 것을 깔끔하게 하나로 묶어 관리할 수 있는 '스위칭 시스템(switching system)'을 구축하는 것이 좋습니다.

먼저, 메인 스레드 쪽에 QueryableWorker라는 멋진 클래스를 하나 만들어 보겠습니다. 이 클래스는 워커 파일의 URL, 기본(default) 리스너, 그리고 에러 핸들러를 인자로 받습니다. 이 클래스의 역할은 우리가 사용할 다양한 '리스너(listener)들의 목록'을 추적하고 관리하면서 워커와의 통신을 아주 매끄럽게 도와주는 것입니다.

function QueryableWorker(url, defaultListener, onError) {
  const worker = new Worker(url);
  const listeners = {};

  this.defaultListener = defaultListener ?? (() => {});

  if (onError) {
    worker.onerror = onError;
  }

  this.postMessage = (message) => {
    worker.postMessage(message);
  };

  this.terminate = () => {
    worker.terminate();
  };
}

그다음으로, 이 클래스에 특정 동작을 처리할 리스너들을 추가하거나 제거할 수 있는 메서드들을 달아줍니다.

this.addListener = (name, listener) => {
  listeners[name] = listener;
};

this.removeListener = (name) => {
  delete listeners[name];
};

이 예제에서는 설명을 위해 워커가 두 가지 아주 간단한 연산을 처리하도록 해보겠습니다. 하나는 '두 숫자의 차이(빼기) 구하기'이고, 다른 하나는 '3초 뒤에 알림(alert) 띄우기'입니다.

이를 구현하기 위해 먼저 sendQuery라는 메서드를 구현해 볼게요. 이 메서드는 메인 스레드에서 워커에게 "너 혹시 이런 이름의 함수(method) 가지고 있어? 있으면 이 데이터 좀 처리해 줘!"라고 질의(query)를 던지는 역할을 합니다.

// 이 함수는 최소한 하나의 인자(우리가 워커에게 실행을 요청할 메서드의 이름)를 받아야 합니다.
// 그 뒤를 이어 해당 메서드가 처리하는 데 필요한 추가 인자들을 무한정(...) 넘겨줄 수 있습니다.
this.sendQuery = (queryMethod, ...queryMethodArguments) => {
  if (!queryMethod) {
    throw new TypeError(
      "QueryableWorker.sendQuery takes at least one argument",
    );
  }
  worker.postMessage({
    queryMethod,
    queryMethodArguments,
  });
};

이제 QueryableWorker 클래스의 마지막 퍼즐인 onmessage 메서드를 완성해 보겠습니다. 워커가 우리가 질의한(요청한) 함수를 무사히 실행하고 나면, 워커는 메인 스레드에 있는 어떤 리스너(콜백 함수)를 실행해야 하는지 그 '리스너의 이름'과 '필요한 결과 데이터'를 다시 돌려보낼 것입니다. 그러면 우리는 아까 만들어둔 listeners 목록에서 그 이름과 똑같은 함수를 찾아서 실행해주기만 하면 됩니다.

worker.onmessage = (event) => {
  if (
    event.data instanceof Object &&
    Object.hasOwn(event.data, "queryMethodListener") &&
    Object.hasOwn(event.data, "queryMethodArguments")
  ) {
    listeners[event.data.queryMethodListener].apply(
      this,
      event.data.queryMethodArguments,
    );
  } else {
    this.defaultListener(event.data);
  }
};

자, 이제 워커(백그라운드 스레드) 쪽을 살펴볼 차례입니다. 먼저 우리가 아까 메인 스레드에서 요청하기로 했던 두 가지 간단한 연산을 실제로 수행할 함수들을 만들어 줍니다.

const queryableFunctions = {
  getDifference(a, b) {
    reply("printStuff", a - b);
  },
  waitSomeTime() {
    setTimeout(() => {
      reply("doAlert", 3, "seconds");
    }, 3000);
  },
};

function reply(queryMethodListener, ...queryMethodArguments) {
  if (!queryMethodListener) {
    throw new TypeError("reply - takes at least one argument");
  }
  postMessage({
    queryMethodListener,
    queryMethodArguments,
  });
}

// 이 메서드는 메인 페이지가 QueryWorker의 postMessage를 직접 호출했을 때(우회했을 때)를 대비한 기본 응답기입니다.
function defaultReply(message) {
  // do something
}

이렇게 준비를 해두면, 워커 쪽의 onmessage 메서드는 엄청나게 단순해집니다! 메인 스레드에서 어떤 함수를 실행해 달라고 요청이 오면, 그냥 queryableFunctions 객체에서 그 이름과 똑같은 함수를 꺼내서 실행해버리면 그만이니까요.

onmessage = (event) => {
  if (
    event.data instanceof Object &&
    Object.hasOwn(event.data, "queryMethod") &&
    Object.hasOwn(event.data, "queryMethodArguments")
  ) {
    queryableFunctions[event.data.queryMethod].apply(
      self,
      event.data.queryMethodArguments,
    );
  } else {
    defaultReply(event.data);
  }
};

💡 강사의 팁: 이 패턴은 현업에서도 굉장히 많이 쓰이는 "RPC (Remote Procedure Call)" 패턴과 매우 유사합니다. 메인 스레드는 워커 안에 무슨 코드가 있는지 신경 쓸 필요 없이 그저 "A 함수 실행해서 결과 줘!" 라고 메시지를 쏘고, 워커는 "A 함수 결과 여깄어, B 콜백으로 받아가!" 라고 우아하게 응답하는 것이죠. 구조가 복잡한 프로젝트일수록 이렇게 통신 규약(Protocol)을 만들어 두는 것이 유지보수에 압도적으로 유리합니다!

아래에 전체 구현 코드를 모아두었습니다. 천천히 흐름을 따라가 보세요.

example.html (메인 페이지 HTML):

<ul>
  <li>
    <button id="first-action">What is the difference between 5 and 3?</button>
  </li>
  <li>
    <button id="second-action">Wait 3 seconds</button>
  </li>
  <li>
    <button id="terminate">terminate() the Worker</button>
  </li>
</ul>

메인 페이지에서 실행해야 할 자바스크립트 코드입니다 (HTML 내부에 인라인으로 넣거나 외부 파일로 뺍니다):

// QueryableWorker 인스턴스 메서드들:
//   * sendQuery(질의할 함수 이름, 넘길 인자1, 넘길 인자2, ...): 워커 내부에 있는 특정 함수를 호출합니다.
//   * postMessage(문자열 또는 JSON 데이터): 기본 Worker.prototype.postMessage()와 동일하게 작동합니다.
//   * terminate(): 워커를 즉시 종료합니다.
//   * addListener(이름, 콜백함수): 특정 응답을 처리할 리스너를 등록합니다.
//   * removeListener(이름): 리스너를 제거합니다.

function QueryableWorker(url, defaultListener, onError) {
  const worker = new Worker(url);
  const listeners = {};

  this.defaultListener = defaultListener ?? (() => {});

  if (onError) {
    worker.onerror = onError;
  }

  this.postMessage = (message) => {
    worker.postMessage(message);
  };

  this.terminate = () => {
    worker.terminate();
  };

  this.addListener = (name, listener) => {
    listeners[name] = listener;
  };

  this.removeListener = (name) => {
    delete listeners[name];
  };

  this.sendQuery = (queryMethod, ...queryMethodArguments) => {
    if (!queryMethod) {
      throw new TypeError(
        "QueryableWorker.sendQuery takes at least one argument",
      );
    }
    worker.postMessage({
      queryMethod,
      queryMethodArguments,
    });
  };

  worker.onmessage = (event) => {
    if (
      event.data instanceof Object &&
      Object.hasOwn(event.data, "queryMethodListener") &&
      Object.hasOwn(event.data, "queryMethodArguments")
    ) {
      listeners[event.data.queryMethodListener].apply(
        this,
        event.data.queryMethodArguments,
      );
    } else {
      this.defaultListener(event.data);
    }
  };
}

// 1. 우리가 정성껏 만든 커스텀 워커 클래스를 생성합니다.
const myTask = new QueryableWorker("my_task.js");

// 2. 워커가 일을 끝내고 응답을 보냈을 때 반응할 커스텀 "리스너(콜백)"들을 등록합니다.
myTask.addListener("printStuff", (result) => {
  document
    .getElementById("first-action")
    .parentNode.appendChild(
      document.createTextNode(`The difference is ${result}!`),
    );
});

myTask.addListener("doAlert", (time, unit) => {
  alert(`Worker waited for ${time} ${unit} :-)`);
});

// 3. 버튼을 클릭하면 워커에게 특정 작업을 하라고 "질의(Query)"를 날립니다!
document.getElementById("first-action").addEventListener("click", () => {
  myTask.sendQuery("getDifference", 5, 3);
});
document.getElementById("second-action").addEventListener("click", () => {
  myTask.sendQuery("waitSomeTime");
});
document.getElementById("terminate").addEventListener("click", () => {
  myTask.terminate();
});

my_task.js (워커 파일):

const queryableFunctions = {
  // 예제 #1: 두 숫자의 차이(빼기) 구하기
  getDifference(minuend, subtrahend) {
    reply("printStuff", minuend - subtrahend);
  },

  // 예제 #2: 3초 동안 기다리기
  waitSomeTime() {
    setTimeout(() => {
      reply("doAlert", 3, "seconds");
    }, 3000);
  },
};

// 시스템 관련 함수들

function defaultReply(message) {
  // 메인 페이지에서 sendQuery 대신 postMessage()를 직접 호출했을 때만 실행되는 기본 처리 함수입니다.
}

function reply(queryMethodListener, ...queryMethodArguments) {
  if (!queryMethodListener) {
    throw new TypeError("reply - not enough arguments");
  }
  postMessage({
    queryMethodListener,
    queryMethodArguments,
  });
}

onmessage = (event) => {
  if (
    event.data instanceof Object &&
    Object.hasOwn(event.data, "queryMethod") &&
    Object.hasOwn(event.data, "queryMethodArguments")
  ) {
    queryableFunctions[event.data.queryMethod].apply(
      self,
      event.data.queryMethodArguments,
    );
  } else {
    defaultReply(event.data);
  }
};

메인 페이지에서 워커로, 워커에서 메인 페이지로 전송되는 메시지의 구조(내용)는 여러분 마음대로 바꿀 수 있습니다. "queryMethod", "queryMethodListeners", "queryMethodArguments" 같은 속성 이름들도 QueryableWorker 클래스와 worker 파일 내에서 일관성 있게 똑같이 맞춰주기만 한다면 어떤 이름이든 자유롭게 지어 쓰셔도 무방합니다.


소유권 이전을 통한 데이터 전달 (Transferable objects)

최신 모던 브라우저들은 특정한 타입의 객체들을 아주 극강의 퍼포먼스(high performance)로 워커와 주고받을 수 있는 특별한 추가 기능을 지원합니다. 바로 이전 가능한 객체(Transferable objects)인데요, 이 객체들은 데이터를 하나하나 복사하는 대신 제로-카피(zero-copy) 방식으로 한 컨텍스트에서 다른 컨텍스트로 훅 넘어가 버립니다. 덕분에 엄청나게 거대한 데이터 셋을 주고받을 때 성능이 비약적으로 향상되죠!

예를 들어, 메인 앱에서 워커 스크립트로 1GB짜리 ArrayBuffer를 전송(transfer)한다고 가정해 봅시다. 이 전송이 일어나는 순간, 메인 앱에 있던 원본 ArrayBuffer는 텅 비워져서(cleared) 더 이상 메인 스레드에서는 사용할 수 없게 됩니다. 그 데이터 덩어리의 '소유권' 자체가 (말 그대로 진짜) 워커 컨텍스트로 몽땅 넘어가 버리기 때문입니다.

// 32MB짜리 거대한 "파일" 메모리 공간을 만들고, 0부터 255까지의 연속된 값으로 꽉꽉 채워 넣습니다. (32MB = 1024 * 1024 * 32)
const uInt8Array = new Uint8Array(1024 * 1024 * 32).map((v, i) => i);

// 데이터를 복사하지 않고, 소유권 자체를 통째로 워커에게 '이전(transfer)' 시킵니다!
worker.postMessage(uInt8Array.buffer, [uInt8Array.buffer]);

💡 강사의 팁: 3D 렌더링이나 비디오/오디오 인코딩 같은 미친 듯이 무거운 작업을 프론트엔드에서 돌려야 할 때, postMessage로 데이터를 '복사'해서 넘기면 브라우저가 뻗어버릴 수 있습니다. 이때 두 번째 인자로 버퍼 배열을 넘겨주는 이 Transferable Object 방식을 쓰면, 복사 과정 없이 메모리 주소(소유권)만 휙 넘겨주기 때문에 전송 시간이 0초에 가깝게 단축됩니다! 성능 최적화의 끝판왕이죠.

데이터 공유하기 (Sharing data)

SharedArrayBuffer 객체를 사용하면 워커 스레드와 메인 스레드 두 곳에서 동일한 메모리 공간을 동시에 읽고 쓰며 데이터를 교환할 수 있습니다. 지긋지긋한 메시지 시스템(postMessage)을 거치지 않고 말이죠!

하지만 이렇게 메모리를 공유하는 방식은 결정론(determinism, 예기치 않은 타이밍 문제), 보안, 성능과 관련된 아주 중대한 문제들을 동반할 수 있습니다. 이에 대한 자세한 내용은 자바스크립트 실행 모델 (JavaScript execution model) 문서에서 다루고 있으니 꼭 참고해 보시길 권장합니다.


임베디드 워커 (Embedded workers)

우리가 평소에 쓰는 일반적인 스크립트는 <script> 태그의 src 속성으로 불러오거나 태그 안에 직접 코드를 적어 넣죠. 하지만 워커의 코드를 웹 페이지 안에 직접 끼워 넣는(embed) "공식적인" 방법은 아직 존재하지 않습니다.

하지만 개발자들이 누구입니까? 항상 길을 찾아내죠! <script> 요소에 src 속성을 주지 않고, type 속성에 브라우저가 실행할 수 없는 이상한 MIME 타입(예: text/js-worker)을 적어두면, 브라우저는 이 스크립트를 파싱(실행)하지 않고 무시합니다. 자바스크립트 입장에서는 이 태그를 그냥 거의 모든 텍스트 데이터를 담아둘 수 있는 "데이터 블록(Data blocks)"처럼 활용할 수 있게 되죠.

이 트릭을 활용하면 아래처럼 워커 코드를 HTML 안에 직접 임베드할 수 있습니다!

<!doctype html>
<html lang="en-US">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width" />
    <title>MDN Example - Embedded worker</title>
    <script type="text/js-worker">
      // 이 스크립트는 브라우저의 JS 엔진에 의해 파싱(실행)되지 않습니다! MIME 타입이 'text/js-worker'니까요.
      const myVar = "Hello World!";
      // 여기에 여러분의 워커 코드가 쭉 들어갑니다.
    </script>
    <script>
      // 이 스크립트는 정상적인 'text/javascript'이므로 브라우저가 파싱하고 실행합니다.
      function pageLog(sMsg) {
        // DocumentFragment를 사용하면 브라우저의 렌더링/리플로우가 딱 한 번만 일어나서 성능에 좋습니다.
        const frag = document.createDocumentFragment();
        frag.appendChild(document.createTextNode(sMsg));
        frag.appendChild(document.createElement("br"));
        document.querySelector("#logDisplay").appendChild(frag);
      }
    </script>
    <script type="text/js-worker">
      // 이 스크립트 역시 JS 엔진이 무시합니다.
      onmessage = (event) => {
        postMessage(myVar);
      };
      // 나머지 워커 코드...
    </script>
    <script>
      // 이 스크립트는 정상 실행됩니다.

      // 과거에는 Blob Builder라는 게 있었지만, 지금은 그냥 Blob 객체를 씁니다.
      const blob = new Blob(
        // Array.map을 빌려와서 'text/js-worker' 타입의 모든 script 태그 안에 텍스트를 싹 다 긁어모읍니다.
        Array.prototype.map.call(
          document.querySelectorAll("script[type='text/js-worker']"),
          (script) => script.textContent,
        ),
        // 긁어모은 텍스트 덩어리를 뭉쳐서 어엿한 'text/javascript' 타입의 거대한 Blob 데이터로 만듭니다.
        { type: "text/javascript" },
      );

      // 이 가상의 파일(Blob)을 가리키는 고유한 가상 URL(ObjectURL)을 생성해서 Worker의 인자로 넘겨줍니다!
      const worker = new Worker(window.URL.createObjectURL(blob));

      worker.onmessage = (event) => {
        pageLog(`Received: ${event.data}`);
      };
    </script>
  </head>
  <body>
    <div id="logDisplay"></div>
    <script>
      // 워커를 깨워서 일을 시작하게 만듭니다.
      worker.postMessage("");
    </script>
  </body>
</html>

이렇게 하면 임베드된 워커 코드가 새로운 커스텀 속성인 document.worker 아래에 예쁘게 중첩되어 돌아가게 됩니다.

참고로, 어떤 자바스크립트 '함수' 자체를 문자열로 바꿔서 Blob으로 만든 다음, 거기서 객체 URL(Object URL)을 생성해 내는 것도 가능합니다. 정말 기발하죠?

function fn2workerURL(fn) {
  // 함수를 통째로 문자열로 변환(fn.toString())한 다음, 즉시 실행 함수 형태 `(...)()`로 감싸서 Blob으로 만듭니다.
  const blob = new Blob([`(${fn.toString()})()`], { type: "text/javascript" });
  return URL.createObjectURL(blob);
}

추가 예제 (Further examples)

이 섹션에서는 웹 워커를 어떻게 활용할 수 있는지 보여주는 추가적인 예시들을 소개합니다.

백그라운드에서 복잡한 연산 수행하기 (Performing computations in the background)

웹 워커가 존재하는 가장 큰 이유는 메인(UI) 스레드를 멈추게(Blocking) 하지 않으면서 프로세서를 끔찍하게 괴롭히는 무거운 연산들을 백그라운드에서 돌리기 위함입니다. 이 예제에서는 워커를 사용해 피보나치(Fibonacci) 수열을 계산해 보겠습니다.

자바스크립트 코드 (The JavaScript code)

다음 자바스크립트 코드는 "fibonacci.js"라는 파일에 저장되어 있으며, 다음 섹션의 HTML에서 이 파일을 워커로 불러와서 사용하게 됩니다.

// 메인 스레드에서 메시지가 도착하면 이 함수가 실행됩니다.
self.onmessage = (event) => {
  const userNum = Number(event.data);
  // 계산이 끝나면 결과를 다시 메인 스레드로 쏘아 보냅니다!
  self.postMessage(fibonacci(userNum));
};

// 아주 무거운 연산을 하는 함수 (재귀를 쓰지 않고 반복문을 써서 그나마 낫긴 하지만, 숫자가 커지면 엄청 오래 걸립니다!)
function fibonacci(num) {
  let a = 1;
  let b = 0;
  while (num > 0) {
    [a, b] = [a + b, a];
    num--;
  }

  return b;
}

워커는 onmessage 속성에 함수를 지정해 두었다가, 메인 스레드에서 postMessage()를 호출해 메시지를 보낼 때마다 이 함수를 실행하여 메시지를 받아옵니다. 이 핸들러 함수가 열심히 수학 계산을 끝마치고 나면, 그 결과를 다시 메인 스레드로 안전하게 반환합니다.

HTML 코드 (The HTML code)

<form>
  <div>
    <label for="number">
      원하는 피보나치 수열의 0 기반 인덱스 번호를 입력하세요. 예를 들어, 6을 입력하면 인덱스 6번째에 해당하는 피보나치 수인 8이 결과로 나옵니다.
    </label>
    <input type="number" id="number" />
  </div>
  <div>
    <input type="submit" />
  </div>
</form>

<p id="result"></p>

이제 메인 페이지에서 아래의 스크립트 코드를 실행해야 합니다. (HTML 안에 인라인으로 넣거나 외부 파일로 연결해도 됩니다.)

const form = document.querySelector("form");
const input = document.querySelector('input[type="number"]');
const result = document.querySelector("p#result");

// 피보나치 계산 전담 워커를 고용합니다.
const worker = new Worker("fibonacci.js");

// 워커가 계산을 마치고 정답을 보내오면 화면에 찍어줍니다.
worker.onmessage = (event) => {
  result.textContent = event.data;
  console.log(`Got: ${event.data}`);
};

// 워커가 일하다가 뻗어버리면(에러) 콘솔에 빨간 줄을 띄워줍니다.
worker.onerror = (error) => {
  console.log(`Worker error: ${error.message}`);
  throw error;
};

// 폼을 제출(Submit)하면 워커에게 일거리를 던져줍니다.
form.onsubmit = (e) => {
  e.preventDefault();
  worker.postMessage(input.value);
  input.value = "";
};

이 웹 페이지는 결과를 띄워줄 id="result"<p> 요소를 만든 다음, 계산을 대신해 줄 워커를 생성합니다. 워커를 만든 직후에는, 워커가 일을 끝내고 보내온 결과를 <p> 태그에 넣어 화면에 보여주도록 onmessage 핸들러를 세팅하고, 만약 워커에서 에러가 나면 개발자 도구 콘솔에 로그를 찍도록 onerror 핸들러도 꼼꼼히 챙겨줍니다.

그리고 사용자가 버튼을 눌렀을 때 worker.postMessage를 통해 메시지를 쏴주면서 워커에게 큐사인(시작 명령)을 내리는 구조입니다.

이 피보나치 예제를 여기서 라이브로 직접 실행해 보세요!

여러 워커에게 작업 분산시키기 (Dividing tasks among multiple workers)

요즘 우리가 쓰는 컴퓨터나 스마트폰은 대부분 멀티코어(Multi-core) CPU를 탑재하고 있습니다. 그래서 연산이 아주 복잡한 덩어리 작업을 맞닥뜨렸을 때, 여러 개의 워커를 생성해서 작업을 잘게 쪼개어 나눠준 다음, 여러 개의 CPU 코어가 동시에 병렬로 작업을 처리하게 만들면 엄청난 성능 향상을 맛볼 수 있습니다!


또 다른 종류의 워커들 (Other types of workers)

우리가 깊게 알아본 전용 워커(Dedicated worker)와 공유 워커(Shared worker) 외에도, 웹 생태계에는 특수한 목적을 가진 다른 워커들이 존재합니다.

  • 서비스 워커 (ServiceWorkers): 웹 애플리케이션과 브라우저, 그리고 네트워크망 사이에 떡하니 버티고 서서 프록시 서버(Proxy server) 역할을 하는 녀석입니다. 이 친구의 주특기는 네트워크 요청을 가로채서 오프라인 상태일 때도 캐시 된 화면을 보여주거나, 네트워크 연결 상태에 따라 똑똑하게 대응하는 것입니다. 또한 푸시 알림(Push notifications)을 받거나 백그라운드 동기화(Background sync) API에 접근하는 일도 이 녀석의 몫입니다.
  • 오디오 워클릿 (Audio Worklet): 오디오 데이터를 자바스크립트로 직접 아주 세밀하게 조작해야 할 때, 메인 스레드에 부담을 주지 않고 별도의 가벼운 워커(Worklet) 컨텍스트에서 처리할 수 있는 능력을 제공합니다.

워커 스레드 디버깅하기 (Debugging worker threads)

크롬이나 파이어폭스 같은 대부분의 최신 브라우저들은 여러분이 평소에 메인 스레드 자바스크립트를 디버깅할 때 쓰던 자바스크립트 디버거를 완벽하게 똑같은 방식으로 워커 스레드에도 쓸 수 있게 해줍니다! 개발자 도구를 열어보시면 메인 스레드용 소스 파일뿐만 아니라 현재 활발하게 돌아가고 있는 워커 스레드의 소스 파일들까지 싹 다 리스트업 되어 있는 걸 보실 수 있어요. 이 파일들을 열어서 브레이크포인트(중단점)를 찍거나 로그를 남기는 것도 당연히 가능하답니다.

웹 워커를 디버깅하는 방법을 더 자세히 알고 싶다면, 각 브라우저의 공식 문서를 참고해 보세요.

웹 워커 전용 개발자 도구 화면을 열고 싶으시다면, 브라우저 주소창에 아래 URL을 입력해 보세요.

  • Edge: edge://inspect/
  • Chrome: chrome://inspect/
  • Firefox: about:debugging#/runtime/this-firefox

이 페이지에 들어가시면 현재 돌아가고 있는 모든 서비스 워커들의 현황판을 볼 수 있습니다. 거기서 원하는 녀석의 URL을 찾은 다음 inspect(검사) 버튼을 누르면, 해당 워커만을 위한 독립적인 콘솔과 디버거 창이 짠! 하고 열립니다.


워커에서 사용할 수 있는 함수와 인터페이스 (Functions and interfaces available in workers)

웹 워커 내부라는 다소 고립된 환경 안에서도, 여러분은 아주 다양한 표준 자바스크립트 기능들을 자유롭게 구사할 수 있습니다.

워커에서 유일하게 절대 해서는 안 되고 할 수도 없는 일은 바로 부모 페이지(화면)를 직접 건드리는 것입니다. DOM 트리(HTML 요소들)를 직접 조작하거나, 부모 페이지에 선언된 객체들을 마음대로 가져다 쓰는 행위는 금지되어 있습니다. 무언가 화면을 바꾸고 싶다면, 계산된 결과를 담아 DedicatedWorkerGlobalScope.postMessage()를 통해 메인 스크립트로 얌전히 메시지를 보낸 다음, 메인 스레드의 이벤트 핸들러가 그 결과를 받아서 대신 화면을 변경해주도록 우회적인 방법을 사용해야만 합니다.

참고: 내가 쓰려는 특정 메서드나 인터페이스가 워커 안에서 작동하는지 헷갈린다면, 워커 플레이그라운드 (Worker Playground)에서 직접 코드를 쳐보고 테스트해 볼 수 있습니다.

참고: 워커에서 허락된 모든 함수와 인터페이스의 전체 목록이 궁금하시다면 워커에서 사용 가능한 함수와 클래스들 (Functions and interfaces available to workers) 문서를 확인해 주세요!

안녕하세요! 프론트엔드 개발의 꽃 중 하나인 비동기 처리와 성능 최적화를 공부하고 계시는군요. 이번에 가져오신 문서는 바로 웹 워커(Web Workers)에 관한 내용입니다.

문서를 본격적으로 번역하기 전에 강사로서 조금 부연 설명을 드릴게요. 웹 워커는 브라우저의 메인 스레드(우리가 흔히 아는 UI를 그리고 클릭 이벤트를 받는 곳)와는 완전히 분리된 '백그라운드 스레드'에서 자바스크립트 코드를 실행할 수 있게 해주는 기술입니다.

쉽게 말해, 메인 스레드가 화면을 부드럽게 그리는 데 집중할 수 있도록 무겁고 복잡한 연산을 대신 처리해 주는 '듬직한 조수'라고 생각하시면 됩니다. 다만 메인 스레드와 완전히 분리된 공간에 있기 때문에, 우리가 평소에 자주 쓰던 windowdocument 객체(DOM 조작)에는 접근할 수 없어요. 대신 어떤 기능들을 사용할 수 있는지, 이 공식 문서가 그 목록을 쫙 정리해 주고 있습니다.

딱딱하지 않게, 실무 팁과 함께 꼼꼼히 번역해 드릴 테니 잘 따라와 주세요!


Web Worker에서 사용할 수 있는 함수와 클래스 (Functions and classes available to Web Workers)

웹 워커 내부에서는 (String, Array, Object, JSON 등과 같은) 표준 자바스크립트(JavaScript) 함수 세트 외에도, 아주 다양한 함수들과 웹 API들을 사용할 수 있습니다. 이 문서는 워커 환경에서 사용할 수 있는 기능들의 목록을 제공합니다.


이 문서에서는 (In this article)


워커에서 사용 가능한 함수들 (Functions available in workers)

워커에서는 다음과 같은 함수들을 사용할 수 있습니다.

그리고 다음 함수는 오직 워커 내부에서만 사용할 수 있는 특수한 함수입니다.

  • WorkerGlobalScope.importScripts()

    💡 강사의 핵심 팁: > 워커는 별도의 스레드이기 때문에 HTML의 <script> 태그로 외부 라이브러리를 불러올 수 없습니다. 대신 바로 이 importScripts() 함수를 사용해서 워커 내부로 필요한 외부 자바스크립트 파일들을 쏙쏙 불러와서 사용할 수 있답니다!


워커에서 사용 가능한 웹 API들 (Web APIs available in workers)

참고:
만약 특정 버전의 플랫폼(브라우저)에서 아래 나열된 API를 지원한다면, 일반적으로 웹 워커 환경에서도 해당 API를 사용할 수 있다고 가정하셔도 좋습니다. 특정 객체나 함수가 워커에서 제대로 지원되는지 직접 테스트해 보고 싶다면 Worker Playground (워커 플레이그라운드)를 활용해 보세요.

워커에서는 다음과 같이 정말 다양한 웹 API들을 사용할 수 있습니다. (알파벳 순서대로 나열되어 있어요!)

💡 실무 적용 팁:
목록을 보시면 Fetch APIWebSockets API, IndexedDB API가 포함되어 있는 게 보이실 거예요. 이게 무슨 의미일까요?
만약 여러분이 복잡한 데이터 구조를 다루거나, 그래프 탐색(Graph Coloring 등)과 같은 무거운 알고리즘 연산을 수행해야 한다고 가정해 봅시다. 메인 스레드에서 이런 무거운 작업을 돌리면 연산이 끝날 때까지 브라우저 화면이 멈춰버리는(프리징) 현상이 발생합니다.
하지만 웹 워커 내부에서 Fetch로 대량의 데이터를 불러오고, 무거운 알고리즘 계산을 마친 뒤, 그 결과물만 메인 스레드로 쏙 보내주면 사용자는 아무런 끊김 없이 부드러운 UI를 경험할 수 있습니다! 프론트엔드 성능 최적화의 핵심 무기 중 하나죠.

웹 워커는 자신의 내부에서 또 다른 워커를 파생(spawn)시킬 수도 있습니다. 따라서 다음과 같은 API들도 당연히 사용할 수 있습니다.


함께 보기 (See also)

안녕하세요! 이번에 가져오신 문서는 프론트엔드 개발, 특히 자바스크립트의 핵심 동작과 아주 밀접하게 맞닿아 있는 '구조화된 클론 알고리즘(The structured clone algorithm)'입니다.

리액트(React)나 Zustand 같은 상태 관리 도구를 사용하다 보면 '불변성(Immutability)'을 지키기 위해 객체를 복사해야 하는 일이 정말 많죠? 또한, 코딩 테스트를 준비하시면서 그래프 탐색이나 그리디 알고리즘, 특히 2차원 배열의 상태를 변경해가며 답을 찾는 문제(예: 그래프 색칠하기 등)를 풀 때 원본 배열을 망가뜨리지 않기 위해 '깊은 복사(Deep Copy)'를 해야 했던 경험이 있으실 겁니다.

이 문서가 바로 그 '깊은 복사'를 브라우저가 내부적으로 어떻게 처리하는지, 그리고 어떤 타입들이 복사 가능한지를 명확하게 알려주는 아주 중요한 자료입니다. 실무 팁과 함께 꼼꼼히 번역해 드릴 테니 잘 따라와 주세요!


구조화된 클론 알고리즘 (The structured clone algorithm)

구조화된 클론 알고리즘(structured clone algorithm)은 복잡한 자바스크립트 객체를 복사(깊은 복사)하는 역할을 합니다.
이 알고리즘은 structuredClone() 함수를 호출할 때나, postMessage()를 통해 워커(Workers) 간에 데이터를 전송할 때, IndexedDB에 객체를 저장할 때, 또는 다른 API들에서 객체를 복사해야 할 때 브라우저 내부적으로 사용됩니다.

이 알고리즘은 입력된 객체를 재귀적으로 파고들면서 복제합니다. 이때 무한하게 반복되는 순환 참조(cycle)에 빠지지 않도록, 이전에 방문했던 참조들의 맵(Map)을 유지하면서 안전하게 복사본을 만들어냅니다.

💡 강사의 핵심 보충 설명:
과거에는 깊은 복사를 하기 위해 JSON.parse(JSON.stringify(object)) 방식을 꼼수처럼 많이 썼습니다. 하지만 이 방식은 순환 참조가 있는 객체(A가 B를 참조하고, B가 다시 A를 참조하는 경우)를 넣으면 에러를 뿜으며 프로그램이 뻗어버리죠.
반면, 최신 브라우저에 도입된 structuredClone()은 내부적으로 이 알고리즘을 사용하기 때문에 순환 참조까지 완벽하게 파악하고 끊어내어 안전하게 복사해 줍니다!


이 문서에서는 (In this article)


구조화된 클론으로 복사할 수 없는 것들 (Things that don't work with structured clone)

구조화된 클론 알고리즘이 만능은 아닙니다. 다음과 같은 경우에는 복사가 불가능하거나 일부 데이터가 유실될 수 있습니다.

  • Function (함수) 객체는 이 알고리즘으로 복제할 수 없습니다. 함수를 복사하려고 시도하면 DataCloneError 예외가 발생하며 에러를 던집니다.
  • DOM 노드(DOM nodes)를 복사하려고 할 때도 마찬가지로 DataCloneError 예외가 발생합니다.
  • 특정 객체 속성(properties)들은 보존되지 않고 날아갑니다:
    • RegExp (정규표현식) 객체의 lastIndex 속성은 보존되지 않습니다.
    • 프로퍼티 디스크립터(Property descriptors), setter, getter 그리고 이와 유사한 메타데이터 성격의 기능들은 복제되지 않습니다.
      예를 들어, 원본 객체가 프로퍼티 디스크립터를 통해 읽기 전용(readonly)으로 표시되어 있더라도, 복제된 객체에서는 기본값인 읽기/쓰기(read/write)가 가능한 상태가 됩니다.
    • 프로토타입 체인(prototype chain)은 탐색되지 않으며 복제되지도 않습니다.
    • 클래스의 비공개(private) 요소들은 복제되지 않습니다. (다만, 내장 타입의 내부 필드들은 복사될 수 있습니다.)

💡 강사의 실무 팁: > "함수(Function)가 복사되지 않는다"는 점을 꼭 명심하셔야 해요! 상태 관리 도구인 Zustand의 스토어나 클래스 인스턴스처럼 내부에 메서드(함수)를 품고 있는 객체를 structuredClone()으로 복사하면 함수는 쏙 빠지거나 에러가 납니다. 오로지 순수한 데이터(Data)만을 안전하게 복사하는 용도로 사용하셔야 합니다.


지원되는 타입 (Supported types)

JavaScript 타입 (JavaScript types)

💡 강사의 보충 설명:
기존의 JSON.parse(JSON.stringify()) 꼼수는 Date, Map, Set을 제대로 복사하지 못하고 단순히 문자열이나 빈 객체로 변환해버리는 치명적인 단점이 있었습니다. 하지만 구조화된 클론 알고리즘은 보시다시피 이런 모던 자바스크립트의 유용한 타입들까지 완벽하게 복사해 줍니다!

에러 타입 (Error types)

Error 타입의 경우, 에러의 이름(name)이 반드시 다음 중 하나여야 합니다: Error, EvalError, RangeError, ReferenceError, SyntaxError, TypeError, URIError. (만약 다른 이름이라면 그냥 "Error"로 설정되어 복사됩니다.)

브라우저는 에러의 namemessage 속성을 반드시 직렬화(serialize, 복사를 위해 데이터를 변환하는 과정)해야 하며, 부가적으로 stack(스택 추적), cause(원인) 등 에러와 관련된 기타 유용한 속성들도 함께 직렬화할 것으로 기대됩니다.

AggregateError 지원 기능은 whatwg/html#5749 명세에 추가될 예정입니다 (그리고 이미 일부 최신 브라우저에서는 지원하고 있습니다).

Web/API 타입 (Web/API types)

순수 자바스크립트 타입뿐만 아니라, 브라우저 환경에서 제공하는 수많은 API 관련 객체들도 복사할 수 있습니다. (예를 들어 이미지를 자르거나 파일 데이터를 다룰 때 유용하죠.)

참고:
직렬화(복사)가 가능한 객체들은 Web IDL 파일들(명세서)에서 [Serializable] 이라는 속성으로 별도 표시되어 있습니다.


함께 보기 (See also)

관련 기능들을 코드로 다루실 때 유용한 문서들입니다.

안녕하세요! 이전 시간에 배운 '구조화된 클론(Structured Clone)' 알고리즘에 이어서, 이번에는 웹 워커 성능 최적화의 핵심인 '전송 가능한 객체(Transferable objects)' 문서를 가져오셨네요.

방금 전 문서에서 객체를 워커로 넘길 때 복사(깊은 복사)를 한다고 배웠죠? 그런데 만약 수십 메가바이트짜리 영상 데이터나 엄청나게 큰 3D 모델링 버퍼(ArrayBuffer)를 워커로 보낸다면 어떻게 될까요? 복사하는 데에만 엄청난 시간과 메모리가 낭비될 겁니다.

바로 이럴 때 사용하는 것이 '전송 가능한 객체'입니다. 복사(Copy)하는 대신 소유권을 완전히 넘겨버리는(Transfer) 아주 멋지고 효율적인 개념이죠! 강사의 팁을 곁들여서 이해하기 쉽게 구어체로 번역해 드릴 테니 잘 읽어보세요.


전송 가능한 객체 (Transferable objects)

전송 가능한 객체(Transferable objects)는 한 실행 환경(컨텍스트)에서 다른 환경으로 전송(transfer)될 수 있는 리소스를 소유한 객체를 말합니다. 이 방식은 해당 리소스가 한 번에 오직 하나의 환경에서만 사용되도록 철저하게 보장해 줍니다.
객체가 한 번 전송되고 나면, 원래 환경에 남아있던 원본 객체는 더 이상 사용할 수 없게 됩니다. 원본 객체는 전송된 리소스를 더 이상 가리키지 않으며, 이 객체를 읽거나 쓰려고 시도하면 곧바로 예외(에러)가 발생하게 되죠.

💡 강사의 핵심 보충 설명:
일상생활로 비유해 볼까요? 일반적인 복사(Clone)가 책을 '복사기'로 복사해서 사본을 친구에게 주는 것이라면, 전송(Transfer)은 내 손에 있던 원본 책을 친구에게 '직접 건네주는' 것입니다. 책을 건네줬으니 내 손에는 더 이상 책이 없겠죠?
자바스크립트에서도 마찬가지로 메모리를 복사하지 않고 소유권만 휙 넘기기 때문에(Zero-copy), 속도가 번개처럼 빠르고 메모리도 전혀 낭비되지 않습니다!

전송 가능한 객체는 한 번에 단 하나의 자바스크립트 스레드에만 노출되어야 안전한 리소스들을 공유할 때 아주 흔하게 사용됩니다.
대표적인 예로, 메모리 블록을 소유하고 있는 전송 가능한 객체인 ArrayBuffer가 있습니다.
이러한 버퍼가 스레드 간에 전송될 때, 관련된 메모리 리소스는 원본 버퍼에서 뚝 떨어져 나와(분리되어) 새로운 스레드에 생성된 버퍼 객체에 찰칵 붙게 됩니다.
원본 스레드에 남아있는 버퍼 객체는 이제 메모리 리소스를 소유하고 있지 않기 때문에 더 이상 사용할 수 없는 껍데기가 되는 것이죠.

이 '전송' 메커니즘은 structuredClone() 함수를 사용해 객체의 깊은 복사본을 만들 때도 사용할 수 있습니다.
복제 작업 시 전송을 지시하면, 전송된 리소스들은 복제된 객체로 단순히 복사되는 것이 아니라 아예 이동(move)하게 됩니다.

postMessage()structuredClone() 모두에서, 전송할 리소스들은 반드시 데이터 객체에 포함(첨부)되어 있어야 합니다. 그렇지 않으면 받는 쪽에서 해당 리소스를 사용할 수 없게 됩니다. 왜냐하면 '전송 목록 배열(transferable array)'은 특정 리소스를 어떻게 보내야 하는지 방식만 지시할 뿐, 실제로 그 데이터를 보내는 역할을 하지는 않기 때문입니다 (물론 원본 데이터가 분리되는 작업은 무조건 일어납니다).

객체의 리소스를 전송하는 구체적인 메커니즘은 객체의 종류마다 다릅니다.
예를 들어, ArrayBuffer가 스레드 간에 전송될 때, 그것이 가리키는 메모리 리소스는 빠르고 효율적인 제로 카피(zero-copy) 연산을 통해 컨텍스트 사이를 말 그대로 문자 그대로(literally) 이동합니다.
반면 다른 객체들은 관련된 리소스를 복사한 다음 이전 컨텍스트에서 삭제하는 방식으로 전송이 이루어질 수도 있습니다.

모든 객체가 다 전송 가능한 것은 아닙니다.
전송 가능한 객체들의 목록은 아래에 제공되어 있습니다.


스레드 간에 객체 전송하기 (Transferring objects between threads)

아래 코드는 메인 스레드에서 웹 워커 스레드(web worker thread)로 메시지를 보낼 때 전송이 어떻게 이루어지는지 보여줍니다.
여기서 Uint8Array 자체는 워커로 복사(복제)되지만, 그 기반이 되는 버퍼(buffer)는 전송됩니다.
전송이 완료된 후, 메인 스레드에서 uInt8Array를 읽거나 쓰려고 시도하면 에러가 발생합니다. 하지만 byteLength 속성을 확인해 보면 크기가 0이 된 것을 통해 정상적으로 비워졌음을 확인할 수 있습니다.

// Create an 8MB "file" and fill it. 8MB = 1024 * 1024 * 8 B
const uInt8Array = new Uint8Array(1024 * 1024 * 8).map((v, i) => i);
console.log(uInt8Array.byteLength); // 8388608

// Transfer the underlying buffer to a worker
worker.postMessage(uInt8Array, [uInt8Array.buffer]);
console.log(uInt8Array.byteLength); // 0

참고:
Int32ArrayUint8Array 같은 형식화된 배열(Typed arrays)직렬화(serializable)가 가능하지만, 그 자체로 전송 가능한 객체(transferable)는 아닙니다.
하지만 이 배열들이 내부적으로 깔고 있는 기반 버퍼는 전송 가능한 객체인 ArrayBuffer입니다.
따라서 우리는 두 번째 인자(전송 배열)에 uInt8Array 자체를 넣을 수는 없고, uInt8Array.buffer를 넣어서 버퍼만 전송해야 합니다.


복제 작업 중에 전송하기 (Transferring during a cloning operation)

아래 코드는 structuredClone() 작업을 수행할 때 기반 버퍼가 원본 객체에서 복제본으로 어떻게 복사(사실상 이동)되는지를 보여줍니다.

const original = new Uint8Array(1024);
const clone = structuredClone(original);
console.log(original.byteLength); // 1024
console.log(clone.byteLength); // 1024

original[0] = 1;
console.log(clone[0]); // 0

// Transferring the Uint8Array would throw an exception as it is not a transferable object
// const transferred = structuredClone(original, {transfer: [original]});

// We can transfer Uint8Array.buffer.
const transferred = structuredClone(original, { transfer: [original.buffer] });
console.log(transferred.byteLength); // 1024
console.log(transferred[0]); // 1

// After transferring Uint8Array.buffer cannot be used.
console.log(original.byteLength); // 0

💡 강사의 실무 팁:
위 코드를 보면 일반적인 structuredClone은 완전히 독립적인 메모리를 복사해냅니다(clone의 크기가 1024로 유지됨). 하지만 옵션으로 { transfer: [original.buffer] }를 줘버리면, 원본 original.buffer의 메모리가 transferred 쪽으로 소유권이 넘어가면서 원본인 originalbyteLength가 0이 되어 빈 껍데기가 된 것을 볼 수 있죠!


지원되는 객체 (Supported objects)

전송될 수 있는 인터페이스들은 보통 각 공식 문서의 도입부에 이 사실을 명시하고 있습니다.

다양한 명세서에서 전송 가능(transferred)하다고 나타낸 항목들 중 일부를 아래에 정리해 두었습니다 (이 목록에 없는 객체가 더 있을 수도 있습니다!):

참고:
전송 가능한 객체들은 Web IDL 파일들(명세서)에서 [Transferable] 이라는 속성으로 표시되어 있습니다.
브라우저 지원 여부는 각 객체의 호환성 정보 표에서 transferable 하위 기능(subfeature) 항목을 통해 확인할 수 있습니다 (예시는 RTCDataChannel 문서 참고).


함께 보기 (See also)

profile
프론트에_가까운_풀스택_개발자

0개의 댓글