정의
Service Worker(페이지와 네트워크 사이에서 요청을 가로채는 백그라운드 스크립트)에 오프라인 대비 "셸 폴백"을 넣을 때, 네트워크가 실패하면 앱 껍데기 HTML(예: /)을 대신 돌려주도록 짜기 쉽다. 하지만 이 폴백을 모든 요청에 걸면, 이미지·API 요청이 실패했을 때도 HTML을 반환해 <img>가 깨지고 blob이 셸 캐시에 무한 적재된다. 셸 폴백은 반드시 request.mode === 'navigate' 인 문서 이동 요청에만 걸어야 한다.
셸(app shell) 은 화면 골격을 담은 최소 HTML, request.mode 는 그 요청이 페이지 이동인지(navigate) 이미지·스크립트 같은 하위 리소스인지 구분하는 표식이다.
왜 필요한가
증상: 사진이 깨져 보이고, 시간이 지날수록 브라우저 저장소가 알 수 없는 데이터로 부풀어 오른다. 원인은 SW가 모든 동일 출처 GET을 가로채 실패 시 셸("/")로 폴백하게 짜인 것이다. 사진 요청이 네트워크 실패로 셸 폴백을 타면, <img>가 이미지 대신 HTML 문서를 받아 렌더에 실패한다. 게다가 그 HTML이 사진 URL 키로 캐시에 쌓여 저장소가 오염된다.
문서 이동(페이지 로드)만 셸로 폴백하면, 이미지·API·정적 자산은 네트워크 결과(또는 각자의 캐시 전략)를 그대로 받아 이런 오염이 사라진다.
동작 원리
fetch 이벤트:
if (request.mode === 'navigate') {
// 문서 이동만: 네트워크 실패 시 셸 HTML 폴백
try { return await fetch(request); }
catch { return caches.match('/'); }
}
// 이미지·API·정적 자산: 셸 폴백 금지 (각자 전략 또는 네트워크 직행)
return fetch(request);
핵심은 "셸은 문서 요청의 폴백일 뿐"이라는 것이다. 이미지·API에는 각자의 정책(예: 이미지엔 stale-while-revalidate 캐시, API는 네트워크 직행)을 따로 준다.
실무 적용
- SW 캐시 전략을 요청 종류별로 분기한다:
navigate→ 셸 폴백, 이미지 GET → SWR 캐시, API → 네트워크(+오프라인 큐). - 기존에 오염된 캐시가 있으면 캐시 버전을 올려(
v1→v2) 과거 항목을 purge한다. - 셸 폴백을 넣기 전에 "이 폴백이 문서가 아닌 요청에도 걸리나?"를 반드시 확인한다.
트레이드오프
- navigation 한정 셸 폴백: 오프라인에서도 페이지 골격은 뜨고, 이미지·API는 깨진 HTML 대신 정상 실패(빈 이미지/에러)로 처리된다. 대신 요청 종류별 분기를 직접 짜야 한다.
- 전체 셸 폴백(안티패턴): 코드는 짧지만 이미지 깨짐 + 캐시 오염을 부른다.
사용하면 안 되는 경우
- SW를 아예 쓰지 않는 앱 — 서버 CDN·HTTP 캐시로 충분한지 먼저 본다.
- 오프라인 문서 표시가 필요 없는 경우 — 셸 폴백 자체가 불필요.
흔한 실수
- 모든 GET에 셸 폴백을 걸어 이미지·API 실패에 HTML 반환.
- 폴백으로 받은 HTML이 리소스 URL 키로 캐시에 적재돼 저장소 오염.
- 캐시 오염을 고친 뒤 캐시 버전을 안 올려 과거 오염분이 남음.
관련 개념
- service-worker-stale-while-revalidate — 이미지 등 하위 리소스의 캐시 전략
- server-image-proxy-transcoding-cache — 프록시 이미지 URL과 캐시 역할 분담