Service Worker?
Service Worker는 브라우저 백그라운드 스레드(background thread)로 동작한다.
즉, 메인 스레드(UI 렌더링)와는 별도로 작동.
웹페이지가 닫혀 있어도 네트워크 요청을 가로채거나 캐싱 제어를 할 수 있는 권한을 갖는다.
클라이언트와 네트워크 사이에서 프록시(Proxy) 역할을 수행한다.
Service Worker의 동작 구조
다음 3단계 생명주기를 갖는다.
install → activate → fetch
- install : 처음 등록될 때, 필요한 리소스를 캐시에 저장
- activate: 이전 버전의 캐시를 정리하고 새 워커 활성화
- fetch : 모든 네트워크 요청을 가로채 캐시 또는 네트워크로 응답
이 구조로 인해 한 번 방문한 페이지는 오프라인에서도 작동할 수 있는 로컬 앱처럼 작동한다.
등록(Registration)
먼저 클라이언트 측에서 서비스 워커를 등록해야 한다.
if ('serviceWorker' in navigator) {
window.addEventListener('load', () => {
navigator.serviceWorker
.register('/sw.js')
.then(() => console.log('Service Worker registered'))
.catch(console.error);
});
}
/sw.js 파일은 루트 경로에 위치해야 전체 사이트를 제어할 수 있다.
(하위 폴더에 있으면 해당 스코프 내에서만 동작)
install 단계 : 캐시 준비
Service Worker가 처음 설치될 때, 주요 정적 파일을 캐시에 미리 저장한다.
self.addEventListener('install', (event) => {
event.waitUntil(
caches.open('v1').then((cache) => {
return cache.addAll([
'/',
'/index.html',
'/styles.css',
'/main.js',
'/logo.png',
]);
})
);
});
- caches.open() : 캐시 스토리지에 새로운 버전 생성
- cache.addAll() : 지정한 자원들을 미리 다운로드 후 저장
- waitUntil() : 설치 완료 전 종료되지 않도록 대기
이 단계는 앱 설치 시 초기 리소스 다운로드와 같은 과정으로 볼 수 있다.
activate 단계 : 버전 관리 및 정리
Service Worker가 새로 설치되면 이전 캐시와 교체해야 한다.
이때 activate 이벤트 실행,
self.addEventListener('activate', (event) => {
const cacheWhitelist = ['v1'];
event.waitUntil(
caches.keys().then((keys) =>
Promise.all(
keys
.filter((key) => !cacheWhitelist.includes(key))
.map((key) => caches.delete(key))
)
)
);
});
이 과정들을 통해 이전 버전 캐시가 정리되고 업데이트 된 파일만 남는다.
fetch 단계 : 요청 가로채기
fetch 이벤트는 사용자가 페이지를 요청할 때마다 호출된다.
Service Worker는 이 요청을 가로채 캐시나 네트워크에서 응답을 선택할 수 있다.
e.g. Cache First + Network Fallback 전략
self.addEventListener('fetch', (event) => {
event.respondWith(
caches.match(event.request).then((cached) => {
return (
cached ||
fetch(event.request).then((response) => {
return caches.open('v1').then((cache) => {
cache.put(event.request, response.clone());
return response;
});
})
);
})
);
});
캐싱 전략
PWA의 성능과 오프라인 대응을 좌우하는 건 캐싱 전략이다.
전략은 상황에 따라 다르게 적용한다.
Cache First(캐시 우선)
한 번 저장한 건 그대로 쓴다. 네트워크는 보조, 즉시 로딩이 최우선
- 동작 흐름
- 캐시에 해당 요청이 있는지 확인
- 있으면 즉시 반환(네트워크 요청 안 함)
- 없으면 네트워크 요청 후 캐시에 저장
- 장점
- 가장 빠른 응답 속도(특히 이미지, 폰트, 정적 JS/CSS)
- 네트워크 불안정 시 강력함
- 장점
- 캐시 갱신이 어려움(오래된 리소스가 남을 수 있다)
- 사용 예시
- 앱 셸(App Shell) 구조의 JS/CSS
- 로고, 아이콘 등 변하지 않는 정적 자산
Network First(네트워크 우선)
항상 최신 데이터를 원한다. 네트워크가 실패하면, 캐시에서 백업한다.
- 동작 흐름
- 네트워크로 요청 시도
- 실패 시 캐시에서 가져온다.
- 장점
- 항상 최신 데이터 유지
- API 요청, 뉴스 피드 등 실시간 콘텐츠에 적합
- 단점
- 네트워크 지연이 크면 느려짐
- 오프라인에서는 첫 로드 실패
- 예시
- REST API, GraphQL 요청
- JSON 데이터, 동적 요청
Stale-While-Revalidate(캐시 우선 + 백그라운드 갱신)
사용자에겐 캐시를 즉시 보여주고, 뒤에서는 최신 데이터를 받아 업데이트.
- 동작 흐름
- 캐시에 있으면 즉시 반환
- 동시에 네트워크 요청으로 최신 데이터 가져오기
- 응답을 캐시에 덮어쓰기
- 장점
- 빠른 로딩 + 최신성 확보
- UX적으로 로딩 없이 즉시 뜨는 화면.
- 단점
- 캐시와 최신 데이터 불일치 시 순간적 차이 발생 가능.
- 사용 예시
- 블로그, 상품 리스트, 피드
- 자주 바뀌지만 실시간이 아닌 콘텐츠
Cache Only(캐시 전용)
네트워크는 무시하고, 오직 캐시만 본다.
- 동작 흐름
- 캐시에서만 자원을 찾음
- 없으면 실패 응답 반환
- 장점
- 완전한 오프라인 대응 가능
- 네트워크 불필요
- 단점
- 캐시 미스 시 실패(데이터 불일치 가능성 높음)
- 사용 예시
- 완전 오프라인 앱(설치 후 고정 콘텐츠)
- 내부 도움말, 튜토리얼 페이지)
Network Only(네트워크 전용)
항상 실시간, 캐시는 전혀 사용하지 않는다.
- 동작 흐름
- 요청을 그대로 네트워크로 보낸다.
- 캐시 관련 로직 없음
- 장점
- 항상 최신 데이터
- 캐시 동기화 불필요
- 단점
- 오프라인/느린 네트워크 환경에 매우 취약
- 사용 예시
- 로그인, 결제 인증 요청
- 민감 데이터(POST 요청, 토큰 등)
어떤 전략을 언제 쓸까?
| 상황 | 추천 전략 | 이유 |
|---|---|---|
| 정적 파일(JS/CSS/폰트) | Cache First | 변경 거의 없음, 속도 최우선 |
| API 요청, JSON 데이터 | Network First | 최신성 중요 |
| 콘텐츠 페이지(블로그, 피드) | Stale-While-Revalidate | 빠른 UX + 자동 업데이트 |
| 완전 오프라인 앱 | Cache Only | 네트워크 불필요 |
| 로그인/결제/POST 요청 | Network Only | 항상 실시간 통신 필요 |
하나의 전략만 사용하기 어려운 상황에서는?
실무에서는 하나의 전략만을 사용하지 않는다.
상황에 따라서, 또는 자원 유형별로 다르게 적용하는 것이 필요하다.
→ 혼합 전략 사용.
실무에서는?
캐시 버전 관리
캐시 이름에 버전을 붙여야 업데이트가 명확히 구분되고 관리하기 용이하다.
const CACHE_VERSION = 'v3';
const CACHE_NAME = `static-${CACHE_VERSION}`;
캐시 청소 주기 설정
이전 버전 캐시는 activate 이벤트에서 정리한다.
self.addEventListener('activate', (event) => {
event.waitUntil(
caches.keys().then((keys) =>
Promise.all(keys.filter((k) => k !== CACHE_NAME).map((k) => caches.delete(k)))
)
);
});
Workbox 적극 활용
Google이 만든 Workbox는 캐싱 전략, 버전 관리, 프리캐싱을 자동화한다.
importScripts('<https://storage.googleapis.com/workbox-cdn/releases/6.5.3/workbox-sw.js>');