서비스 워커는 재방문 시 연결 문제에 더 잘 대응하고 반복되는 네트워크 작업을 줄일 수 있습니다. 그러나 등록하는 것만으로 Core Web Vitals가 개선되지는 않습니다. 어떤 요청을 처리할지, 응답이 얼마나 최신이어야 하는지, 새 버전을 배포할 때 어떻게 전환할지를 먼저 정해야 합니다. 공개 정적 리소스 중 제한된 범위부터 시작하세요. 테스트에서 유지 관리 비용을 감수할 만한 효과가 확인될 때 범위를 넓히는 것이 좋습니다.
워커와 캐시는 서로 다른 구성 요소입니다
서비스 워커는 자신이 제어하는 페이지의 요청을 가로챌 수 있는 이벤트 기반 스크립트입니다. Cache Storage는 요청과 응답의 쌍을 보관하는 별도의 저장소입니다. 어느 쪽도 브라우저의 HTTP 캐시를 대체하지 않습니다. 워커가 가져오는 응답에도 일반 HTTP 캐시가 관여할 수 있으므로, 빨라진 결과를 코드의 효과로 판단하기 전에 실제 응답을 제공한 계층을 확인해야 합니다.
Cache Storage는 HTTP의 최신성 규칙을 자동으로 적용하지 않습니다. 언제 저장하고 갱신하며 삭제할지는 애플리케이션이 결정합니다. 서버 응답 헤더가 이 정책을 대신해 주지 않습니다. 공개 스타일시트나 이미지를 재사용하는 것이 목적이라면 프로그래밍 가능한 계층을 추가하기 전에 기존 브라우저 캐시 설정부터 살펴보세요.
리소스마다 전략을 선택하세요
내용을 식별하는 지문이 URL에 포함된 공개 파일이라면 네트워크보다 캐시를 먼저 확인하는 전략이 합리적인 출발점입니다. URL이 그대로라면 내용도 같다는 전제가 성립하기 때문입니다. 계정 정보, 재고 상태, 새 배포 내용을 반영해야 하는 HTML에는 이 논리를 그대로 적용할 수 없습니다.
네트워크를 먼저 확인하면 최신 응답을 얻는 데 유리하지만 연결이 불안정할 때 사용자가 오래 기다릴 수 있습니다. 취소 조건, 대체 응답, 실패 처리를 명확히 정하세요. 저장된 응답을 즉시 제공하면서 백그라운드에서 갱신하는 방식은 빠른 재사용에 유리하지만 현재 방문자는 이전 내용을 볼 수 있습니다. 캐시가 있어도 네트워크 작업이 발생한다는 점도 고려해야 합니다.
이 선택은 제품의 요구사항과 연결됩니다. 오프라인 도움말은 유용할 수 있지만 오래된 결제 확인 내용을 보여 주면 사용자가 오해할 수 있습니다. 모든 요청이 같은 핸들러를 통과한다고 해서 동일한 캐시 전략을 적용할 이유는 없습니다.
첫 방문도 함께 고려하세요
등록, 설치, 활성화, 페이지 제어는 서로 다른 상태입니다. 새로 등록한 워커는 일반적으로 자신을 등록한 최초 페이지를 바로 제어하지 않습니다. 준비가 끝난 후의 재방문은 혜택을 받을 수 있지만 신규 방문자는 여전히 일반적인 로딩 경로에 의존합니다.
과도한 사전 캐싱은 현재 페이지에 필요한 리소스와 경쟁할 수 있습니다. 제공하려는 경험에 필요한 항목만 준비하세요. 서버 응답, 리소스 발견, 렌더링 경로는 별도로 개선해야 합니다. 이미 캐시가 채워진 방문이 빠르게 보이더라도 서버 응답 시간 개선 안내는 여전히 중요합니다.
범위가 분명한 예제로 시작하세요
다음 예제 워커는 명시적으로 나열한 공개 파일을 미리 저장하고, 일치하는 요청에 가능한 경우 캐시 응답을 제공합니다. 워커 스크립트에 넣고 보안 컨텍스트에서 별도로 등록하세요. 경로는 실제 파일로 바꿔야 합니다. 목록에 포함한 응답은 공개되어 있어야 하며 로그인 여부에 따라 달라져서는 안 됩니다.
const CACHE = "editorialAssetsRelease";
const ASSETS = [
"/assets/site.abc123.css",
"/assets/brand.def456.svg"
];
self.addEventListener("install", event => {
event.waitUntil(
caches.open(CACHE).then(cache => cache.addAll(ASSETS))
);
});
self.addEventListener("fetch", event => {
const request = event.request;
const url = new URL(request.url);
if (request.method !== "GET" ||
url.origin !== self.location.origin ||
!ASSETS.includes(url.pathname + url.search)) return;
event.respondWith((async () => {
let cached;
try {
const cache = await caches.open(CACHE);
cached = await cache.match(request);
} catch {}
return cached || fetch(request);
})());
});새 배포에서는 캐시 이름과 내용 지문이 들어간 리소스 URL을 함께 변경하세요. 필수 사전 캐싱 요청이 실패하면 설치도 실패할 수 있습니다. 이 예제에는 HTML 대체 페이지, 실행 중 계속 커지는 캐시, 업데이트 직후 자동 제어 전환이 없습니다. 범위를 명확히 하기 위한 구성입니다. 실제 적용에는 등록, 업데이트 처리, 이 애플리케이션이 소유한 캐시를 정리하는 정책이 추가로 필요합니다.
열린 탭과 호환되는 업데이트를 설계하세요
새로 설치한 워커는 기존 워커가 현재 클라이언트를 처리하는 동안 대기할 수 있습니다. 이 동작은 이전 페이지와 호환되지 않는 새 리소스가 섞이는 일을 줄여 줍니다. skipWaiting()과 clients.claim()은 생명주기와 페이지 제어 방식을 바꿉니다. 일반적인 속도 개선 기법처럼 무조건 추가해서는 안 됩니다.
업데이트 안내를 표시할지, 자연스러운 새로고침을 기다릴지, 호환되는 버전을 함께 지원할지 결정하세요. 기존 페이지에서 요청할 수 있는 동안은 이전 지문 URL의 파일을 유지해야 합니다. 애플리케이션이 소유한 캐시만 삭제하고 현재 사용 중인 클라이언트를 보존 정책에 반영하세요. 전환 설계에는 서비스 워커 생명주기 문서를 참고할 수 있습니다.
비공개 콘텐츠와 저장소 손실을 고려하세요
별도로 검토한 설계에서 요구하지 않는 한 인증, 계정 페이지, 민감한 API 응답은 캐싱 대상에서 제외하세요. 성공한 요청을 모두 저장하는 방식보다 허용 URL 목록을 명시하는 방식이 검토하기 쉽습니다. 쿠키나 응답 헤더가 있다는 이유로 포괄적인 Cache Storage 처리 로직이 안전하다고 가정해서는 안 됩니다.
사용자별 콘텐츠를 의도적으로 저장한다면 로그아웃, 계정 전환, 삭제 절차를 함께 설계해야 합니다. 브라우저가 저장된 리소스를 제거하는 경우도 예상하세요. 오프라인 지원은 어떤 기능을 계속 쓸 수 있고 무엇에 연결이 필요한지 알려 주어야 합니다. 데이터가 없는데도 작업이 성공한 것처럼 표시해서는 안 됩니다.
캐시가 채워진 새로고침 외의 상태도 검증하세요
새 방문, 워커가 제어하고 저장소가 채워진 방문, 오프라인 방문, 이전 탭을 열어 둔 업데이트를 비교하세요. 워커의 범위와 컨트롤러, Cache Storage의 실제 항목, 응답 출처를 확인해야 합니다. 개발자 도구에서 HTTP 캐시를 끄는 것만으로 Cache Storage까지 우회했다고 볼 수는 없습니다.
느린 동작은 성능 기록으로 조사하고 방문자에게 미친 영향은 필드 데이터로 검증하세요. 양호 기준은 LCP 2.5 s 이하, INP 200 ms 이하, CLS 0.1 이하이며 모바일과 데스크톱을 구분한 75번째 백분위수로 평가합니다. 로컬에서 반복 방문이 빠르다는 사실만으로 실제 사용자 분포가 이 기준을 충족한다고 판단할 수는 없습니다.
효과가 명확할 때 서비스 워커를 유지하세요. 올바른 콘텐츠, 예측 가능한 업데이트, 안정적인 복구도 성능 설계의 일부입니다. 필드 데이터와 실험실 데이터 안내를 활용하면 서로 다른 관찰을 적절한 맥락에서 해석할 수 있습니다.