Node.js Backend Development Bootcamp · 강의

워커 스레드 생성 및 메시지 전달

워커를 만들고 postMessage로 데이터를 교환하며 workerData와 MessageChannel로 구조화된 통신을 구현합니다.

레슨 2/413개 단계

워커 스레드 생성 및 메시지 전달은(는) CoddyKit의 무료 Node.js Backend Development Bootcamp 강의입니다. 이것은 4개 중 2번째 강의입니다. 아래에서 전체 강의를 무료로 읽을 수 있으며, 내장 코드 에디터와 24/7 AI 튜터와 함께 브라우저에서 직접 실습할 수 있습니다. 이 강의는 Node.js Backend Development Bootcamp 학습 경로의 일부이며, 진행 상황이 웹과 CoddyKit 앱에 동기화됩니다. Node.js Backend Development Bootcamp 강의에는 총 4개의 강의가 포함되어 있습니다.

이 강의의 일부는 아직 번역되지 않았으며 영어로 표시됩니다.

Why Worker Threads Exist

Node.js runs your JavaScript on a single thread. That is great for I/O-bound work (HTTP, DB, files) because the event loop never blocks. But a CPU-bound task — image resizing, hashing, parsing huge payloads, crypto — runs synchronously and freezes the entire server until it finishes.

  • I/O-bound → stay on the event loop, use async APIs.
  • CPU-bound → offload to a Worker thread so the main thread keeps serving requests.

The worker_threads module lets you run JavaScript in parallel on separate OS threads, each with its own V8 isolate and event loop.

Spawning Your First Worker

You create a thread with new Worker(filename). The file runs on a fresh thread. Use isMainThread to make one file behave differently depending on where it runs.

Below, the main thread spawns a worker that points back to the same file. The worker branch does the work and exits.

const { Worker, isMainThread } = require('node:worker_threads');

if (isMainThread) {
  console.log('main: spawning worker');
  const worker = new Worker(__filename);
  worker.on('exit', (code) => console.log('worker exited with', code));
} else {
  // This branch runs inside the worker thread
  let sum = 0;
  for (let i = 0; i < 1e7; i++) sum += i;
  console.log('worker computed sum =', sum);
}

Sending Data In with workerData

To hand input to a worker at creation time, pass workerData in the options object. Inside the worker, read it from the worker_threads module.

  • workerData is cloned (structured clone), not shared — mutating it in the worker does not affect the main thread.
  • Use it for the initial job parameters: a file path, a number range, config.
const { Worker, isMainThread, workerData } = require('node:worker_threads');

if (isMainThread) {
  new Worker(__filename, { workerData: { start: 1, end: 5 } });
} else {
  const { start, end } = workerData;
  let product = 1;
  for (let i = start; i <= end; i++) product *= i;
  console.log('factorial-ish product =', product);
}

Getting Results Back with postMessage

workerData is one-way and one-time. For results, use the message channel that every worker has built in:

  • Inside the worker: parentPort.postMessage(value) sends data to the parent.
  • On the main thread: worker.on('message', cb) receives it.

Messages are asynchronous and structured-cloned, so the main thread stays responsive while the worker computes.

const { Worker, isMainThread, parentPort, workerData } = require('node:worker_threads');

if (isMainThread) {
  const worker = new Worker(__filename, { workerData: 40 });
  worker.on('message', (result) => {
    console.log('main received:', result);
  });
} else {
  function fib(n) { return n < 2 ? n : fib(n - 1) + fib(n - 2); }
  parentPort.postMessage(fib(workerData));
}

Two-Way Messaging

The channel works both directions. The parent can worker.postMessage() and the worker listens with parentPort.on('message', ...). This turns a worker into a long-lived service that processes many jobs instead of one.

Notice the worker stays alive listening for messages; you must explicitly tell it to stop (here, a null sentinel triggers process.exit).

const { Worker, isMainThread, parentPort } = require('node:worker_threads');

if (isMainThread) {
  const worker = new Worker(__filename);
  worker.on('message', (r) => console.log('squared ->', r));
  [2, 3, 4].forEach((n) => worker.postMessage(n));
  worker.postMessage(null); // sentinel to stop
} else {
  parentPort.on('message', (n) => {
    if (n === null) { process.exit(0); }
    parentPort.postMessage(n * n);
  });
}

