WebWorker(웹 워커)란 무엇인가

hannah·2024년 6월 26일

JavaScript

목록 보기
108/108
post-thumbnail

자바스크립트는 기본적으로 메인 스레드 하나에서 동작하는 싱글 스레드 언어다. 한 번에 하나의 작업만 처리할 수 있다는 뜻이다. 만약 우리가 웹 페이지에서 매우 복잡한 수학 연산을 하거나 대용량 데이터를 처리한다고 가정해보자.

메인 스레드가 이 무거운 작업을 처리하느라 바빠지면 화면을 그리는 작업이나 사용자의 클릭 이벤트를 처리할 수 없게 된다. 결과적으로 브라우저 화면이 멈추거나 버벅거리는 현상이 발생한다. 이것은 사용자 경험을 심각하게 해친다.

이러한 한계를 극복하기 위해 등장한 것이 바로 Web Worker다. 이것은 메인 스레드와는 별개로 백그라운드 스레드에서 스크립트를 실행할 수 있는 기술이다. 무거운 작업을 워커 스레드에게 맡기면 메인 스레드는 방해받지 않고 UI를 렌더링하거나 사용자와 상호작용할 수 있다.

이해를 돕기 위해 백그라운드 스레드를 활용해서 1부터 100억까지 숫자를 더하는 예제 코드를 작성해보았다.

worker.js

이 파일은 백그라운드에서 실행될 코드를 담는다.

// 메인 스레드로부터 메시지를 받으면 실행된다
self.onmessage = function(e) {
  // 메인 스레드에서 보낸 데이터
  const limit = e.data;
  
  let sum = 0;
  // 시간이 오래 걸리는 무거운 연산
  for (let i = 1; i <= limit; i++) {
    sum += i;
  }

  // 연산 결과를 다시 메인 스레드로 보낸다
  self.postMessage(sum);
};

WorkerDemo.tsx

이 파일은 웹 페이지의 메인 로직을 담당한다. (React 환경의 컴포넌트 내부 로직이다)

// Worker 생성자로 워커 스레드를 만든다
const myWorker = new Worker(new URL('./worker.js', import.meta.url));

// 워커에게 100억이라는 데이터를 보내며 작업을 지시한다
myWorker.postMessage(10000000000);
console.log('메인 스레드: 워커에게 일을 시켰습니다.');

// 워커가 작업을 끝내고 메시지를 보내면 실행된다
myWorker.onmessage = function(e) {
  const result = e.data;
  console.log('메인 스레드: 워커로부터 결과를 받았습니다 -> ' + result);
};

console.log('메인 스레드: 워커가 일하는 동안 다른 작업을 계속합니다.');

실행 결과 및 분석

아래 이미지는 작성한 코드를 바탕으로 실제 UI를 구성하여 테스트한 결과다.

메인 스레드에서 실행 버튼 클릭

첫 번째 이미지는 메인 스레드에서 작업을 실행한 결과다. 계산을 완료하기까지 약 1.5초(1559ms)가 걸렸다. 중요한 것은 이 1.5초 동안 브라우저는 이 연산에 모든 자원을 쏟아붓느라 화면을 갱신하지 못한다는 점이다. 사용자가 느끼기에는 화면이 완전히 멈춘 것처럼 보인다.

워커 스레드에서 실행 버튼 클릭

두 번째 이미지는 워커 스레드를 사용한 결과 로그다. 워커에게 일을 시킨 직후 메인 스레드는 멈추지 않고 즉시 다음 코드를 실행했음을 콘솔 로그 순서로 확인할 수 있다.

워커에게 일을 시킴
즉시 다른 작업을 계속함
나중에 워커로부터 결과를 받음

이것이 바로 Web Worker를 사용하는 핵심 이유다. 덕분에 계산이 진행되는 수 초 동안에도 버튼 클릭이나 스크롤 같은 사용자 상호작용이 매끄럽게 동작한다.

React 환경에서의 활용

앞서 본 WorkerDemo.tsx와 같이 React나 Next.js 환경에서도 원리는 동일하다. 다만 컴포넌트 생명주기를 고려해야 한다. 일반적으로 useEffect 훅 내부에서 워커 인스턴스를 생성하고 컴포넌트가 언마운트될 때 워커를 종료하여 메모리 누수를 방지한다.

// React 컴포넌트 전체 예시

import { useEffect, useState } from 'react';

export default function WorkerComponent() {
  const [result, setResult] = useState<number | null>(null);

  useEffect(() => {
    const worker = new Worker(new URL('./worker.js', import.meta.url));

    worker.onmessage = (e) => {
      setResult(e.data);
    };

    worker.postMessage(10000000000);

    return () => {
      worker.terminate();
    };
  }, []);

  return <div>계산 결과: {result}</div>;
}

주의할 점

Web Worker가 강력한 도구임은 분명하지만 몇 가지 제약 사항이 있다. 워커 스레드는 DOM에 직접 접근할 수 없다. 즉 document나 window 객체를 조작하여 화면을 업데이트하는 작업은 반드시 메인 스레드에서 처리해야 한다. 또한 워커 간의 데이터 통신은 깊은 복사 방식을 사용하므로 전송하는 데이터 크기가 매우 크면 통신 비용이 발생할 수 있다.

정리

즉 내가 이해한 Web Workder란, 메인 스레드의 부하를 줄이기 위해 워커 스레드라는 아예 별도의 스레드에서 병렬로 처리할 수 있게 해주는 최적화 기술이다.

Web Worker는 자바스크립트의 싱글 스레드 제약을 넘어설 수 있게 해준다. 메인 스레드와 워커 스레드는 postMessage 메서드와 onmessage 이벤트를 통해 서로 데이터를 주고받는다. 이미지 처리나 대용량 데이터 정렬과 같이 CPU 소모가 큰 작업이 필요할 때 Web Worker를 사용하면 쾌적하고 부드러운 웹 애플리케이션을 만들 수 있다.

0개의 댓글