browser
서비스 워커와 PWA — 오프라인 웹의 원리
서비스 워커는 프록시 서버처럼 브라우저와 네트워크 사이에서 동작합니다. Push 알림, 오프라인 지원, 백그라운드 동기화 등 네이티브 앱 수준의 기능을 웹에 가져옵니다. "PWA를 구현해보셨나요?" "오프라인 캐싱 전략은 어떻게 선택하시나요?"는 점점 자주 나오는 면접 질문이며, Next.js의 next/pwa, Vite PWA 플러그인 등으로 대중화되고 있습니다.
학습 개요
탄생 배경
해결하려 했던 문제
웹은 기본적으로 온라인 의존적입니다. 네트워크가 끊기면 앱이 동작하지 않고, 매번 동일한 리소스를 서버에서 받아야 했습니다. 네이티브 앱은 오프라인 지원, Push 알림, 백그라운드 동기화를 제공했지만 웹은 불가능했습니다. AppCache(2012)가 시도했지만 설계 결함으로 2015년 deprecated됐습니다.
역사적 맥락
서비스 워커는 2014년 W3C 표준으로 시작됐습니다. Chrome 40(2015)에서 최초 지원. 2016년 iOS Safari를 제외한 대부분 브라우저 지원. 2018년 iOS 11.3에서 Safari가 서비스 워커 지원 시작으로 진정한 크로스 플랫폼 PWA가 가능해졌습니다. 2019년 Workbox(Google) 릴리즈로 서비스 워커 개발 생산성이 대폭 향상됐습니다.
이전에는 어떻게 했나
AppCache(deprecated), localStorage/sessionStorage(메모리 제한, 동기), IndexedDB(비동기 대용량 저장, 서비스 워커와 함께 사용). 서버가 없는 오프라인을 위해서는 서비스 워커가 유일한 표준 솔루션입니다. React Native, Capacitor, Electron은 완전히 다른 접근(네이티브 래퍼)입니다.
멘탈 모델
동작 원리
서비스 워커 특성: - 메인 스레드와 분리된 워커 스레드에서 실행 (DOM 접근 불가) - 이벤트 기반: install, activate, fetch, push, sync - HTTPS에서만 동작 (보안 요구사항, localhost 예외) - 브라우저 종료 후에도 백그라운드에서 실행 가능 - 스코프: 등록된 경로와 하위 경로를 제어 생명주기: 1. 등록(Registration): navigator.serviceWorker.register('/sw.js') 2. 설치(Install): install 이벤트 — 정적 자원 pre-cache 3. 활성화(Activate): activate 이벤트 — 이전 캐시 정리 4. 제어(Controlling): 페이지 fetch를 가로채기 시작 5. 업데이트: 새 sw.js 감지 시 대기(waiting) → 이전 버전 종료 후 활성화 캐싱 전략: - Cache First: 캐시 있으면 캐시 반환. 오프라인 지원 - Network First: 네트워크 먼저, 실패 시 캐시. 최신성 우선 - Stale-While-Revalidate: 캐시 즉시 반환 + 백그라운드 업데이트 - Network Only: 항상 네트워크 (API 요청) - Cache Only: 항상 캐시 (이미 pre-cache된 자원)
핵심 구성 요소
install 이벤트
초기 자원을 Cache API에 pre-cache. event.waitUntil()로 완료 보장
activate 이벤트
이전 버전 캐시 정리. clients.claim()으로 즉시 페이지 제어
fetch 이벤트
네트워크 요청을 가로채서 캐시/네트워크 전략 적용
Cache API
Request/Response 쌍을 저장하는 비동기 KV 스토어
push 이벤트
Web Push API — 서버에서 보낸 알림을 백그라운드에서 수신
Workbox
Google의 서비스 워커 라이브러리 — 캐싱 전략, 라우팅, pre-caching 추상화
흐름 설명
[서비스 워커 fetch 인터셉터 흐름]
브라우저 → [서비스 워커 fetch 이벤트] → 전략 결정
↓
┌───────────────────────┐
│ Cache First 전략 │
│ 1. Cache API 확인 │
│ 2. Hit? → 캐시 반환 │
│ 3. Miss? → 네트워크 │
│ 4. 응답을 캐시에 저장 │
└───────────────────────┘
[Stale-While-Revalidate]
1. 캐시 응답 즉시 반환 (빠름)
2. 백그라운드에서 네트워크 요청
3. 응답 받으면 캐시 업데이트 (다음 요청에 반영)
코드 예제
// sw.js — 기본 서비스 워커
const CACHE_NAME = 'v1';
const STATIC_ASSETS = ['/', '/index.html', '/styles.css', '/bundle.js'];
// install: 정적 자원 pre-cache
self.addEventListener('install', (event) => {
event.waitUntil(
caches.open(CACHE_NAME).then(cache => cache.addAll(STATIC_ASSETS))
);
self.skipWaiting(); // 대기 없이 즉시 activate
});
// activate: 이전 캐시 정리
self.addEventListener('activate', (event) => {
event.waitUntil(
caches.keys().then(keys =>
Promise.all(keys.filter(k => k !== CACHE_NAME).map(k => caches.delete(k)))
)
);
self.clients.claim();
});
// fetch: Stale-While-Revalidate 전략
self.addEventListener('fetch', (event) => {
if (!event.request.url.startsWith('http')) return;
event.respondWith(
caches.match(event.request).then(cached => {
const networkFetch = fetch(event.request).then(response => {
caches.open(CACHE_NAME).then(cache => cache.put(event.request, response.clone()));
return response;
});
return cached || networkFetch;
})
);
});
// 등록 (main.js)
if ('serviceWorker' in navigator) {
navigator.serviceWorker.register('/sw.js').then(reg => {
console.log('SW 등록:', reg.scope);
});
}
비교 분석
서비스
vsHTTP 캐시 (Cache-Control)
서비스
vsWeb Workers
트레이드오프
이상적인 사용 사례