본문 바로가기
개발

[JS] 메인 스레드를 비워라! Web Worker를 활용한 무거운 연산 분산 처리

by 돌미나리는야생미나리 2026. 4. 27.

프런트엔드 개발자라면 누구나 한 번쯤 겪어봤을 상황이 있습니다. "복잡한 JSON 데이터를 필터링하거나, 큰 이미지 파일을 처리할 때 화면이 잠시 멈춘다"는 사용자 불만 말이죠. 리액트나 뷰(Vue)를 아무리 잘 다뤄도, 메인 스레드(Main Thread)가 막히면 사용자에게는 '고장 난 사이트'로 보일 뿐입니다.

자바스크립트의 실행 환경인 브라우저는 기본적으로 싱글 스레드(Single-threaded)입니다. 즉, 한 번에 하나의 작업만 처리할 수 있습니다. 메인 스레드가 무거운 연산에 잡혀 있으면 화면을 그리는 일(렌더링)을 하지 못하게 되고, 이것이 바로 '화면 프리징'의 원인입니다.

오늘 소개할 Web Worker(웹 워커)는 브라우저의 이 한계를 돌파하는 강력한 도구입니다. 메인 스레드와 별개의 '백그라운드 스레드'에서 작업을 처리하여, 복잡한 연산 중에도 화면은 매끄럽게 움직이게 만들 수 있습니다.


1. 메인 스레드와 워커 스레드의 이해

웹 워커의 핵심은 '분리'입니다. 브라우저는 하나의 메인 스레드와 여러 개의 워커 스레드를 가질 수 있습니다. 메인 스레드는 UI를 그리고, 사용자의 입력을 받고, 네트워크 통신을 관리합니다. 반면, 워커 스레드는 순수하게 '계산'만 담당합니다.

1.1 왜 분리해야 하는가?

메인 스레드에서 for문을 1억 번 돌린다면? 브라우저는 그 1억 번의 루프가 끝날 때까지 아무것도 하지 못합니다. 버튼을 눌러도 반응이 없고, 애니메이션도 멈춥니다. 하지만 이 작업을 워커 스레드로 넘기면, 워커가 연산하는 동안 메인 스레드는 유유히 사용자의 마우스 클릭을 처리하고 애니메이션을 그릴 수 있습니다.


2. Web Worker 구현하기: 3단계 프로세스

웹 워커는 메인 스레드와 직접적인 메모리 공유를 하지 않습니다. 대신 '메시지(Message)'를 주고받는 방식으로 데이터를 전달합니다.

2.1 워커 파일 생성 (worker.js)

먼저, 별도의 파일에 무거운 작업 내용을 작성합니다.

JavaScript
 
// worker.js
self.onmessage = function(e) {
  const data = e.data;
  
  // 여기서 아주 무거운 연산 수행
  const result = heavyCalculation(data);
  
  // 결과를 메인 스레드로 반환
  self.postMessage(result);
};

function heavyCalculation(data) {
  // 예: 수만 개의 데이터를 처리하는 복잡한 알고리즘
  return data.map(item => item * 2);
}

2.2 메인 스레드에서 워커 호출하기

이제 메인 스레드에서 이 워커를 실행하고 메시지를 보냅니다.

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

// 연산 시작
worker.postMessage([1, 2, 3, 4, 5]);

// 결과 받기
worker.onmessage = function(e) {
  console.log('연산 결과:', e.data);
};

이 간단한 코드로 복잡한 로직을 메인 스레드 밖으로 안전하게 격리할 수 있습니다.


3. 웹 워커가 가지는 제약 사항과 주의점

웹 워커는 매우 강력하지만, 모든 곳에 사용할 수는 없습니다. 반드시 알아야 할 한계점이 존재합니다.

  1. DOM 접근 불가: 웹 워커는 DOM을 전혀 조작할 수 없습니다. document.querySelector나 window에 접근하려고 하면 에러가 발생합니다. 오직 연산(CPU 집약적 작업)만 가능합니다.
  2. 메모리 격리: 메인 스레드와 메모리를 공유하지 않습니다. 데이터를 전달할 때마다 데이터를 '복사'해서 보냅니다. (아주 큰 데이터를 주고받을 경우 복사 비용이 발생할 수 있음)
  3. 파일 경로 제약: 워커 파일은 반드시 메인 스레드와 동일한 오리진(Origin)에 있어야 합니다. (CORS 정책 적용)

4. 실전 활용 사례: 언제 도입해야 할까?

모든 함수를 웹 워커로 옮기는 것은 낭비입니다. 다음 상황일 때 도입을 강력히 권장합니다.

  • 이미지/비디오 처리: Canvas API를 활용한 필터 적용이나 압축 작업.
  • 복잡한 데이터 정제: 수만 개의 JSON 데이터를 필터링하거나 변환해야 하는 대시보드.
  • 암호화/복호화: JWT 토큰 암호화나 파일 암호화 작업 등 CPU를 많이 쓰는 작업.
  • 실시간 텍스트 분석: 사용자가 입력하는 텍스트의 맞춤법 검사나 복잡한 정규식 검색.

5. 생산성을 높이는 도구: Comlink

순수하게 웹 워커를 다루면 postMessage와 onmessage를 매번 작성해야 해서 코드가 다소 지저분해집니다. 이때 구글에서 만든 Comlink 라이브러리를 사용하면 마치 일반 함수를 호출하듯 웹 워커를 사용할 수 있습니다.

JavaScript
 
// worker.js
import * as Comlink from 'comlink';

const obj = {
  doWork(a, b) { return a + b; }
};

Comlink.expose(obj);

// main.js
import * as Comlink from 'comlink';

async function init() {
  const worker = new Worker('worker.js');
  const api = Comlink.wrap(worker);
  
  // 그냥 함수처럼 호출하면 끝!
  const result = await api.doWork(10, 20);
  console.log(result);
}

이렇게 하면 복잡한 메시지 핸들링 로직을 추상화하여 코드의 가독성을 크게 높일 수 있습니다.