구조적 복제의 비용
Web Worker로 데이터를 전달할 때 기본 동작은 구조적 복제 (Structured Clone)입니다. 이는 JSON.parse(JSON.stringify(obj))와 비슷하지만, Map, Set, ArrayBuffer, Date, RegExp 등 더 많은 타입을 지원합니다. 수신 측에서 원본과 완전히 독립적인 깊은 복사본을 받습니다.
대부분의 경우 이것으로 충분하지만, 대용량 이진 데이터에서는 문제가 됩니다.
const bigBuffer = new ArrayBuffer(256 * 1024 * 1024); // 256MB
// 기본 방식: 256MB를 복사합니다, 느리고 메모리 2배 사용
worker.postMessage({ buf: bigBuffer });256MB 버퍼를 복사하면 수십에서 수백 ms가 걸릴 수 있습니다. 이미지 처리, 오디오 데이터, WebAssembly 메모리처럼 대용량 버퍼를 자주 주고받는 경우 병목이 됩니다.
Transferable: 소유권 이전
Transferable은 복사 대신 소유권을 이전 합니다. 메모리를 복사하지 않고 해당 메모리를 가리키는 핸들을 다른 스레드에 넘깁니다.
const buf = new ArrayBuffer(256 * 1024 * 1024);
// 전송 방식: 소유권 이전, 거의 즉시, 0바이트 복사
worker.postMessage({ buf }, [buf]);
// ^^^^^ Transferable 목록postMessage의 두 번째 인자는 transfer 배열 입니다. 여기에 포함된 객체는 복사되지 않고 소유권이 이전됩니다.
아래 시각화로 두 방식의 차이를 확인하세요.
전송 후 원본은 분리됩니다
소유권이 이전된 이후, 원본 객체는 분리 (detached) 상태가 됩니다.
const buf = new ArrayBuffer(1024);
const view = new Uint8Array(buf);
worker.postMessage({ buf }, [buf]);
console.log(buf.byteLength); // 0, 분리됨
console.log(view[0]);
// TypeError: Cannot perform Uint8Array.prototype.get on a detached ArrayBuffer분리된 버퍼의 byteLength는 0이 되고, 해당 버퍼를 참조하는 TypedArray 뷰의 모든 읽기/쓰기가 TypeError를 발생시킵니다. 소유권은 워커에게만 있습니다.
Transferable 객체 종류
MDN Web API 문서 기준, Transferable 인터페이스를 구현하는 주요 타입입니다.
| 타입 | 용도 |
|---|---|
ArrayBuffer | 이진 데이터 버퍼 |
MessagePort | MessageChannel의 통신 채널 |
ImageBitmap | 디코딩된 이미지 데이터 |
OffscreenCanvas | 워커에서 Canvas 렌더링 |
ReadableStream / WritableStream | 스트리밍 데이터 |
AudioData / VideoFrame | WebCodecs API |
OffscreenCanvas: 워커에서 Canvas 렌더링
OffscreenCanvas를 Transferable로 전달하면 워커에서 직접 Canvas에 그릴 수 있습니다.
// main.js
const canvas = document.getElementById('myCanvas');
const offscreen = canvas.transferControlToOffscreen();
// OffscreenCanvas의 제어권을 워커에 이전
worker.postMessage({ canvas: offscreen }, [offscreen]);
// 이후 canvas를 main에서 제어할 수 없음// worker.js
self.onmessage = (e) => {
const { canvas } = e.data;
const ctx = canvas.getContext('2d');
// 워커에서 직접 렌더링
requestAnimationFrame(function draw() {
ctx.clearRect(0, 0, canvas.width, canvas.height);
ctx.fillRect(Math.random() * canvas.width, 0, 10, 10);
requestAnimationFrame(draw);
});
};Three.js와 PixiJS는 OffscreenCanvas를 통한 오프-스레드 렌더링 모드를 지원합니다. 메인 스레드는 렌더링에서 완전히 해방됩니다.
MessageChannel로 워커 간 직접 통신
MessagePort를 Transferable로 전달하면 워커 간 직접 통신이 가능합니다.
// main.js
const { port1, port2 } = new MessageChannel();
// 각 워커에 포트를 이전
worker1.postMessage({ port: port1 }, [port1]);
worker2.postMessage({ port: port2 }, [port2]);
// 이후 worker1과 worker2는 메인 스레드를 거치지 않고 직접 통신// worker1.js
let directPort;
self.onmessage = (e) => {
directPort = e.data.port;
directPort.postMessage('worker1에서 직접 전송');
};
// worker2.js
self.onmessage = (e) => {
const port = e.data.port;
port.onmessage = (e) => console.log('받음:', e.data);
};TypedArray와 전송
TypedArray (Int32Array, Float64Array 등)는 내부적으로 ArrayBuffer를 가리키는 뷰(view)입니다. TypedArray 자체는 Transferable이 아니지만, 내부 .buffer를 전송할 수 있습니다.
const floatArray = new Float64Array(1024);
floatArray[0] = 3.14;
// Float64Array가 아닌 내부 버퍼를 전송
worker.postMessage({ arr: floatArray }, [floatArray.buffer]);
console.log(floatArray.buffer.byteLength); // 0, detached
console.log(floatArray.length); // 0, 뷰도 비어짐전송 후에는 원본 TypedArray의 length도 0이 됩니다. 뷰가 가리키던 버퍼가 분리되었기 때문입니다.
실무 적용 사례
WebCodecs API : 브라우저에서 비디오 프레임을 인코딩/디코딩할 때 VideoFrame과 AudioData를 Transferable로 Worker에 전달합니다. 고성능 영상 처리를 메인 스레드 없이 수행합니다.
WebAssembly : WASM 모듈이 사용하는 ArrayBuffer 메모리를 Transferable로 이전할 수 있습니다. 단, 이전 후에는 해당 WASM 인스턴스를 그쪽 스레드에서만 사용해야 합니다.
Transferable의 한계
Transferable은 "소유권 이전"이라 한 스레드만 접근 가능 합니다. 두 스레드가 동시에 같은 데이터를 읽고 쓰려면 다른 메커니즘이 필요합니다. 다음 글에서는 진정한 공유 메모리인 SharedArrayBuffer와 Atomics를 살펴봅니다.