Service Worker 심화 — 캐시 전략, 오프라인, 백그라운드 동기화
Service Worker는 브라우저 백그라운드에서 실행되는 스크립트로, 네트워크 요청을 가로채서 캐시 응답을 반환하거나, 오프라인 기능을 제공하거나, 푸시 알림을 처리합니다. PWA(Progressive Web App)의 핵심 기술입니다.
등록과 수명주기
// 메인 스레드에서 등록
if ("serviceWorker" in navigator) {
const registration = await navigator.serviceWorker.register("/sw.js", {
scope: "/",
});
console.log("SW 등록 성공:", registration.scope);
}
수명주기 이벤트
// sw.js
const CACHE_NAME = "v1";
// 1. install — 최초 설치 시
self.addEventListener("install", (event) => {
event.waitUntil(
caches.open(CACHE_NAME).then((cache) => {
return cache.addAll([
"/",
"/index.html",
"/styles.css",
"/app.js",
"/offline.html",
]);
})
);
self.skipWaiting(); // 대기 상태 건너뛰기
});
// 2. activate — 이전 SW 교체 시
self.addEventListener("activate", (event) => {
event.waitUntil(
caches.keys().then((names) => {
return Promise.all(
names
.filter((name) => name !== CACHE_NAME)
.map((name) => caches.delete(name)) // 이전 캐시 정리
);
})
);
self.clients.claim(); // 즉시 제어 시작
});
// 3. fetch — 네트워크 요청 가로채기
self.addEventListener("fetch", (event) => {
event.respondWith(
caches.match(event.request).then((cached) => {
return cached || fetch(event.request);
})
);
});
캐시 전략
Cache First (캐시 우선)
정적 자산(CSS, JS, 이미지)에 적합합니다.
self.addEventListener("fetch", (event) => {
event.respondWith(
caches.match(event.request).then((cached) => {
if (cached) return cached;
return fetch(event.request).then((response) => {
const clone = response.clone();
caches.open(CACHE_NAME).then((cache) => {
cache.put(event.request, clone);
});
return response;
});
})
);
});
Network First (네트워크 우선)
API 요청, 최신 데이터가 중요한 경우에 적합합니다.
self.addEventListener("fetch", (event) => {
event.respondWith(
fetch(event.request)
.then((response) => {
const clone = response.clone();
caches.open(CACHE_NAME).then((cache) => {
cache.put(event.request, clone);
});
return response;
})
.catch(() => {
return caches.match(event.request);
})
);
});
Stale-While-Revalidate (캐시 반환 + 백그라운드 갱신)
빠른 응답과 최신 데이터 모두 필요한 경우에 적합합니다.
self.addEventListener("fetch", (event) => {
event.respondWith(
caches.open(CACHE_NAME).then((cache) => {
return cache.match(event.request).then((cached) => {
const fetchPromise = fetch(event.request).then((response) => {
cache.put(event.request, response.clone());
return response;
});
return cached || fetchPromise;
});
})
);
});
전략 선택기
self.addEventListener("fetch", (event) => {
const { request } = event;
const url = new URL(request.url);
// API 요청 → Network First
if (url.pathname.startsWith("/api/")) {
event.respondWith(networkFirst(request));
return;
}
// 정적 자산 → Cache First
if (request.destination === "image" ||
request.destination === "style" ||
request.destination === "script") {
event.respondWith(cacheFirst(request));
return;
}
// HTML → Network First (오프라인 폴백)
if (request.mode === "navigate") {
event.respondWith(
fetch(request).catch(() => caches.match("/offline.html"))
);
return;
}
});
오프라인 페이지
// install 시 오프라인 페이지 캐시
self.addEventListener("install", (event) => {
event.waitUntil(
caches.open(CACHE_NAME).then((cache) => {
return cache.add("/offline.html");
})
);
});
// 네비게이션 실패 시 오프라인 페이지 표시
self.addEventListener("fetch", (event) => {
if (event.request.mode === "navigate") {
event.respondWith(
fetch(event.request).catch(() => {
return caches.match("/offline.html");
})
);
}
});
백그라운드 동기화
오프라인에서 수행한 작업을 온라인 복구 시 자동 전송합니다.
// 메인 스레드 — 동기화 등록
async function sendMessage(message) {
// IndexedDB에 저장
await saveToOutbox(message);
// 온라인이면 바로 전송, 오프라인이면 동기화 등록
const registration = await navigator.serviceWorker.ready;
await registration.sync.register("outbox-sync");
}
// sw.js — 동기화 이벤트 처리
self.addEventListener("sync", (event) => {
if (event.tag === "outbox-sync") {
event.waitUntil(
getOutboxMessages().then((messages) => {
return Promise.all(
messages.map((msg) =>
fetch("/api/messages", {
method: "POST",
body: JSON.stringify(msg),
}).then(() => removeFromOutbox(msg.id))
)
);
})
);
}
});
업데이트 처리
// 메인 스레드 — 새 버전 감지
navigator.serviceWorker.addEventListener("controllerchange", () => {
// 새 SW가 활성화됨 — 페이지 새로고침
window.location.reload();
});
// 업데이트 확인
const registration = await navigator.serviceWorker.register("/sw.js");
registration.addEventListener("updatefound", () => {
const newWorker = registration.installing;
newWorker.addEventListener("statechange", () => {
if (newWorker.state === "activated") {
showUpdateBanner("새 버전이 있습니다. 새로고침하시겠습니까?");
}
});
});
주의사항
// 1. HTTPS 필수 (localhost 제외)
// 2. DOM 접근 불가
// 3. 동기 API 사용 불가 (localStorage 등)
// 4. 요청 범위(scope) 주의
navigator.serviceWorker.register("/sw.js", {
scope: "/app/", // /app/ 하위 경로만 제어
});
캐시 전략 요약
| 전략 | 속도 | 최신성 | 적합한 대상 |
|---|---|---|---|
| Cache First | 빠름 | 낮음 | 정적 자산 (CSS, JS, 이미지) |
| Network First | 보통 | 높음 | API 데이터, HTML |
| Stale-While-Revalidate | 빠름 | 중간 | 자주 변경되는 자산 |
| Network Only | 보통 | 최신 | 인증, 결제 |
| Cache Only | 매우 빠름 | 없음 | 설치 시 캐시된 자산 |
**기억하기 **: Service Worker는 네트워크 프록시 역할을 합니다. 정적 자산은 Cache First, API는 Network First, 빠른 응답이 중요하면 Stale-While-Revalidate를 선택합니다. 오프라인 지원은 install 이벤트에서 필수 자원을 미리 캐시하면 됩니다.
댓글 로딩 중...