웹 워커와 서비스 워커
우리는 9장에서 Fetch API로 서버 통신, 로컬/세션 스토리지로 클라이언트 데이터 저장, History API로 SPA 라우팅 구현, Canvas API로 웹 그래픽 처리 등 다양한 웹 API와 브라우저 기능들을 학습했습니다. 이제 웹 애플리케이션의 성능과 사용자 경험을 한 단계 더 끌어올릴 수 있는 고급 기술인 웹 워커(Web Workers)와 서비스 워커(Service Workers)에 대해 알아볼 차례입니다.
자바스크립트는 기본적으로 단일 스레드(Single-threaded) 언어입니다. 이는 웹 페이지의 UI 렌더링, 이벤트 처리, DOM 조작 등 모든 작업이 하나의 메인 스레드에서 순차적으로 실행된다는 의미입니다. 만약 이 메인 스레드에서 복잡하거나 시간이 오래 걸리는 연산(예: 대용량 데이터 처리, 이미지 필터링, 복잡한 애니메이션 계산)을 수행하게 되면, 해당 작업이 끝날 때까지 UI가 멈추거나 반응이 없어지는 블로킹(Blocking) 현상이 발생하여 사용자 경험을 크게 저해합니다.
웹 워커는 메인 스레드 블로킹 문제를 줄이기 위해 자바스크립트 코드를 별도 스레드에서 실행합니다. 이를 통해 계산 비용이 큰 작업을 UI 렌더링 흐름과 분리할 수 있습니다. 서비스 워커는 네트워크 요청 가로채기, 캐싱, 오프라인 응답 같은 기능을 담당하는 브라우저의 백그라운드 실행 단위입니다.
이 절에서는 웹 워커와 서비스 워커의 동작 원리, 기본 사용법, 그리고 활용 시나리오를 정리합니다. 두 기술은 Progressive Web Apps (PWA)의 핵심 구성 요소이기도 합니다.
웹 워커 (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() 메서드를 호출하여 워커 스레드를 즉시 종료할 수 있습니다. 이는 더 이상 워커가 필요하지 않을 때 리소스 누수를 방지하는 데 중요합니다.
웹 워커는 계산을 분리해 주지만, 메인 스레드와 데이터를 주고받는 비용도 함께 생깁니다.
아래 다이어그램은 postMessage, 구조화 복제, Transferable 객체, 작업 종료 기준을 한 번에 점검하는 흐름입니다.
서비스 워커 (Service Workers)
서비스 워커는 웹 워커의 한 종류로, 웹 페이지와 네트워크 사이에서 프록시(Proxy) 역할을 하는 자바스크립트 파일입니다. 웹 페이지와 독립된 백그라운드에서 실행되며, 주로 다음 기능을 제공합니다.
- 오프라인 경험: 네트워크 연결 없이도 웹 애플리케이션을 동작하게 합니다.
- 캐싱: 네트워크 요청을 가로채서 캐시에서 리소스를 반환하여 웹 페이지 로딩 속도를 향상시킵니다.
- 푸시 알림: 사용자가 브라우저를 닫았거나 오프라인 상태일 때도 서버로부터 푸시 알림을 받을 수 있게 합니다.
- 백그라운드 동기화: 백그라운드에서 서버와 데이터를 동기화합니다.
서비스 워커는 Progressive Web Apps (PWA)의 핵심 기술이며, 웹 사이트를 모바일 앱처럼 동작하게 만드는 데 필수적입니다.
서비스 워커의 생명주기
서비스 워커는 메인 스크립트와는 다른 복잡한 생명주기를 가집니다.
등록 (Registration): 웹 페이지에서 서비스 워커 파일을 등록합니다.
설치 (Installation): 서비스 워커 파일이 다운로드되고, install 이벤트가 발생합니다. 여기서 보통 필요한 애셋(asset)들을 캐시에 저장합니다.
활성화 (Activation): activate 이벤트가 발생하며, 이전 버전의 서비스 워커를 정리하고 새 서비스 워커가 제어권을 가져옵니다.
제어 (Controlling): 활성화된 서비스 워커는 이제 해당 출원의 네트워크 요청을 가로채고 처리할 수 있습니다.
종료 (Termination): 사용되지 않을 때는 메모리를 절약하기 위해 종료될 수 있으며, 필요할 때 다시 시작됩니다.
아래 다이어그램은 서비스 워커가 등록된 뒤 install, activate,
fetch 단계로 넘어가며 실제 요청을 캐시와 네트워크 중 어디로 보낼지
결정하는 흐름을 한 장으로 묶어 보여줍니다.
서비스 워커 사용법 (기초)
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 계산, 네트워크 캐싱, DOM 상호작용 중 어느 문제를 해결하려는지에 따라 선택 지점을 나누어 보여줍니다.
워커 기반 성능 개선 정리
핵심은 메인 스레드 반응성을 유지하면서 백그라운드 작업을 어떤 기준으로 분리할지 판단하는 것입니다. 이번 절에서는 웹 애플리케이션의 성능과 사용자 경험을 개선하는 두 가지 백그라운드 스크립트 기술인 웹 워커(Web Workers)와 서비스 워커(Service Workers)를 살펴봤습니다.
웹 워커는 복잡한 연산을 메인 스레드 밖에서 처리해 UI 반응성을 유지하도록 돕습니다. postMessage() 기반 통신과 DOM 접근 불가라는 제약을 함께 기억해야 합니다.
이어서, 서비스 워커가 웹 워커의 개념을 확장하여 웹 페이지와 네트워크 사이에서 프록시 역할을 하며, 오프라인 지원, 캐싱, 푸시 알림 등 PWA의 핵심 기능을 구현하는 데 필수적임을 배웠습니다. 서비스 워커의 생명주기(등록, 설치, 활성화, 제어)와 install, activate, fetch 이벤트를 통한 캐싱 및 네트워크 요청 가로채기 방식의 기초를 알아보았습니다.
웹 워커와 서비스 워커는 PWA와 고성능 웹 애플리케이션에서 자주 쓰입니다. 서비스 워커는 실제 배포 환경에서 HTTPS를 요구하고 디버깅이 복잡할 수 있으므로, Chrome 개발자 도구의 Application 탭에서 상태와 캐시를 확인해야 합니다.
이로써 9장 웹 API와 브라우저 기능의 주요 내용을 마무리합니다. 최신 자바스크립트 기능과 브라우저 API를 함께 이해하면, UI 반응성, 저장, 통신, 백그라운드 처리까지 한 흐름으로 점검할 수 있습니다.
웹 워커와 서비스 워커는 모두 메인 스레드 밖에서 동작하지만, 해결하는 문제가 계산 분리와 네트워크 중간자라는 점에서 다릅니다.
웹 워커와 서비스 워커 절은 웹 워커, 웹 워커의 특징, 웹 워커 사용법 중심으로 웹 워커의 역할, 사용법, 적용 전 확인할 제약을 정리합니다.
아래 다이어그램은 웹 워커 (Web Workers)에서 요청 흐름, 화면 전환, 작업 분리 지점을 확인합니다.
웹 워커와 서비스 워커를 작업 위치, 생명주기, 캐시 책임 기준으로 정리합니다.
다음 학습으로 넘어가기 전, 웹 워커와 서비스 워커에서 남은 개념 경계와 실습 확인 포인트를 점검합니다.