Service Worker, 너 도대체 뭐니?

재영·2025년 10월 17일
post-thumbnail

들어가며

MSWPWA를 사용할 때마다 ‘Service Worker’라는 말을 봤지만, 그 정체를 깊이 생각해 본 적은 없었습니다.

그러던 중 PWA 개발 과정에서 캐싱 문제를 겪으면서 “도대체 Service Worker가 뭐길래 서비스 전체에 영향을 주지?”라는 의문이 생겼습니다. 그저 백그라운드에서 도는 스크립트라고만 생각했는데, 실제로는 서비스 전반의 동작 방식까지 바꿀 수 있는 존재였던 것입니다.

그래서 이번 글에서는 Service Worker가 무엇인지, 어떤 원리로 동작하는지 차근차근 살펴보려 합니다.

대상 독자

  • Service Worker를 들어본 적은 있지만 정확히 어떤 역할을 하는지 모르는 분
  • Service Worker의 개념과 활용 방법을 더 깊이 이해하고 싶은 분

Service Worker란?

브라우저의 페이지와 네트워크 사이에서 요청을 가로채고 처리하는 독립 실행 환경(자바스크립트 파일).

Service Worker는 페이지의 JS와는 별도로 워커 컨텍스트에서 동작하며, 브라우저가 이벤트를 트리거할 때 실행됩니다. 이를 통해 네트워크 요청을 가로채서 원하는 응답을 반환하거나, 캐시를 이용해 오프라인 상태에서도 콘텐츠를 제공할 수 있습니다. PWA의 핵심 기술로서 푸시 알림과 백그라운드 동기화 같은 기능 구현에도 사용됩니다.

주요 특징

  • 이벤트 기반: install, activate, fetch, push 등 이벤트로 동작.
  • 비동기 중심: 대부분의 브라우저 API가 Promise 기반으로 동작.
  • 백그라운드 실행: UI와 독립적으로 동작(직접 DOM에 접근 불가).
  • HTTPS 필요: 보안상의 이유로 HTTPS 환경에서만 동작 (개발 시 localhost는 예외)

⚠️ Service Worker는 페이지와 네트워크 사이에서 요청을 가로채고 조작할 수 있기 때문에, 악의적인 중간자(Man-in-the-Middle)나 변조된 스크립트가 끼어들면 서비스 전체에 치명적인 영향을 줄 수 있다. 그래서 브라우저는 Service Worker를 HTTPS 환경에서만 동작하며, 개발 시 localhost는 예외다. 개발자는 서드파티 스크립트 검증과 안전한 배포 절차를 반드시 지켜야 한다.

Service Worker 등록하기

Service Worker는 단순한 JS 파일이 아니라, 브라우저가 별도로 인식하고 관리해야 하는 워커입니다. 따라서 일반 스크립트처럼 <script>로 불러오는 것이 아니라, 명시적으로 등록(register) 해야 브라우저가 이를 감지하고 관리할 수 있습니다.

Service Worker API를 통해서 브라우저에 Service Worker를 등록 가능합니다.

Service Worker 파일 생성

serviceWorker.js

self.addEventListener('install', function () {
  self.skipWaiting();
});

self.addEventListener('activate', function (event) {
  event.waitUntil(self.clients.claim());
});
  • Service Worker에서 실행할 기능을 정의하는 자바스크립트 파일입니다.
  • selfServiceWorkerGlobalScope를 가리키며, Service Worker의 전역 실행 환경입니다.
  • Service Worker는 window와는 별도의 독립 환경에서 동작하므로, 생명주기(install, activate)나 네트워크 이벤트(fetch, push 등)를 처리할 때는 self에 이벤트를 등록해야 합니다.

Service Worker 등록

const registerServiceWorker = async () => {
  if ("serviceWorker" in navigator) {
    try {
      const registration = await navigator.serviceWorker.register("/serviceWorker.js");
      if (registration.installing) {
        console.log("Service worker installing");
      } else if (registration.waiting) {
        console.log("Service worker installed");
      } else if (registration.active) {
        console.log("Service worker active");
      }
    } catch (error) {
      console.error(`Registration failed with ${error}`);
    }
  }
};

// …

registerServiceWorker();
  • navigator.serviceWorker.register를 사용해 브라우저에 Service Worker를 등록합니다.
  • 등록을 성공하면 installing, waiting, active 상태를 확인할 수 있습니다.
if (isProdunction) {
	registerPwaServiceWorker()
}

if (isDevelopment) {
	const { worker } = await import('./mocks/browser');
	worker.start()
}
  • Service Worker는 도메인당 하나만 등록할 수 있습니다.
  • 따라서 msw(Mock Service Worker)처럼 Service Worker를 활용하는 라이브러리를 함께 쓴다면, 환경별로 분기하여 충돌을 방지하는 것이 좋습니다.

