웹 워커와 서비스 워커
Web Worker로 무거운 계산을 메인 스레드에서 분리하고 Service Worker의 생명주기와 캐시·오프라인 응답을 익힙니다.
우리는 9장에서 Fetch API로 서버 통신, 로컬/세션 스토리지로 클라이언트 데이터 저장, History API로 SPA 라우팅 구현, Canvas API로 웹 그래픽 처리 등 다양한 웹 API와 브라우저 기능들을 학습했습니다.
이제 웹 애플리케이션의 성능과 사용자 경험을 한 단계 더 끌어올릴 수 있는 고급 기술인 웹 워커(Web Workers)와 서비스 워커(Service Workers)에 대해 알아볼 차례입니다.
자바스크립트는 기본적으로 단일 스레드(Single-threaded) 언어입니다.
이는 웹 페이지의 UI 렌더링, 이벤트 처리, DOM 조작 등 모든 작업이 하나의 메인 스레드에서 순차적으로 실행된다는 의미입니다.
만약 이 메인 스레드에서 복잡하거나 시간이 오래 걸리는 연산(예: 대용량 데이터 처리, 이미지 필터링, 복잡한 애니메이션 계산)을 수행하게 되면, 해당 작업이 끝날 때까지 UI가 멈추거나 반응이 없어지는 블로킹(Blocking) 현상이 발생하여 사용자 경험을 크게 저해합니다.
웹 워커는 메인 스레드 블로킹 문제를 줄이기 위해 자바스크립트 코드를 별도 스레드에서 실행합니다.
이를 통해 계산 비용이 큰 작업을 UI 렌더링 흐름과 분리할 수 있습니다.
서비스 워커는 네트워크 요청 가로채기, 캐싱, 오프라인 응답 같은 기능을 담당하는 브라우저의 백그라운드 실행 단위입니다.
이 절에서는 웹 워커와 서비스 워커의 동작 원리, 기본 사용법, 그리고 활용 시나리오를 정리합니다.
두 기술은 Progressive Web Apps (PWA)의 핵심 구성 요소이기도 합니다.
ARCHITECTURE
Main thread와 Web Worker 책임 경계
Main thread는 DOM과 UI를 소유하고 Worker는 계산을 맡으며, postMessage 왕복 edge에서 structured clone 또는 transferable ownership을 선택한다.
Main thread
정상 흐름 DOM event·UI state·layout/paint 소유
예외·수명 result를 검증한 뒤 화면 반영
Main → Worker
정상 흐름 postMessage(payload)는 structured clone
예외·수명 큰 ArrayBuffer는 transfer list로 ownership 이동 가능
Worker
정상 흐름 DOM 없이 CPU-heavy compute 수행
예외·수명 예외는 error event로 main에 전달
Worker → Main
정상 흐름 message(result)로 반환
예외·수명 clone 불가 payload는 messageerror
Terminate
정상 흐름 정상 result 단계가 아니라 별도 lifecycle 명령
예외·수명 pending work를 중단하고 이후 message를 받지 않음
- structured-clone message
- transfer/result/lifecycle
- error·messageerror
Transferable은 Worker 뒤의 처리 단계가 아니라 message edge의 ownership 옵션이다. terminate와 error도 정상 결과의 다음 단계가 아니라 별도 수명·예외 경로다.
웹 워커 (Web Workers)
웹 워커는 웹 페이지의 메인 실행 스크립트와 독립된 백그라운드 스레드에서 스크립트를 실행할 수 있도록 해줍니다.
이를 통해 메인 스레드가 무거운 연산으로 인해 블로킹되는 것을 방지하고, 웹 페이지의 UI가 항상 반응성을 유지하도록 할 수 있습니다.
웹 워커의 특징
- 독립적인 스레드: 웹 워커는 메인 스레드와 별도의 실행 환경을 가집니다.
- DOM 접근 불가: 워커 스레드는 HTML 문서의 DOM에 직접 접근할 수 없습니다. 이는 워커가 UI 업데이트를 직접 수행할 수 없다는 의미입니다.
- 통신 방식: 메인 스레드와 워커 스레드는
postMessage()메서드를 통한 메시지 기반 통신(onmessage이벤트 리스너)으로 데이터를 주고받습니다. 데이터는 복사되어 전달됩니다 (Serialization). - 제한된 API: 워커는
alert(),confirm()과 같은 브라우저 UI 관련 API에 접근할 수 없습니다. 대신XMLHttpRequest,fetch,IndexedDB,setTimeout,setInterval등은 사용할 수 있습니다. - 동일 출처 정책: 워커 스크립트는 메인 스크립트와 동일한 출처(origin)에 있어야 합니다.
웹 워커 사용법
worker.js)메인 스크립트와 독립적으로 실행될 자바스크립트 파일을 생성합니다.
// worker.js
// 이 스크립트는 웹 워커 스레드에서 실행됩니다.
// 메인 스크립트로부터 메시지를 받으면 실행되는 이벤트 핸들러
self.onmessage = function(event) {
const number = event.data; // 메인 스크립트로부터 전달받은 데이터
console.log(`워커: ${number}까지의 소수 계산을 시작합니다.`);
// 복잡하고 시간이 오래 걸리는 연산 (예시: 소수 계산)
let primes = [];
for (let i = 2; i <= number; i++) {
let isPrime = true;
for (let j = 2; j * j <= i; j++) {
if (i % j === 0) {
isPrime = false;
break;
}
}
if (isPrime) {
primes.push(i);
}
}
console.log(`워커: ${number}까지의 소수 계산 완료.`);
// 계산 결과를 다시 메인 스크립트로 전송
self.postMessage(primes);
};main.js 또는 HTML <script>)메인 페이지 스크립트에서 Worker 객체를 생성하고, postMessage()로 데이터를 주고받습니다.
// main.js (또는 HTML <script>)
const startButton = document.getElementById('startButton');
const resultDiv = document.getElementById('result');
const statusDiv = document.getElementById('status');
let myWorker;
startButton.addEventListener('click', () => {
statusDiv.textContent = "워커 시작 중... 잠시 기다려 주세요.";
// 이전에 워커가 있다면 종료
if (myWorker) {
myWorker.terminate();
myWorker = null;
}
// 새 워커 생성 (워커 스크립트 파일 경로 지정)
myWorker = new Worker('worker.js');
// 워커로부터 메시지를 받을 때 실행될 함수
myWorker.onmessage = function(event) {
const primes = event.data; // 워커로부터 전달받은 소수 배열
resultDiv.textContent = `계산된 소수 개수: ${primes.length}`;
statusDiv.textContent = "계산 완료! (메인 스레드는 블로킹되지 않았습니다.)";
myWorker.terminate(); // 작업이 끝나면 워커 종료
myWorker = null;
};
// 워커에서 에러 발생 시
myWorker.onerror = function(error) {
statusDiv.textContent = `워커 오류: ${error.message}`;
console.error("워커 오류:", error);
};
// 워커에게 계산을 시작하도록 메시지 전송
const numberToCalculate = 500000; // 큰 숫자
myWorker.postMessage(numberToCalculate);
statusDiv.textContent = `워커에게 ${numberToCalculate}까지의 소수 계산 명령을 보냈습니다. (UI는 반응 중)`;
});
// HTML에 버튼과 결과 표시 영역 추가
/*
<button id="startButton">복잡한 계산 시작 (워커 사용)</button>
<div id="status"></div>
<div id="result"></div>
*/이 예시에서 복잡한 소수 계산은 백그라운드 워커 스레드에서 실행되므로, 메인 스레드는 UI 업데이트나 사용자 이벤트 처리에 전혀 지장받지 않습니다.
계산 중에도 다른 UI 요소들을 조작할 수 있습니다.
워커 종료: worker.terminate()
워커 인스턴스의 terminate() 메서드를 호출하여 워커 스레드를 즉시 종료할 수 있습니다.
이는 더 이상 워커가 필요하지 않을 때 리소스 누수를 방지하는 데 중요합니다.
웹 워커는 계산을 분리해 주지만, 메인 스레드와 데이터를 주고받는 비용도 함께 생깁니다.
앞의 Main thread와 Web Worker 책임 경계 다이어그램은 postMessage, 구조화 복제, Transferable 객체, 작업 종료 기준을 함께 보여줍니다.
서비스 워커 (Service Workers)
서비스 워커는 웹 워커의 한 종류로, 웹 페이지와 네트워크 사이에서 프록시(Proxy) 역할을 하는 자바스크립트 파일입니다.
웹 페이지와 독립된 백그라운드에서 실행되며, 주로 다음 기능을 제공합니다.
- 오프라인 경험: 네트워크 연결 없이도 웹 애플리케이션을 동작하게 합니다.
- 캐싱: 네트워크 요청을 가로채서 캐시에서 리소스를 반환하여 웹 페이지 로딩 속도를 향상시킵니다.
- 푸시 알림: 사용자가 브라우저를 닫았거나 오프라인 상태일 때도 서버로부터 푸시 알림을 받을 수 있게 합니다.
- 백그라운드 동기화: 백그라운드에서 서버와 데이터를 동기화합니다.
서비스 워커는 Progressive Web Apps (PWA)의 핵심 기술이며, 웹 사이트를 모바일 앱처럼 동작하게 만드는 데 필수적입니다.
서비스 워커의 생명주기
STATE · REGISTRATION & VERSION TAKEOVER
Service Worker 설치·활성·제어 수명
register → installing → installed/waiting → activating → activated/controlling → redundant 상태와 waitUntil/clients.claim/update 경계
등록·설치·대기
registered → installing → installed: scope를 등록하고 install.waitUntil(precache)을 마칩니다. · 기존 controller가 있으면 새 worker는 waiting에서 clients 종료를 기다립니다.
활성·제어
activating → activated: activate.waitUntil(cleanup) 뒤 event-ready 상태가 됩니다. · 다음 navigation 또는 clients.claim() 뒤 controlling이 되어 fetch를 처리합니다.
Update·redundant
script bytes가 다르면 새 version이 별도로 설치되고 incumbent는 takeover 전까지 계속 제어합니다. · install/activate 실패 또는 새 version에 교체된 worker만 redundant가 됩니다.
installed state와 registration의 waiting slot, activated state와 실제 client control을 구분해야 업데이트가 언제 사용자에게 적용되는지 설명할 수 있다.
서비스 워커는 메인 스크립트와는 다른 복잡한 생명주기를 가집니다.
등록 (Registration): 웹 페이지에서 서비스 워커 파일을 등록합니다.
설치 (Installation): 서비스 워커 파일이 다운로드되고, install 이벤트가 발생합니다.
여기서 보통 필요한 애셋(asset)들을 캐시에 저장합니다.
활성화 (Activation): activate 이벤트가 발생하며, 이전 버전의 서비스 워커를 정리하고 새 서비스 워커가 제어권을 가져옵니다.
제어 (Controlling): 활성화된 서비스 워커는 이제 해당 출원의 네트워크 요청을 가로채고 처리할 수 있습니다.
종료 (Termination): 사용되지 않을 때는 메모리를 절약하기 위해 종료될 수 있으며, 필요할 때 다시 시작됩니다.
서비스 워커의 fetch 이벤트에서는 method, origin, destination을 확인한 뒤 캐시와 네트워크 중 어느 경로를 사용할지 정책으로 결정합니다.
서비스 워커 사용법 (기초)
sw.js)// sw.js
// 이 스크립트는 서비스 워커 스레드에서 실행됩니다.
const CACHE_NAME = 'my-pwa-cache-v1'; // 캐시 이름 정의
const urlsToCache = [ // 캐시할 파일 목록
'/',
'/index.html',
'/styles.css',
'/main.js',
'/logo.png' // 예시 이미지
];
// install 이벤트: 서비스 워커 설치 시점에 발생
self.addEventListener('install', (event) => {
console.log('[Service Worker] 설치 중...', event);
// 캐싱 작업 수행 (비동기)
event.waitUntil(
caches.open(CACHE_NAME) // 캐시 저장소 열기
.then((cache) => {
console.log('[Service Worker] 모든 리소스를 캐시합니다.');
return cache.addAll(urlsToCache); // 파일들을 캐시에 추가
})
);
});
// activate 이벤트: 서비스 워커 활성화 시점에 발생 (이전 워커 제거 등)
self.addEventListener('activate', (event) => {
console.log('[Service Worker] 활성화 중...', event);
event.waitUntil(
// 이전 버전의 캐시를 삭제하여 업데이트 처리
caches.keys().then((cacheNames) => {
return Promise.all(
cacheNames.map((cacheName) => {
if (cacheName !== CACHE_NAME) {
console.log('[Service Worker] 오래된 캐시 삭제:', cacheName);
return caches.delete(cacheName);
}
})
);
})
);
// 서비스 워커가 페이지를 즉시 제어하도록 설정
return self.clients.claim();
});
// fetch 이벤트: 네트워크 요청을 가로챔 (가장 중요!)
self.addEventListener('fetch', (event) => {
console.log('[Service Worker] Fetch 요청 가로챔:', event.request.url);
event.respondWith(
caches.match(event.request) // 요청이 캐시에 있는지 확인
.then((response) => {
// 캐시에 응답이 있으면 캐시된 응답 반환
if (response) {
console.log('[Service Worker] 캐시에서 응답 반환:', event.request.url);
return response;
}
// 캐시에 없으면 네트워크 요청
console.log('[Service Worker] 네트워크 요청:', event.request.url);
return fetch(event.request);
})
);
});main.js 또는 HTML <script>)메인 페이지 스크립트에서 서비스 워커를 등록합니다.
일반적으로 DOMContentLoaded 이벤트 이후에 등록합니다.
// main.js (또는 HTML <script>)
if ('serviceWorker' in navigator) { // 브라우저가 서비스 워커를 지원하는지 확인
window.addEventListener('load', () => {
navigator.serviceWorker.register('/sw.js') // 서비스 워커 파일 경로 지정
.then((registration) => {
console.log('Service Worker 등록 성공:', registration.scope);
})
.catch((error) => {
console.error('Service Worker 등록 실패:', error);
});
});
} else {
console.warn('이 브라우저는 Service Worker를 지원하지 않습니다.');
}이 코드를 배포하면, 사용자가 처음 웹사이트에 접속할 때 서비스 워커가 설치되고, 이후에는 네트워크 요청을 서비스 워커가 가로채 캐시에서 응답을 제공하거나 네트워크 요청을 수행할 수 있습니다.
이를 통해 웹 페이지 로딩 속도를 비약적으로 향상시키고, 심지어 네트워크 연결이 없어도 일부 기능을 사용할 수 있는 오프라인 경험을 제공할 수 있습니다.
서비스 워커의 활용 분야
- 오프라인 웹 애플리케이션: PWA의 핵심으로, 네트워크 연결 유무에 상관없이 동작하는 앱 구현.
- 빠른 로딩: 캐싱 전략을 통해 반복 방문 시 페이지 로딩 속도 극대화.
- 푸시 알림: 사용자에게 서버에서 보낸 푸시 메시지 전달.
- 백그라운드 데이터 동기화: 네트워크가 재연결될 때 자동으로 서버와 데이터 동기화.
- 리소스 미리 가져오기(Pre-caching): 사용자가 방문할 가능성이 있는 페이지의 리소스를 미리 캐싱.
웹 워커와 서비스 워커는 이름이 비슷하지만 적용 기준이 다릅니다.
CPU 계산은 Web Worker, 네트워크 중간자·캐시는 Service Worker, DOM 상호작용은 main thread 책임으로 분리합니다.
워커 기반 성능 개선 정리
핵심은 메인 스레드 반응성을 유지하면서 백그라운드 작업을 어떤 기준으로 분리할지 판단하는 것입니다.
이번 절에서는 웹 애플리케이션의 성능과 사용자 경험을 개선하는 두 가지 백그라운드 스크립트 기술인 웹 워커(Web Workers)와 서비스 워커(Service Workers)를 살펴봤습니다.
웹 워커는 복잡한 연산을 메인 스레드 밖에서 처리해 UI 반응성을 유지하도록 돕습니다.
postMessage() 기반 통신과 DOM 접근 불가라는 제약을 함께 기억해야 합니다.
이어서, 서비스 워커가 웹 워커의 개념을 확장하여 웹 페이지와 네트워크 사이에서 프록시 역할을 하며, 오프라인 지원, 캐싱, 푸시 알림 등 PWA의 핵심 기능을 구현하는 데 필수적임을 배웠습니다.
서비스 워커의 생명주기(등록, 설치, 활성화, 제어)와 install, activate, fetch 이벤트를 통한 캐싱 및 네트워크 요청 가로채기 방식의 기초를 알아보았습니다.
웹 워커와 서비스 워커는 PWA와 고성능 웹 애플리케이션에서 자주 쓰입니다.
서비스 워커는 실제 배포 환경에서 HTTPS를 요구하고 디버깅이 복잡할 수 있으므로, Chrome 개발자 도구의 Application 탭에서 상태와 캐시를 확인해야 합니다.
이로써 9장 웹 API와 브라우저 기능의 주요 내용을 마무리합니다.
최신 자바스크립트 기능과 브라우저 API를 함께 이해하면, UI 반응성, 저장, 통신, 백그라운드 처리까지 한 흐름으로 점검할 수 있습니다.
웹 워커와 서비스 워커는 모두 메인 스레드 밖에서 동작하지만, 해결하는 문제가 계산 분리와 네트워크 중간자라는 점에서 다릅니다.
웹 워커와 서비스 워커 절은 웹 워커, 웹 워커의 특징, 웹 워커 사용법 중심으로 웹 워커의 역할, 사용법, 적용 전 확인할 제약을 정리합니다.
웹 워커와 서비스 워커를 작업 위치, 생명주기, 캐시 책임 기준으로 정리합니다.