Quick Reference
import { Worker } from "node:worker_threads";
function runSum(values) {
return new Promise((resolve, reject) => {
const worker = new Worker(new URL("./sum-worker.mjs", import.meta.url), {
workerData: { values },
});
worker.once("message", resolve);
worker.once("error", reject);
worker.once("exit", (code) => {
if (code !== 0) reject(new Error(`worker exited with ${code}`));
});
});
}
console.log(await runSum([10, 20, 30]));// sum-worker.mjs
import { parentPort, workerData } from "node:worker_threads";
const total = workerData.values.reduce((sum, value) => sum + value, 0);
parentPort.postMessage(total);CPU 바운드 계산으로 메인 이벤트 루프가 막힐 때 Worker를 씁니다. I/O 요청 자체는 Node의 비동기 API가 처리하므로 보통 worker로 옮길 이유가 없습니다. 메시지는 기본적으로 구조화 복사되므로 큰 데이터를 자주 보내면 비용이 생기며, message, error, exit 세 경로를 모두 처리합니다.
워커 사용
먼저 구분해 둘 기본형은 아래입니다.
- 시작 시 데이터 전달:
workerData - 실행 중 메시지 전달:
worker.postMessage(...) - 복사 대신 이동:
transferList - 공유 메모리:
SharedArrayBuffer - 종료 감시:
message/error/exit
워커가 맞는 경우
- 이미지 처리
- 대량 파싱
- 암호화/압축 계산
- 큰 루프 연산
이 패턴은 "메인 루프를 비우고 계산을 옆으로 보낸다"는 의미로 읽으면 됩니다.
워커가 아닌 경우
- 파일 읽기
- 네트워크 요청
- DB 쿼리
이런 작업은 워커를 붙인다고 자동으로 좋아지지 않습니다. 오히려 스레드 생성과 메시지 전달 비용만 늘 수 있습니다.
const worker = new Worker(new URL("./worker.js", import.meta.url), {
workerData: { url: "https://api.example.com/data" },
});이런 식으로 네트워크 요청 자체를 워커로 옮기는 건 보통 이득이 약합니다. Node 런타임이 이미 비동기 I/O를 처리하기 때문입니다.
데이터 전달도 비용이다
workerData: 시작 시 넘김postMessage: 실행 중 메시지 전달SharedArrayBuffer: 공유 메모리
즉 워커 카드는 계산 카드이면서 동시에 "데이터 이동 비용" 카드입니다.
큰 데이터를 자주 복사하면 워커 자체가 병목이 될 수 있습니다.
worker.postMessage(hugeObject);계산보다 복사 비용이 더 커지면 워커를 붙여도 체감 이득이 거의 없습니다. 그래서 워커 카드는 "CPU 계산량"과 "메시지 전달량"을 같이 보는 편이 맞습니다.
워커 하나보다 풀 설계가 더 중요한 경우가 많다
요청마다 새 워커를 만드는 구조는 스레드 생성 비용이 바로 눈에 띌 수 있습니다. 반복적으로 무거운 계산을 처리한다면 보통은 워커를 한 번 띄우는 것보다 풀과 큐를 같이 설계하는 편이 낫습니다.
부모 쪽 기본 이벤트 세트는 같이 본다
워커를 띄웠으면 보통 아래 셋은 같이 붙습니다.
message: 계산 결과 수신error: 런타임 실패 처리exit: 비정상 종료 감지
언제 워커가 맞나
체크포인트
- CPU 바운드면 워커 검토
- I/O 바운드면 보통 워커 불필요
- 계산량과 전달 비용을 같이 본다
- 에러/exit/message 경로를 같이 설계한다
- 반복 작업이면 단일 워커보다 풀 설계 검토
주의할 점
워커를 쓰는 순간 "비동기라서 빨라진다"가 아니라 "병목 위치를 옮긴다"가 됩니다. 메인 루프 차단은 줄일 수 있지만, 메시지 복사와 스레드 관리 비용이 생기므로 CPU 병목이 명확할 때만 쓰는 편이 맞습니다.
참고 링크
1 sources