Service Worker는 브라우저 백그라운드에서 실행되는 스크립트로, 네트워크 요청을 가로채서 캐시 응답을 반환하거나, 오프라인 기능을 제공하거나, 푸시 알림을 처리합니다. PWA(Progressive Web App)의 핵심 기술입니다.

등록과 수명주기

JS
// 메인 스레드에서 등록
if ("serviceWorker" in navigator) {
  const registration = await navigator.serviceWorker.register("/sw.js", {
    scope: "/",
  });
  console.log("SW 등록 성공:", registration.scope);
}

수명주기 이벤트

JS
// 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, 이미지)에 적합합니다.

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 요청, 최신 데이터가 중요한 경우에 적합합니다.

JS
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 (캐시 반환 + 백그라운드 갱신)

빠른 응답과 최신 데이터 모두 필요한 경우에 적합합니다.

JS
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;
      });
    })
  );
});

전략 선택기

JS
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;
  }
});

오프라인 페이지

JS
// 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");
      })
    );
  }
});

백그라운드 동기화

오프라인에서 수행한 작업을 온라인 복구 시 자동 전송합니다.

JS
// 메인 스레드 — 동기화 등록
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))
          )
        );
      })
    );
  }
});

업데이트 처리

JS
// 메인 스레드 — 새 버전 감지
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("새 버전이 있습니다. 새로고침하시겠습니까?");
    }
  });
});

주의사항

JS
// 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 이벤트에서 필수 자원을 미리 캐시하면 됩니다.

댓글 로딩 중...