핵심 정리
const worker = new Worker(new URL("./worker.js", import.meta.url), {
workerData: { n: 5_000_000 },
});워커 사용
먼저 구분해 둘 기본형은 아래입니다.
- 시작 시 데이터 전달:
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