[Daily morning study] Web Workers와 Service Workers 차이점과 활용

#daily morning study

Image


브라우저의 단일 스레드 한계

자바스크립트는 기본적으로 싱글 스레드로 실행된다. 메인 스레드가 무거운 연산을 처리하는 동안 UI는 응답하지 못하고 화면이 멈추는 현상(jank)이 발생한다.

이를 해결하기 위해 브라우저는 두 가지 Worker API를 제공한다.

  • Web Workers: 병렬 연산을 위한 백그라운드 스레드
  • Service Workers: 네트워크 프록시 역할을 하는 특수 워커

Web Workers

개념

Web Workers는 메인 스레드와 분리된 별도의 스레드에서 자바스크립트를 실행한다. DOM에는 접근할 수 없고, 메시지 패싱 방식으로 메인 스레드와 통신한다.

사용 방법

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

// 워커에 데이터 전송
worker.postMessage({ data: [1, 2, 3, 4, 5] });

// 워커에서 결과 수신
worker.onmessage = (event) => {
  console.log('결과:', event.data);
};

worker.onerror = (error) => {
  console.error('워커 오류:', error);
};
// worker.js
self.onmessage = (event) => {
  const { data } = event.data;
  
  // 무거운 연산 수행
  const result = data.reduce((sum, n) => sum + n, 0);
  
  // 메인 스레드에 결과 전송
  self.postMessage(result);
};

Transferable Objects

기본적으로 postMessage는 데이터를 복사(structured clone)해서 전달한다. 대용량 데이터는 이 복사 비용이 크다.

ArrayBuffer 같은 Transferable Object는 복사 없이 소유권을 이전할 수 있다.

const buffer = new ArrayBuffer(1024 * 1024 * 100); // 100MB

// 복사 대신 소유권 이전 (원본은 무효화됨)
worker.postMessage(buffer, [buffer]);

console.log(buffer.byteLength); // 0 (소유권 이전 후 무효화됨)

활용 사례

사례설명
이미지/비디오 처리필터 적용, 포맷 변환
대용량 데이터 파싱CSV, JSON 파일 파싱
암호화 연산해싱, 서명 등 CPU 집약적 작업
물리 엔진게임의 충돌 계산
머신러닝 추론TensorFlow.js 모델 실행

Shared Worker

여러 탭이나 iframe에서 동일한 워커를 공유할 수 있다. 탭 간 상태 공유가 필요할 때 유용하다.

// 여러 탭에서 동일한 워커 공유
const shared = new SharedWorker('shared-worker.js');
shared.port.start();
shared.port.postMessage('hello');

Service Workers

개념

Service Worker는 브라우저와 네트워크 사이에 위치하는 프록시 서버처럼 동작한다. 네트워크 요청을 가로채고, 캐싱을 제어하며, 오프라인 기능과 백그라운드 동기화를 구현한다.

HTTPS에서만 동작하며(localhost 제외), 페이지와 독립적으로 실행되어 탭이 닫혀있어도 동작할 수 있다.

생명주기

Install → Activate → Fetch (이후 반복)
// service-worker.js
const CACHE_NAME = 'my-cache-v1';
const URLS_TO_CACHE = ['/', '/styles.css', '/app.js'];

// 설치 단계: 필요한 리소스를 캐시에 저장
self.addEventListener('install', (event) => {
  event.waitUntil(
    caches.open(CACHE_NAME).then((cache) => {
      return cache.addAll(URLS_TO_CACHE);
    })
  );
});

// 활성화 단계: 오래된 캐시 정리
self.addEventListener('activate', (event) => {
  event.waitUntil(
    caches.keys().then((cacheNames) => {
      return Promise.all(
        cacheNames
          .filter((name) => name !== CACHE_NAME)
          .map((name) => caches.delete(name))
      );
    })
  );
});

// 네트워크 요청 가로채기
self.addEventListener('fetch', (event) => {
  event.respondWith(
    caches.match(event.request).then((response) => {
      // 캐시에 있으면 캐시 반환, 없으면 네트워크 요청
      return response || fetch(event.request);
    })
  );
});

등록 방법

