← 글 목록

notes

web worker

Web Worker의 백그라운드 실행·메시지 통신과 적용 기준 정리.

  • Web Worker는 무거운 계산을 별도 스레드로 빼서 클릭이 안 막히게 해 준다.
  • 메인과 워커는 postMessage로만 주고받고, 데이터는 복사된다. DOM은 못 만진다.
  • 큰 데이터는 transfer로 넘기고, 다 쓴 워커는 terminate()로 정리한다.

메인 스레드에서 무거운 계산·파싱을 돌리면 클릭이 먹통이 된다. 그때 백그라운드 스레드로 빼는 API가 Web Worker다. 나는 대시보드에서 수만 건짜리 JSON을 정렬하다가 화면이 멈추는 걸 보고 처음 썼다.

한 줄로

new Worker('worker.js')로 별도 스레드에서 JS를 돌린다. UI 스레드는 안 막고, 결과는 postMessage로 주고받는다. 데이터는 공유가 아니라 복사된다.

쓸 수 있는 것 / 없는 것

Worker 안에서는 String, Array, fetch, setTimeout 같은 건 대부분 된다. 안 되는 것:

  • document / window / DOM 조작
  • 일부 Window 전용 API

그래서 UI는 메인이 그리고, 숫자 계산·JSON 파싱·이미지 픽셀 처리·MQTT 클라이언트처럼 오래 걸리는 일만 워커로 보낸다. MQTT 글에 Next에서 클라이언트를 워커로 돌린 예제를 따로 적어 뒀다.

최소 예제

// main.js
const myWorker = new Worker('worker.js');
myWorker.postMessage({ type: 'sum', values: [1, 2, 3] });
myWorker.onmessage = (e) => {
  console.log('결과', e.data);
};
// worker.js
onmessage = (e) => {
  const { type, values } = e.data;
  if (type === 'sum') {
    const total = values.reduce((a, b) => a + b, 0);
    postMessage({ type: 'sum', total });
  }
};

종류

보통 앱 로직 오프로딩은 Dedicated부터.

같은 origin

워커 스크립트도 같은 origin에서 불러와야 한다. CDN에 올린 파일을 바로 new Worker()에 넣으면 막힌다. 번들러를 쓰면 new Worker(new URL('./worker.js', import.meta.url))로 쓰면 되고, 이러면 webpack과 Vite가 워커 파일을 따로 묶어 준다.

Next.js 클라이언트 컴포넌트에서 쓴 방식

워커는 브라우저에서만 있어서 서버 컴포넌트에서 만들면 에러가 난다. useEffect 안에서 만들고, 언마운트될 때 꼭 종료한다.

'use client';
import { useEffect, useRef, useState } from 'react';

export default function SumBox() {
  const workerRef = useRef<Worker | null>(null);
  const [total, setTotal] = useState<number>();

  useEffect(() => {
    const worker = new Worker(new URL('../workers/sum.worker.ts', import.meta.url), { type: 'module' });
    worker.onmessage = (e) => setTotal(e.data.total);
    worker.onerror = (e) => console.error('worker error', e.message);
    workerRef.current = worker;
    return () => worker.terminate();
  }, []);

  return (
    <button onClick={() => workerRef.current?.postMessage({ type: 'sum', values: [1, 2, 3] })}>
      합계 {total}
    </button>
  );
}

언제 쓰나 / 안 쓰나

  • 씀: 메인 스레드 50ms 넘는 동기 작업, INP 나쁠 때, 대용량 파싱
  • 안 씀: DOM이 꼭 필요한 일, 메시지 직렬화 비용이 이득보다 클 때, 단순 fetch 한 번

큰 데이터를 넘길 때

메시지를 자주, 크게 주고받으면 복사 비용이 계산 시간보다 커지는 일이 생긴다. 워커를 붙였는데 오히려 느려졌다면 대부분 이 경우였다.

ArrayBuffer 같은 건 두 번째 인자로 넘기면 복사 없이 소유권만 옮긴다. 대신 보낸 쪽에서는 더 못 쓴다.

const buf = new ArrayBuffer(32 * 1024 * 1024); // 32MB
worker.postMessage(buf, [buf]);
console.log(buf.byteLength); // 0, 이제 워커 것

캔버스 그리기가 무거우면 canvas.transferControlToOffscreen()으로 그리기 자체를 워커에 넘길 수도 있다. 차트 라이브러리가 이걸 지원하는지는 먼저 확인해야 한다.