💡 TIP: 등록된 Service Worker는 브라우저 개발자 도구에서 확인할 수 있습니다.

  • Application 탭 → Service Workers: 등록 상태(installing, waiting, activated)와 업데이트/중지 버튼 확인 가능
  • Sources 탭 → serviceWorker.js: 실제 등록된 스크립트를 디버깅 가능
  • chrome://inspect/#service-workers
    • 크롬에서 등록된 전체 Service Worker 목록을 확인할 수 있음
    • 다른 도메인에 등록된 워커까지 한 번에 점검 가능

Service Worker의 생명주기

1. Installing

브라우저가 Service Worker 파일을 다운로드하고 파싱한 뒤, 설치 중 상태가 됩니다.

  • 설치가 성공하면 Installed 상태로 넘어가고, 실패하면 Redundant 상태가 됩니다.
  • install 이벤트에서 event.waitUntil()을 사용하면 특정 비동기 작업(예: 캐시 저장)이 끝날 때까지 설치 과정을 지연시킬 수 있습니다.

2. Installed/waiting

설치가 완료되면 설치됨 상태가 됩니다.

  • 현재 앱을 제어하는 다른 Service Worker가 없다면 곧바로 Activating으로 넘어갑니다.
  • 기존 Service Worker가 이미 동작 중이라면 새 워커는 waiting 상태로 머무릅니다.

👉 새 버전의 Service Worker가 자동으로 바로 교체되지 않고, 기존 워커가 종료될 때까지 대기하는 이유는 사용자 경험을 보호하기 위함입니다.

3. Activating

설치된 워커가 앱을 제어하기 직전 상태입니다.

  • 이 단계에서 activate 이벤트가 발생합니다.
  • event.waitUntil()을 사용해 캐시 정리나 마이그레이션 같은 작업을 완료할 때까지 활성화를 지연시킬 수 있습니다.

4. Activated

Service Worker가 앱을 제어할 준비가 끝난 상태입니다.

  • 이 시점부터 네트워크 요청을 가로채 fetch 이벤트를 처리하거나, push 이벤트를 받아 푸시 알림을 보낼 수 있습니다.
  • 사실상 실제 기능이 동작하는 단계입니다.

5. Redundant

Service Worker가 더 이상 쓰이지 않게 된 상태입니다.

  • 설치 실패, 혹은 새로운 버전의 Service Worker가 교체되었을 때 발생합니다.
  • 이 상태의 워커는 앱 동작에 영향을 주지 않습니다.

Service Worker 이벤트 소개

ServiceWorkerGlobalScope에는 다양한 이벤트가 정의되어 있습니다. 그중 자주 쓰이는 이벤트를 정리하면 다음과 같습니다.

생명주기 관련

install

  • Service Worker가 설치될 때 발생합니다.
  • 초기 리소스 캐싱 같은 설치 준비 작업을 수행합니다.
  • 이 이벤트는 취소할 수 없으며, 다른 이벤트로 대체되지 않습니다.
self.addEventListener('install', function (event) {
  // 기존 활성화된 워커가 있어도 곧바로 새 워커를 활성화
  self.skipWaiting();

  // 캐시에 필요한 리소스를 미리 저장 가능
  event.waitUntil(
    caches
    .open("v1")
    .then((cache) =>
          cache.addAll([
      "/",
      "/index.html",
      "/style.css",
      "/app.js",
      "/image-list.js",
      "/star-wars-logo.jpg",
      "/gallery/",
      "/gallery/bountyHunters.jpg",
      "/gallery/myLittleVader.jpg",
      "/gallery/snowTroopers.jpg",
    ]),
         ),
  );
});

activate

  • 새 Service Worker가 활성화 직전에 발생합니다.
  • 이전 버전의 캐시를 정리하거나, 마이그레이션 작업을 수행합니다.
self.addEventListener("activate", (event) => {
  const cacheAllowlist = ["v2"];

  event.waitUntil(
    caches.keys().then((cacheNames) =>
                       Promise.all(
      cacheNames.map((cacheName) => {
        if (!cacheAllowlist.includes(cacheName)) {
          return caches.delete(cacheName);
        }
        return undefined;
      }),
    ),
                      ),
  );
});

네트워크 / 메시지 관련

fetch

  • fetch() 메서드가 호출될 경우 발생
self.addEventListener("fetch", (event) => {
  const { method, url } = event.request;

  // 특정 요청 가로채기
  if (method === "GET" && url.endsWith("/hello")) {
    event.respondWith(
      new Response(
        JSON.stringify({ message: "Hello from Service Worker!" }),
        { headers: { "Content-Type": "application/json" } }
      )
    );
  }
});

