자바스크립트는 싱글 스레드이지만, Web Worker를 사용하면 별도 스레드에서 무거운 연산을 실행할 수 있습니다. 대용량 데이터 처리, 이미지 변환, 암호화 같은 작업에서 UI가 멈추지 않게 해줍니다.

왜 Web Worker가 필요한가?

JS
// 메인 스레드에서 무거운 연산 → 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)

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 });
  }
});

메인 스레드

JS
// 워커 생성
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)

별도 파일 없이 워커를 생성할 수 있습니다.

JS
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

JS
// 워커에서 사용 가능
self.postMessage();
self.addEventListener();
fetch();
setTimeout();
setInterval();
indexedDB;
crypto;
WebSocket;
importScripts("lib.js"); // 스크립트 동기 로딩

// 워커에서 사용 불가!
document;       // DOM 접근 불가
window;         // window 객체 없음
localStorage;   // 스토리지 접근 불가
alert();        // UI API 불가

데이터 전달 — 복사 vs 전송

JS
// 기본: 구조화된 복제 (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 래퍼 패턴

JS
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]

워커 풀

JS
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))
);

실전 활용 예시

JS
// 이미지 처리 워커
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)

JS
// 모듈 워커 생성 (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로 전달하면 복사 비용을 없앨 수 있습니다.

댓글 로딩 중...