// main.js
if ('serviceWorker' in navigator) {
  navigator.serviceWorker
    .register('/service-worker.js')
    .then((registration) => {
      console.log('Service Worker 등록 성공:', registration.scope);
    })
    .catch((error) => {
      console.error('Service Worker 등록 실패:', error);
    });
}

캐싱 전략

전략설명적합한 경우
Cache First캐시 우선, 없으면 네트워크정적 자산 (이미지, 폰트)
Network First네트워크 우선, 실패시 캐시자주 바뀌는 API 데이터
Stale While Revalidate캐시 즉시 반환 후 백그라운드 업데이트뉴스 피드, 목록 페이지
Cache Only캐시만 사용오프라인 전용 앱
Network Only네트워크만 사용결제, 인증 등 항상 최신 데이터 필요

푸시 알림 (Push Notifications)

Service Worker는 앱이 닫혀있어도 푸시 알림을 수신할 수 있다.

// service-worker.js
self.addEventListener('push', (event) => {
  const data = event.data.json();
  
  event.waitUntil(
    self.registration.showNotification(data.title, {
      body: data.body,
      icon: '/icon.png',
    })
  );
});

Background Sync

네트워크 연결이 끊겼을 때 요청을 큐에 저장하고, 연결이 복구되면 자동으로 재시도한다.

// main.js
navigator.serviceWorker.ready.then((sw) => {
  return sw.sync.register('sync-messages');
});

// service-worker.js
self.addEventListener('sync', (event) => {
  if (event.tag === 'sync-messages') {
    event.waitUntil(sendPendingMessages());
  }
});

Web Workers vs Service Workers 비교

항목Web WorkersService Workers
목적CPU 집약적 연산 병렬 처리네트워크 프록시, 오프라인 지원
생명주기생성한 페이지와 함께독립적 (페이지 없이도 동작)
범위단일 페이지등록된 스코프의 모든 페이지
DOM 접근불가불가
네트워크 가로채기불가가능 (fetch 이벤트)
HTTPS 필요불필요필요 (localhost 제외)
캐싱 API불가가능 (Cache API)
브라우저 지원IE 10+IE 미지원

실전 사용 예시

React에서 Web Worker 활용

복잡한 데이터 가공 로직을 워커로 이동시켜 UI 블로킹을 방지한다.

// useWorker.js
import { useEffect, useRef, useState } from 'react';

function useWorker(workerFile) {
  const workerRef = useRef(null);
  const [result, setResult] = useState(null);

  useEffect(() => {
    workerRef.current = new Worker(workerFile);
    workerRef.current.onmessage = (e) => setResult(e.data);
    
    return () => workerRef.current.terminate();
  }, [workerFile]);

  const postMessage = (data) => {
    workerRef.current?.postMessage(data);
  };

  return { result, postMessage };
}

Workbox를 이용한 Service Worker 구성

직접 Service Worker 코드를 작성하는 대신 Google의 Workbox 라이브러리를 사용하면 캐싱 전략을 선언적으로 설정할 수 있다.

// service-worker.js (with Workbox)
import { precacheAndRoute } from 'workbox-precaching';
import { registerRoute } from 'workbox-routing';
import { NetworkFirst, CacheFirst } from 'workbox-strategies';

// 빌드 시 생성된 파일 목록 프리캐싱
precacheAndRoute(self.__WB_MANIFEST);

// API 요청: Network First
registerRoute(
  ({ url }) => url.pathname.startsWith('/api/'),
  new NetworkFirst({ cacheName: 'api-cache' })
);

// 이미지: Cache First
registerRoute(
  ({ request }) => request.destination === 'image',
  new CacheFirst({ cacheName: 'image-cache' })
);

핵심 정리

  • Web Workers는 메인 스레드 블로킹을 피하기 위한 병렬 연산 도구다. DOM 접근은 불가하고 메시지 패싱으로 통신한다.
  • Service Workers는 네트워크 레이어를 제어해 오프라인 지원, 캐싱, 푸시 알림을 구현한다. 페이지 생명주기와 독립적으로 동작한다.
  • 두 워커 모두 DOM에 직접 접근할 수 없고, postMessage를 통해 메인 스레드와 통신한다.
  • Workbox 같은 라이브러리를 사용하면 Service Worker의 복잡한 캐싱 전략을 단순하게 구성할 수 있다.