Web Worker 기초 — 메인 스레드 블로킹을 피하는 법
자바스크립트는 싱글 스레드이지만, Web Worker를 사용하면 별도 스레드에서 무거운 연산을 실행할 수 있습니다. 대용량 데이터 처리, 이미지 변환, 암호화 같은 작업에서 UI가 멈추지 않게 해줍니다.
왜 Web Worker가 필요한가?
// 메인 스레드에서 무거운 연산 → UI 멈춤
function heavyCalculation() {
let sum = 0;
for (let i = 0; i < 1_000_000_000; i++) {
sum += i;
}
return sum;
}
button.addEventListener("click", () => {
const result = heavyCalculation(); // 이 동안 UI 완전 멈춤!
console.log(result);
});
기본 사용법
워커 파일 (worker.js)
// worker.js — 별도 스레드에서 실행
self.addEventListener("message", (event) => {
const { type, data } = event.data;
if (type === "calculate") {
let sum = 0;
for (let i = 0; i < data.iterations; i++) {
sum += i;
}
// 결과를 메인 스레드로 전송
self.postMessage({ type: "result", data: sum });
}
});
메인 스레드
// 워커 생성
const worker = new Worker("worker.js");
// 메시지 전송
worker.postMessage({
type: "calculate",
data: { iterations: 1_000_000_000 },
});
// 결과 수신
worker.addEventListener("message", (event) => {
console.log("결과:", event.data.data);
});
// 에러 처리
worker.addEventListener("error", (event) => {
console.error("워커 에러:", event.message);
});
// 워커 종료
worker.terminate();
인라인 워커 (Blob URL)
별도 파일 없이 워커를 생성할 수 있습니다.
function createWorker(fn) {
const blob = new Blob([`(${fn.toString()})()`], {
type: "application/javascript",
});
const url = URL.createObjectURL(blob);
const worker = new Worker(url);
URL.revokeObjectURL(url); // URL 해제
return worker;
}
const worker = createWorker(() => {
self.addEventListener("message", (e) => {
const result = e.data * 2;
self.postMessage(result);
});
});
worker.postMessage(21);
worker.addEventListener("message", (e) => {
console.log(e.data); // 42
});
워커에서 접근 가능한 API
// 워커에서 사용 가능
self.postMessage();
self.addEventListener();
fetch();
setTimeout();
setInterval();
indexedDB;
crypto;
WebSocket;
importScripts("lib.js"); // 스크립트 동기 로딩
// 워커에서 사용 불가!
document; // DOM 접근 불가
window; // window 객체 없음
localStorage; // 스토리지 접근 불가
alert(); // UI API 불가
데이터 전달 — 복사 vs 전송
// 기본: 구조화된 복제 (Structured Clone) — 데이터가 복사됨
const largeArray = new Float64Array(1_000_000);
worker.postMessage(largeArray); // 복사 발생 — 메모리 2배
// Transferable Objects — 소유권 이전 (복사 없음)
const buffer = new ArrayBuffer(1024 * 1024); // 1MB
worker.postMessage(buffer, [buffer]); // 두 번째 인자로 전달
console.log(buffer.byteLength); // 0 — 소유권이 이전됨!
Promise 래퍼 패턴
function workerTask(worker, data) {
return new Promise((resolve, reject) => {
const id = Math.random().toString(36);
function handler(event) {
if (event.data.id === id) {
worker.removeEventListener("message", handler);
resolve(event.data.result);
}
}
worker.addEventListener("message", handler);
worker.addEventListener("error", reject, { once: true });
worker.postMessage({ id, ...data });
});
}
// 사용
const result = await workerTask(worker, {
type: "sort",
data: [3, 1, 4, 1, 5, 9],
});
console.log(result); // [1, 1, 3, 4, 5, 9]
워커 풀
class WorkerPool {
constructor(workerScript, size = navigator.hardwareConcurrency || 4) {
this.workers = Array.from({ length: size }, () => new Worker(workerScript));
this.queue = [];
this.freeWorkers = [...this.workers];
}
run(data) {
return new Promise((resolve, reject) => {
const task = { data, resolve, reject };
if (this.freeWorkers.length > 0) {
this.dispatch(this.freeWorkers.pop(), task);
} else {
this.queue.push(task);
}
});
}
dispatch(worker, task) {
worker.onmessage = (e) => {
task.resolve(e.data);
if (this.queue.length > 0) {
this.dispatch(worker, this.queue.shift());
} else {
this.freeWorkers.push(worker);
}
};
worker.onerror = task.reject;
worker.postMessage(task.data);
}
terminate() {
this.workers.forEach((w) => w.terminate());
}
}
// 사용 — 4개 워커로 병렬 처리
const pool = new WorkerPool("worker.js", 4);
const results = await Promise.all(
tasks.map((task) => pool.run(task))
);
실전 활용 예시
// 이미지 처리 워커
self.addEventListener("message", (e) => {
const { imageData, filter } = e.data;
const data = imageData.data;
if (filter === "grayscale") {
for (let i = 0; i < data.length; i += 4) {
const avg = (data[i] + data[i + 1] + data[i + 2]) / 3;
data[i] = data[i + 1] = data[i + 2] = avg;
}
}
self.postMessage({ imageData }, [imageData.data.buffer]);
});
모듈 워커 (ES Modules)
// 모듈 워커 생성 (import/export 사용 가능)
const worker = new Worker("worker.js", { type: "module" });
// worker.js
import { heavyCompute } from "./utils.js";
self.addEventListener("message", async (e) => {
const result = await heavyCompute(e.data);
self.postMessage(result);
});
**기억하기 **: Web Worker는 메인 스레드를 블로킹하지 않고 무거운 연산을 처리하는 방법입니다. DOM에 접근할 수 없고, 데이터는 postMessage로 주고받습니다. 대용량 데이터는 Transferable Objects로 전달하면 복사 비용을 없앨 수 있습니다.
댓글 로딩 중...