Promisifying a Single Job

In backend code you usually want a clean async function: call it, await a result. Wrap the worker lifecycle in a Promise, resolving on message and rejecting on error or a non-zero exit.

This pattern is the foundation of any worker-pool: one promise per task.

const { Worker, isMainThread, parentPort, workerData } = require('node:worker_threads');

function runJob(payload) {
  return new Promise((resolve, reject) => {
    const worker = new Worker(__filename, { workerData: payload });
    worker.on('message', resolve);
    worker.on('error', reject);
    worker.on('exit', (code) => {
      if (code !== 0) reject(new Error('exit code ' + code));
    });
  });
}

if (isMainThread) {
  runJob([5, 10, 15]).then((sum) => console.log('total =', sum));
} else {
  const total = workerData.reduce((a, b) => a + b, 0);
  parentPort.postMessage(total);
}

Structured Clone: What You Can Send

Messages are copied using the structured clone algorithm, not JSON.stringify. That means you can send more than plain JSON.

  • Supported: objects, arrays, Map, Set, Date, RegExp, ArrayBuffer, typed arrays, BigInt.
  • Not supported: functions, class instances with methods, DOM-like objects, anything with closures — these throw a DataCloneError.

Cloning large objects costs CPU and memory. For big binary buffers, prefer transferring instead of copying (next scene).

Transferring Buffers Instead of Copying

For large binary data, copying is wasteful. Pass a transfer list as the second argument to postMessage. Ownership of the ArrayBuffer moves to the other thread — zero-copy — and the buffer becomes unusable on the sender (its byteLength becomes 0).

Use this for image bytes, file chunks, or any megabyte-scale payload to avoid duplicating memory.

const { Worker, isMainThread, parentPort, workerData } = require('node:worker_threads');

if (isMainThread) {
  const buf = new Uint8Array([1, 2, 3, 4]).buffer;
  const worker = new Worker(__filename, { workerData: buf }, { transferList: [buf] });
  worker.on('message', (sum) => console.log('byte sum =', sum));
} else {
  const bytes = new Uint8Array(workerData);
  let sum = 0;
  for (const b of bytes) sum += b;
  parentPort.postMessage(sum);
}

MessageChannel for Side Channels

Beyond the built-in parentPort, you can create your own channel with new MessageChannel(). It gives two linked ports, port1 and port2. Keep one, transfer the other into a worker, and now you have a dedicated pipe — useful for separating control messages from data, or for worker-to-worker communication.

A MessagePort is itself transferable, so you send it inside workerData or a message with a transfer list.

const { Worker, isMainThread, MessageChannel, parentPort, workerData } =
  require('node:worker_threads');

if (isMainThread) {
  const { port1, port2 } = new MessageChannel();
  new Worker(__filename, { workerData: { port: port2 }, transferList: [port2] });
  port1.on('message', (msg) => {
    console.log('main got on side channel:', msg);
    port1.close();
  });
  port1.postMessage('ping');
} else {
  const { port } = workerData;
  port.on('message', (msg) => {
    port.postMessage('pong (you said: ' + msg + ')');
  });
}

Handling Errors and Clean Shutdown

A worker can crash. Always wire up its lifecycle events so a failed job does not silently hang a request:

  • 'error' — an uncaught exception inside the worker.
  • 'exit' — fired once; code is non-zero on failure.
  • 'messageerror' — a message could not be deserialized.

To stop a worker from the main thread, call worker.terminate() (returns a promise). Unterminated workers keep the process alive.

const { Worker, isMainThread, parentPort } = require('node:worker_threads');

if (isMainThread) {
  const worker = new Worker(__filename);
  worker.on('error', (err) => console.log('caught worker error:', err.message));
  worker.on('exit', (code) => console.log('exited, code =', code));
} else {
  throw new Error('boom inside worker');
}

A Practical Backend Shape

In a real Node.js backend you do not spawn a worker per request — thread startup is expensive. Instead:

  • Keep a small pool of long-lived workers (often os.cpus().length).
  • Send each CPU-bound job over postMessage and match results by an id.
  • Reserve workers for genuinely CPU-bound work; leave I/O on the event loop.

This snippet shows the per-worker message protocol using an id to correlate request and response — the core idea every pool library (like piscina) builds on.

const { Worker, isMainThread, parentPort } = require('node:worker_threads');