message

  • 페이지에서 postMessage()를 사용해 Service Worker로 데이터를 보낼 때 발생합니다.
  • 페이지 ↔ Service Worker 간 양방향 통신에 활용됩니다. 예를 들어, 페이지에서 요청한 데이터를 워커가 가공해서 다시 응답할 수 있습니다.
// serviceWorker.js
self.addEventListener("message", (event) => {
  console.log("Message from page:", event.data);

  // 받은 메시지에 따라 응답을 보낼 수도 있음
  event.source.postMessage({
    reply: `Echo: ${event.data}`,
  });
});
// main.js (페이지 스크립트)
navigator.serviceWorker.controller.postMessage("Hello SW!");

// Service Worker가 응답 보낸 걸 수신
navigator.serviceWorker.addEventListener("message", (event) => {
  console.log("Reply from SW:", event.data.reply);
});

messageerror

  • 메시지 전송 도중 직렬화(serialize)할 수 없는 데이터나 전송 불가능한 객체를 보냈을 때 발생합니다.
  • 메시지 통신 실패를 감지하고 로깅하거나 대체 동작을 수행합니다.
// serviceWorker.js
self.addEventListener("messageerror", (event) => {
  console.error("Message failed:", event);
});
// main.js
try {
  // 직렬화 불가능한 데이터(예: 함수)를 보내면 오류 발생
  navigator.serviceWorker.controller.postMessage(() => {});
} catch (err) {
  console.error("Message send failed:", err);
}

푸시 / 알림 관련

push

  • 서버에서 푸시 메시지를 보냈을 때 발생합니다.
  • 알림(Notification API)을 띄우는 데 주로 사용됩니다.
self.addEventListener("push", (event) => {
  const data = event.data?.json() || { title: "Default title" };

  event.waitUntil(
    self.registration.showNotification(data.title, {
      body: data.body || "Hello from Service Worker!",
      icon: "/icon.png",
    })
  );
});

pushsubscriptionchange

  • 브라우저가 푸시 구독을 만료하거나 변경했을 때 발생합니다.
  • 새로운 구독 정보를 서버에 업데이트해야 합니다.
self.addEventListener("pushsubscriptionchange", (event) => {
  event.waitUntil(
    self.registration.pushManager.subscribe({ userVisibleOnly: true })
    .then((subscription) => {
      // 새로운 구독 정보를 서버로 전송
      return fetch("/update-subscription", {
        method: "POST",
        body: JSON.stringify(subscription),
      });
    })
  );
});

notificationclick

  • 사용자가 알림을 클릭했을 때 발생합니다.
  • 특정 URL을 열거나 포커스를 맞출 수 있습니다.
self.addEventListener("notificationclick", (event) => {
  event.notification.close();

  event.waitUntil(
    clients.openWindow("/welcome") // 특정 경로 열기
  );
});

notificationclose

  • 사용자가 알림을 닫았을 때 발생합니다.
  • 통계 수집, 서버 로깅 등에 활용할 수 있습니다.
self.addEventListener("notificationclose", (event) => {
  console.log("Notification closed:", event.notification.tag);
});

기타

sync

  • 네트워크가 다시 연결되었을 때 브라우저가 백그라운드에서 동기화 작업을 수행할 수 있을 때 발생합니다.
  • 오프라인 상태에서 실패한 요청(예: 서버에 저장하지 못한 데이터)을 네트워크가 복구되면 다시 전송하는 데 유용합니다.
// serviceWorker.js
self.addEventListener("sync", (event) => {
  if (event.tag === "sendFormData") {
    event.waitUntil(
      fetch("/submit", {
        method: "POST",
        body: JSON.stringify({ message: "retry after offline" }),
        headers: { "Content-Type": "application/json" },
      })
    );
  }
});
// main.js (페이지)
navigator.serviceWorker.ready.then((registration) => {
  return registration.sync.register("sendFormData");
});

마무리하며

이번 글에서는 Service Worker의 기본 개념부터 생명주기, 그리고 주요 이벤트까지 살펴보았습니다.
Service Worker는 PWA의 핵심 기술이지만, 개념만 보면 추상적으로 느껴지기 쉽습니다.
중요한 것은 언제 어떤 이벤트가 발생하고, 그 이벤트 안에서 무엇을 할 수 있는지를 이해하는 것입니다.

기억해 두면 좋은 포인트

  • Service Worker는 브라우저와 네트워크 사이의 독립 실행 환경이다.
  • HTTPS 환경에서만 동작하며(localhost 예외), 이는 보안상 필수 제약이다.
  • 한 도메인에는 하나의 Service Worker만 등록 가능하다.
  • 생명주기(install → waiting → activate → activated → redundant)를 이해하자.
  • Service Worker는 잘못 설계하면 서비스 전체에 영향을 주므로, 신중하게 설계해야 한다.

참고자료

2개의 댓글

comment-user-thumbnail
2025년 10월 18일

좋은데요?

1개의 답글