[Daily morning study] Web Workers와 Service Workers 차이점과 활용
#daily morning study
브라우저의 단일 스레드 한계
자바스크립트는 기본적으로 싱글 스레드로 실행된다. 메인 스레드가 무거운 연산을 처리하는 동안 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 Workers | Service 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의 복잡한 캐싱 전략을 단순하게 구성할 수 있다.