Ray Book
웹 워커와 병렬 처리

Transferable Objects: 복사 없는 데이터 전달

postMessage의 복사 비용을 제거하는 방법, ArrayBuffer 소유권 이전과 Transferable 객체를 시각화합니다

javascriptweb-workertransferablearraybufferperformance

구조적 복제의 비용

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 배열 입니다. 여기에 포함된 객체는 복사되지 않고 소유권이 이전됩니다.

아래 시각화로 두 방식의 차이를 확인하세요.

코드
1const buf = new ArrayBuffer(256 * 1024 * 1024);
2 
3// 복사 방식 (transfer 배열 없음)
4worker.postMessage({ buf });
5console.log(buf.byteLength); // 268435456
6 
7// 전송 방식 (transfer 배열 지정)
8worker.postMessage({ buf }, [buf]);
9console.log(buf.byteLength); // 0 (detached!)
메모리
메인 스레드 buf256 MB
워커 스레드 buf256 MB
256MB ArrayBuffer를 생성합니다. 현재는 메인 스레드만 이 버퍼를 소유합니다. 이 버퍼를 워커에 전달하는 두 가지 방법을 비교해봅니다.

전송 후 원본은 분리됩니다

소유권이 이전된 이후, 원본 객체는 분리 (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이진 데이터 버퍼
MessagePortMessageChannel의 통신 채널
ImageBitmap디코딩된 이미지 데이터
OffscreenCanvas워커에서 Canvas 렌더링
ReadableStream / WritableStream스트리밍 데이터
AudioData / VideoFrameWebCodecs 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 : 브라우저에서 비디오 프레임을 인코딩/디코딩할 때 VideoFrameAudioData를 Transferable로 Worker에 전달합니다. 고성능 영상 처리를 메인 스레드 없이 수행합니다.

WebAssembly : WASM 모듈이 사용하는 ArrayBuffer 메모리를 Transferable로 이전할 수 있습니다. 단, 이전 후에는 해당 WASM 인스턴스를 그쪽 스레드에서만 사용해야 합니다.

Transferable의 한계

Transferable은 "소유권 이전"이라 한 스레드만 접근 가능 합니다. 두 스레드가 동시에 같은 데이터를 읽고 쓰려면 다른 메커니즘이 필요합니다. 다음 글에서는 진정한 공유 메모리인 SharedArrayBufferAtomics를 살펴봅니다.