if (isMainThread) {
  const worker = new Worker(__filename);
  const pending = new Map();
  let nextId = 0;
  worker.on('message', ({ id, result }) => {
    pending.get(id)(result);
    pending.delete(id);
    if (pending.size === 0) worker.terminate();
  });
  function submit(payload) {
    return new Promise((res) => {
      const id = nextId++;
      pending.set(id, res);
      worker.postMessage({ id, payload });
    });
  }
  Promise.all([submit(3), submit(7)]).then((r) => console.log('results:', r));
} else {
  parentPort.on('message', ({ id, payload }) => {
    parentPort.postMessage({ id, result: payload * payload });
  });
}

Quick Check

You must send a 50 MB image buffer to a worker for processing and you want to avoid duplicating that memory. Which approach is correct?

Recap

You now know how to spawn workers and move data between threads:

  • Spawn with new Worker(file, { workerData }); branch on isMainThread.
  • Send results with parentPort.postMessage() and receive via worker.on('message') — the channel is two-way.
  • Messages use structured clone (richer than JSON, but no functions); large buffers should be transferred via a transfer list for zero-copy.
  • Create extra pipes with MessageChannel and transfer a MessagePort for side or worker-to-worker channels.
  • Always handle error/exit and call terminate(); in production use a pool with id-correlated messages rather than one worker per request.
무료로 시작

AI 튜터와 함께 JavaScript을(를) 배우세요 — 무료

브라우저에서 실제 코드를 작성하고 실행하며, 24/7 AI 튜터로부터 즉각적인 도움을 받고, 웹이나 앱에서 중단한 부분부터 계속 학습하세요.

코스
22
레슨
92

자주 묻는 질문

“워커 스레드 생성 및 메시지 전달” 강의는 무료인가요?

네 — “워커 스레드 생성 및 메시지 전달” 전체 내용을 이 웹사이트에서 무료로 읽을 수 있습니다. 인터랙티브하게 실습하려면(내장 코드 에디터와 24/7 AI 튜터), CoddyKit PRO로 업그레이드하면 Node.js Backend Development Bootcamp 강의 전체를 잠금 해제할 수 있습니다. Node.js Backend Development Bootcamp 강의에는 총 4개의 강의가 포함되어 있습니다.

“워커 스레드 생성 및 메시지 전달”에서 뭘 배우나요?

워커를 만들고 postMessage로 데이터를 교환하며 workerData와 MessageChannel로 구조화된 통신을 구현합니다. 브라우저에서 직접 실행하는 실습 코드로 Node.js Backend Development Bootcamp을(를) 배우며, 24/7 AI 튜터가 강의를 진행하면서 질문에 답변해줍니다.

Node.js Backend Development Bootcamp을(를) 시작하는 데 경험이 필요한가요?

사전 경험은 필요하지 않습니다. CoddyKit의 Node.js Backend Development Bootcamp은(는) 초급자부터 고급 학습자까지를 위해 구성되어 있으므로, 여기서 시작하거나 처음부터 시작할 수 있으며 자신의 속도대로 진행할 수 있습니다. 이것은 4개 중 2번째 강의입니다.

“워커 스레드 생성 및 메시지 전달” 강의는 얼마나 걸리나요?

대부분의 CoddyKit 강의는 약 5~10분이 소요됩니다. 각 강의는 간결하고 인터랙티브하여 꾸준한 진행이 가능하며, 웹과 앱에서 중단한 부분부터 바로 시작할 수 있습니다.

이 Node.js Backend Development Bootcamp 강의에서 코드를 작성하고 실행할 수 있나요?

네. 모든 Node.js Backend Development Bootcamp 강의에는 내장 코드 에디터가 포함되어 있으므로, 브라우저에서 바로 실제 코드를 작성하고 실행한 후 즉시 AI 피드백을 받을 수 있습니다 — 로컬 설정이 필요 없습니다.

이 강의의 모든 강의

  1. CPU 집약적 작업에서 이벤트 루프가 멈추는 이유
  2. 워커 스레드 생성 및 메시지 전달
  3. SharedArrayBuffer 및 Atomics를 활용한 메모리 공유
  4. 처리량 향상을 위한 재사용 가능한 워커 풀 만들기
← Node.js Backend Development Bootcamp(으)로 돌아가기