본문 바로가기
Frontend2024년 9월 17일5분 읽기

Web Worker로 메인 스레드 부하 줄이기

YS
김영삼
조회 461

Web Worker란?

Web Worker는 브라우저에서 JavaScript를 백그라운드 스레드에서 실행할 수 있게 해주는 API입니다. 메인 스레드와 독립적으로 동작하여, 무거운 연산 중에도 UI가 멈추지 않습니다. Worker는 DOM에 접근할 수 없으며, postMessage를 통해 메인 스레드와 데이터를 주고받습니다.

기본 사용법

// worker.js
self.addEventListener('message', (e) => {
  const { type, data } = e.data;

  if (type === 'HEAVY_CALC') {
    let result = 0;
    for (let i = 0; i < data.iterations; i++) {
      result += Math.sqrt(i) * Math.sin(i);
    }
    self.postMessage({ type: 'RESULT', result });
  }
});

// main.js
const worker = new Worker('worker.js');

worker.addEventListener('message', (e) => {
  console.log('결과:', e.data.result);
});

worker.postMessage({
  type: 'HEAVY_CALC',
  data: { iterations: 100_000_000 }
});

Transferable Objects로 성능 향상

// 대용량 데이터 전송 시 복사 대신 소유권 이전
const buffer = new ArrayBuffer(1024 * 1024 * 100); // 100MB

// 느림: 데이터를 복사
// worker.postMessage({ buffer });

// 빠름: 소유권을 이전 (zero-copy)
worker.postMessage({ buffer }, [buffer]);
// 이후 메인 스레드에서 buffer는 사용 불가

이미지 처리 실전 예제

// image-worker.js
self.addEventListener('message', async (e) => {
  const { imageData, filter } = e.data;
  const pixels = imageData.data;

  if (filter === 'grayscale') {
    for (let i = 0; i < pixels.length; i += 4) {
      const avg = (pixels[i] + pixels[i+1] + pixels[i+2]) / 3;
      pixels[i] = pixels[i+1] = pixels[i+2] = avg;
    }
  } else if (filter === 'blur') {
    const w = imageData.width;
    const h = imageData.height;
    const copy = new Uint8ClampedArray(pixels);
    for (let y = 1; y < h - 1; y++) {
      for (let x = 1; x < w - 1; x++) {
        for (let c = 0; c < 3; c++) {
          let sum = 0;
          for (let dy = -1; dy <= 1; dy++) {
            for (let dx = -1; dx <= 1; dx++) {
              sum += copy[((y+dy)*w + (x+dx))*4 + c];
            }
          }
          pixels[(y*w + x)*4 + c] = sum / 9;
        }
      }
    }
  }

  self.postMessage({ imageData }, [imageData.data.buffer]);
});

Worker Pool 패턴

class WorkerPool {
  constructor(workerUrl, size = navigator.hardwareConcurrency || 4) {
    this.workers = Array.from({ length: size }, () => new Worker(workerUrl));
    this.queue = [];
    this.freeWorkers = [...this.workers];
  }

  exec(data) {
    return new Promise((resolve) => {
      const task = { data, resolve };
      const worker = this.freeWorkers.pop();
      if (worker) {
        this._run(worker, task);
      } else {
        this.queue.push(task);
      }
    });
  }

  _run(worker, task) {
    worker.onmessage = (e) => {
      task.resolve(e.data);
      const next = this.queue.shift();
      if (next) this._run(worker, next);
      else this.freeWorkers.push(worker);
    };
    worker.postMessage(task.data);
  }

  terminate() {
    this.workers.forEach(w => w.terminate());
  }
}

// 사용 예
const pool = new WorkerPool('worker.js', 4);
const results = await Promise.all(
  chunks.map(chunk => pool.exec(chunk))
);

Worker 사용 시 주의사항

  • Worker 생성 비용이 있으므로, 반복 생성보다 재사용(Worker Pool)이 효율적입니다
  • DOM 접근 불가 — document, window 객체를 사용할 수 없습니다
  • SharedArrayBuffer를 사용하면 Worker 간 메모리 공유가 가능합니다 (COOP/COEP 헤더 필요)
  • Webpack에서는 new Worker(new URL('./worker.js', import.meta.url)) 구문을 사용합니다
  • 에러 처리를 위해 worker.onerror 핸들러를 반드시 등록하세요

댓글 0

아직 댓글이 없습니다.
Ctrl+Enter로 등록