Service Worker API

김동현·2026년 3월 21일

Service Worker API

📝 참고: 이 기능은 Web Workers에서도 사용할 수 있어요.

서비스 워커는 본질적으로 웹 애플리케이션, 브라우저, 그리고 네트워크(사용 가능한 경우) 사이에 위치한 프록시 서버처럼 작동해요. 이들은 무엇보다도 효과적인 오프라인 경험을 만들고, 네트워크 요��을 가로채고, 네트워크가 사용 가능한지 여부에 따라 적절한 조치를 취하고, 서버에 있는 자산들을 업데이트할 수 있도록 하기 위해 의도되었어요. 또한 푸시 알림과 백그라운드 동기화 API에 대한 접근을 허용할 거예요.

📝 참고:
서비스 워커는 웹 워커의 한 종류예요. 워커 타입과 사용 사례에 대한 일반적인 정보는 Web workers를 참고하세요.


📑 이 문서의 내용


Service worker concepts and usage

서비스 워커는 출처(origin)와 경로(path)에 대해 등록된 이벤트 기반 worker예요. JavaScript 파일의 형태를 취하며, 연결된 웹 페이지/사이트를 제어하고, 탐색과 리소스 요청을 가로채고 수정하며, 리소스를 매우 세밀하게 캐싱하여 특정 상황(가장 명백한 것은 네트워크를 사용할 수 없을 때)에서 앱이 어떻게 동작할지 완벽하게 제��할 수 있게 해줘요.

서비스 워커는 워커 컨텍스트에서 실행돼요: 따라서 DOM에 접근할 수 없고 앱을 구동하는 메인 JavaScript와는 다른 스레드에서 실행돼요. 논블로킹(non-blocking)이며 완전히 비동기적으로 설계되어 있어요. 결과적으로, 동기적 XHR이나 Web Storage 같은 API는 서비스 워커 내부에서 사용할 수 없어요.

서비스 워커는 JavaScript 모듈을 동적으로 임포트할 수 없고, 서비스 워커 전역 스코프에서 import()가 호출되면 에러를 던져요. import 문을 사용한 정적 임포트는 허용돼요.

서비스 워커는 보안 컨텍스트(secure contexts)에서만 사용할 수 있어요: 이는 문서가 HTTPS를 통해 제공되어야 한다는 의미예요. 다만 브라우저는 로컬 개발을 용이하게 하기 위해 http://localhost도 보안 컨텍스트로 취급해요. HTTP 연결은 중간자 공격(man in the middle)에 의한 악의적인 코드 주입에 취약하고, 이러한 강력한 API에 접근이 허용되면 그런 공격이 더 나빠질 수 있어요.

📝 참고:
Firefox에서 테스트 목적으로는 HTTP를 통해(안전하지 않게) 서비스 워커를 실행할 수 있어요. Firefox DevTools 옵션/기어 메뉴에서 Enable Service Workers over HTTP (when toolbox is open) 옵션을 체크하면 돼요.

📝 참고:
AppCache 같은 이 분야의 이전 시도들과 달리, 서비스 워커는 여러분이 하려는 것에 대해 가정하지 않고, 그 가정들이 정확히 맞지 않을 때 망가지지 않아요. 대신, 서비스 워커는 훨씬 더 세밀한 제어를 제공해요.

📝 참고:
서비스 워커는 promises를 많이 사용해요. 일반적으로 응답이 들어오기를 기다린 후 성공 또는 실패 액션으로 응답하기 때문이에요. Promise 아키텍처는 이것에 이상적이에요.

Registration

서비스 워커는 먼저 ServiceWorkerContainer.register() 메서드를 사용하여 등록돼요. 성공하면, 서비스 워커는 클라이언트에 다운로드되고 전체 출처 내에서 사용자가 접근하는 URL들에 대해, 또는 여러분이 지정한 부분집합에 대해 설치/활성화(아래 참조)를 시도할 거예요.

Download, install and activate

이 시점에서, 서비스 워커는 다음 생명주기를 따라요:

  1. Download (다운로드)
  2. Install (설치)
  3. Activate (활성화)

서비스 워커는 사용자가 서비스 워커로 제어되는 사이트/페이지에 처음 접근할 때 즉시 다운로드돼요.

그 후에는 다음과 같은 경우에 업데이트돼요:

  • 스코프 내 페이지로의 탐색이 발생했을 때.
  • 서비스 워커에서 이벤트가 발생했고 지난 24시간 동안 다운로드되지 않았을 때.

다운로드된 파일이 새로운 것으로 확인되면 설치가 시도돼요 — 기존 서비스 워커와 다르거나(바이트 단위로 비교), 이 페이지/사이트에서 처음 만난 서비스 워커일 때요.

서비스 워커를 처음 사용할 수 있게 된 경우라면, 설치가 시도되고, 설치가 성공하면 활성화돼요.

기존 서비스 워커가 사용 가능한 경우, 새 버전은 백그라운드에서 설치되지만 아직 활성화되지는 않아요 — 이 시점에서 대기 중인 워커(worker in waiting)라고 불려요. 오래된 서비스 워커를 여전히 사용하는 페이지가 더 이상 로드되지 않을 때만 활성화돼요. 더 이상 로드할 페이지가 없자마자, 새 서비스 워커가 활성화돼요(활성 워커(active worker)가 되는 거죠). ServiceWorkerGlobalScope.skipWaiting()을 사용하면 활성화가 더 빨리 일어날 수 있고, 기존 페이지들은 Clients.claim()을 사용하여 활성 워커에 의해 클레임될 수 있어요.

install 이벤트를 리슨할 수 있어요. 표준 액션은 이것이 발생할 때 서비스 워커를 사용할 준비를 하는 거예요. 예를 들어, 내장 스토리지 API를 사용하여 캐시를 만들고, 앱을 오프라인으로 실행하는 데 필요한 자산들을 그 안에 배치하는 것이죠.

activate 이벤트도 있어요. 이 이벤트가 발생하는 시점은 일반적으로 오래된 캐시와 이전 버전의 서비스 워커와 관련된 다른 것들을 정리하기에 좋은 시간이에요.

서비스 워커는 FetchEvent 이벤트를 사용하여 요청에 응답할 수 있어요. FetchEvent.respondWith() 메서드를 사용하여 이러한 요청에 대한 응답을 원하는 대로 수정할 수 있어요.

📝 참고:
install/activate 이벤트가 완료되는 데 시간이 걸릴 수 있기 때문에, 서비스 워커 스펙은 waitUntil() 메서드를 제공해요. promise와 함께 install이나 activate 이벤트에서 호출되면, fetchpush 같은 기능적 이벤트들은 promise가 성공적으로 resolve될 때까지 기다려요.

완전한 튜토리얼로 첫 번째 기본 예제를 구축하는 방법을 보려면, Using Service Workers를 읽어보세요.

Using static routing to control how resources are fetched

서비스 워커는 불필요한 성능 비용을 초래할 수 있어요 — 페이지가 한동안 처음 로드될 때, 브라우저는 서비스 워커가 시작되고 실행되어 어떤 콘텐츠를 로드할지, 그리고 캐시에서 가져올지 네트워크에서 가져올지 알 때까지 기다려야 해요.

특정 콘텐츠를 어디서 가져와야 하는지 미리 알고 있다면, 서비스 워커를 완전히 우회하고 리소스를 즉시 가져올 수 있어요. InstallEvent.addRoutes() 메서드를 사용하여 이 사용 사례 등을 구현할 수 있어요.


Other use case ideas

서비스 워커는 또한 다음과 같은 것들을 위해 사용되도록 의도되었어요:

  • 백그라운드 데이터 동기화.
  • 다른 출처로부터의 리소스 요청에 응답하기.
  • 위치정보나 자이로스코프 같은 계산 비용이 높은 데이터에 대한 중앙집중식 업데이트를 받아서, 여러 페이지가 하나의 데이터 세트를 사용할 수 있도록 하기.
  • 개발 목적으로 CoffeeScript, less, CJS/AMD 모듈 등의 클라이언트 측 컴파일과 의존성 관리.
  • 백그라운드 서비스를 위한 훅(hooks).
  • 특정 URL 패턴에 기반한 커스텀 템플릿.
  • 성능 향상, 예를 들어 사용자가 곧 필요할 것 같은 리소스를 미리 가져오기 (사진 앨범의 다음 몇 장의 사진 같은 것).
  • API 모킹.

미래에는, 서비스 워커가 웹 플랫폼을 네이티브 앱 실행 가능성에 더 가깝게 만드는 여러 다른 유용한 일들을 할 수 있을 거예요. 흥미롭게도, 다른 스펙들이 서비스 워커 컨텍스트를 사용할 수 있고 사용하기 시작할 거예요. 예를 들어:

  • Background synchronization: 사용자가 사이트에 없어도 서비스 워커를 시작하여 캐시를 업데이트할 수 있게 하기 등.
  • Reacting to push messages: 서비스 워커를 시작하여 사용자에게 새 콘텐츠가 사용 가능하다는 메시지를 보내기.
  • 특정 시간 및 날짜에 반응하기.
  • 지오펜스(geo-fence) 진입하기.

Interfaces

Cache
: ServiceWorker 생명주기의 일부로 캐시된 Request / Response 객체 쌍을 위한 스토리지를 나타내요.

CacheStorage
: Cache 객체들을 위한 스토리지를 나타내요. ServiceWorker가 접근할 수 있는 모든 명명된 캐시의 마스터 디렉토리를 제공하고, 문자열 이름과 해당 Cache 객체의 매핑을 유지해요.

Client
: 서비스 워커 클라이언트의 스코프를 나타내요. 서비스 워커 클라이언트는 브라우저 컨텍스트의 문서이거나 활성 워커에 의해 제어되는 SharedWorker예요.

Clients
: Client 객체 목록을 위한 컨테이너를 나타내요. 현재 출처의 활성 서비스 워커 클라이언트들에 접근하는 주요 방법이에요.

ExtendableEvent
: 서비스 워커 생명주기의 일부로 ServiceWorkerGlobalScope에서 전달되는 installactivate 이벤트의 수명을 연장해요. 이를 통해 (FetchEvent 같은) 기능적 이벤트들이 데이터베이스 스키마를 업그레이드하고 오래된 캐시 항목을 삭제하는 등의 작업이 완료될 때까지 ServiceWorker에 전달되지 않도록 보장해요.

ExtendableMessageEvent
: 서비스 워커에서 발생하는 message 이벤트의 이벤트 객체예요 (다른 컨텍스트에서 ServiceWorkerGlobalScope에서 채널 메시지가 수신될 때) — 그러한 이벤트의 수명을 연장해요.

FetchEvent
: onfetch 핸들러에 전달되는 매개변수예요. FetchEventServiceWorkerServiceWorkerGlobalScope에서 전달되는 fetch 액션을 나타내요. 요청과 결과 응답에 대한 정보를 포함하고, 제어되는 페이지에 임의의 응답을 제공할 수 있게 해주는 FetchEvent.respondWith() 메서드를 제공해요.

InstallEvent
: install 이벤트 핸들러 함수에 전달되는 매개변수예요. InstallEvent 인터페이스는 ServiceWorkerServiceWorkerGlobalScope에서 전달되는 설치 액션을 나타내요. ExtendableEvent의 자식으로서, FetchEvent 같은 기능적 이벤트들이 설치 중에 전달되지 않도록 보장해요.

NavigationPreloadManager
: 서비스 워커와 함께 리소스의 preloading을 관리하는 메서드들을 제공해요.

ServiceWorker
: 서비스 워커를 나타내요. 여러 브라우징 컨텍스트(예: 페이지, 워커 등)가 동일한 ServiceWorker 객체와 연결될 수 있어요.

ServiceWorkerContainer
: 네트워크 생태계에서 전체 단위로서의 서비스 워커를 나타내는 객체를 제공해요. 서비스 워커를 등록, 등록 해제, 업데이트하고, 서비스 워커와 그들의 등록 상태에 접근하는 기능을 포함해요.

ServiceWorkerGlobalScope
: 서비스 워커의 전역 실행 컨텍스트를 나타내요.

ServiceWorkerRegistration
: 서비스 워커 등록을 나타내요.

WindowClient
: 활성 워커에 의해 제어되는 브라우저 컨텍스트의 문서인 서비스 워커 클라이언트의 스코프를 나타내요. 이것은 몇 가지 추가적인 메서드와 속성들이 사용 가능한 특별한 타입의 Client 객체예요.

Extensions to other interfaces

Window.caches and WorkerGlobalScope.caches
: 현재 컨텍스트와 연결된 CacheStorage 객체를 반환해요.

Navigator.serviceWorker and WorkerNavigator.serviceWorker
: ServiceWorkerContainer 객체를 반환하는데요, 연결된 문서에 대한 ServiceWorker 객체들의 등록, 제거, 업그레이드, 그리고 통신에 대한 접근을 제공해요.


Specifications

Specification
Service Workers Nightly

See also


Help improve MDN

이 페이지가 도움이 되셨나요?

👍 Yes | 👎 No

기여하는 방법 알아보기

이 페이지는 2026년 3월 8일에 MDN contributors에 의해 마지막으로 수정되었어요.

GitHub에서 이 페이지 보기문제 보고하기


💡 강사 팁

여러분, Service Worker는 현대 웹 개발에서 가장 혁신적인 기술 중 하나예요! 🚀

Service Worker가 뭔가요?

쉽게 말하면, 브라우저와 네트워크 사이에서 프록시 역할을 하는 JavaScript 파일이에요. 마치 중간에서 모든 요청을 가로채서 "이걸 어떻게 처리할까?" 결정하는 똑똑한 중개인 같은 거죠!

왜 혁신적일까요?

오프라인에서도 작동하는 웹!

예전에는 인터넷 연결이 끊어지면 웹사이트가 아무것도 못 했어요. 하지만 Service Worker를 사용하면:

// Service Worker 등록
if ('serviceWorker' in navigator) {
  navigator.serviceWorker.register('/sw.js')
    .then(reg => console.log('Service Worker 등록 성공!', reg))
    .catch(err => console.log('Service Worker 등록 실패', err));
}
// sw.js - Service Worker 파일
// 캐싱 전략
self.addEventListener('install', event => {
  event.waitUntil(
    caches.open('v1').then(cache => {
      return cache.addAll([
        '/',
        '/styles.css',
        '/script.js',
        '/logo.png'
      ]);
    })
  );
});

// 네트워크 요청 가로채기
self.addEventListener('fetch', event => {
  event.respondWith(
    caches.match(event.request)
      .then(response => {
        // 캐시에 있으면 캐시에서, 없으면 네트워크에서
        return response || fetch(event.request);
      })
  );
});

제가 겪은 실전 경험

PWA(Progressive Web App) 프로젝트

한 뉴스 앱을 만들었을 때, Service Worker로 이런 걸 구현했어요:

  1. 오프라인 우선 전략
// 네트워크가 느리면 캐시 먼저 보여주고 백그라운드에서 업데이트
self.addEventListener('fetch', event => {
  event.respondWith(
    caches.open('news-v1').then(cache => {
      return cache.match(event.request).then(response => {
        const fetchPromise = fetch(event.request).then(networkResponse => {
          cache.put(event.request, networkResponse.clone());
          return networkResponse;
        });
        return response || fetchPromise;
      });
    })
  );
});

결과: 지하철에서도 뉴스를 읽을 수 있어요! 📰

  1. 백그라운드 동기화
// 오프라인에서 작성한 댓글을 온라인 되면 자동 전송
self.addEventListener('sync', event => {
  if (event.tag === 'sync-comments') {
    event.waitUntil(sendPendingComments());
  }
});

생명주기 완벽 이해하기

문서에서 설명한 생명주기, 정말 중요해요!

[등록] → [다운로드] → [설치] → [활성화] → [작동]

실제 코드로 보면:

// 1. 설치 단계: 필요한 파일 캐싱
self.addEventListener('install', event => {
  console.log('Service Worker 설치 중...');
  event.waitUntil(
    caches.open('v2').then(cache => {
      return cache.addAll(['/index.html', '/app.js']);
    })
  );
  self.skipWaiting(); // 즉시 활성화 (선택사항)
});

// 2. 활성화 단계: 오래된 캐시 정리
self.addEventListener('activate', event => {
  console.log('Service Worker 활성화 중...');
  event.waitUntil(
    caches.keys().then(cacheNames => {
      return Promise.all(
        cacheNames.map(cacheName => {
          if (cacheName !== 'v2') {
            console.log('오래된 캐시 삭제:', cacheName);
            return caches.delete(cacheName);
          }
        })
      );
    })
  );
  return self.clients.claim(); // 즉시 제어 시작
});

// 3. 작동 단계: fetch 이벤트 처리
self.addEventListener('fetch', event => {
  console.log('Fetch 요청:', event.request.url);
  // 응답 처리...
});

주의사항 & 팁

1. HTTPS 필수!

// ❌ HTTP에서는 작동 안 함 (localhost 제외)
// http://mysite.com - Service Worker 등록 실패

// ✅ HTTPS에서만 작동
// https://mysite.com - Service Worker 정상 작동

2. 스코프 이해하기

// Service Worker는 등록된 경로 이하만 제어해요
navigator.serviceWorker.register('/sw.js', {
  scope: '/app/' // /app/으로 시작하는 경로만 제어
});

3. 업데이트 전략

// Service Worker 파일이 1바이트라도 바뀌면 새로운 버전으로 인식
// 버전 관리를 명시적으로 하세요!
const CACHE_VERSION = 'v1.0.3';

self.addEventListener('install', event => {
  event.waitUntil(
    caches.open(CACHE_VERSION).then(cache => {
      return cache.addAll(CACHED_FILES);
    })
  );
});

4. 디버깅 도구 활용

Chrome DevTools → Application → Service Workers 탭에서:

  • 등록 상태 확인
  • 강제 업데이트
  • Offline 모드 테스트
  • Push 알림 테스트

캐싱 전략 패턴

실무에서 자주 쓰는 패턴들:

1. Cache First (캐시 우선)

// 정적 리소스에 적합 (CSS, JS, 이미지)
self.addEventListener('fetch', event => {
  event.respondWith(
    caches.match(event.request)
      .then(response => response || fetch(event.request))
  );
});

2. Network First (네트워크 우선)

// 동적 콘텐츠에 적합 (API 응답)
self.addEventListener('fetch', event => {
  event.respondWith(
    fetch(event.request)
      .catch(() => caches.match(event.request))
  );
});

3. Stale While Revalidate

// 빠른 응답 + 최신 데이터 (최고의 조합!)
self.addEventListener('fetch', event => {
  event.respondWith(
    caches.open('dynamic').then(cache => {
      return cache.match(event.request).then(response => {
        const fetchPromise = fetch(event.request).then(networkResponse => {
          cache.put(event.request, networkResponse.clone());
          return networkResponse;
        });
        return response || fetchPromise;
      });
    })
  );
});

흔한 실수들

❌ 잘못된 예시들:

// 1. DOM 접근 시도
self.addEventListener('install', () => {
  document.getElementById('test'); // ❌ Service Worker에는 DOM 없음!
});

// 2. 동기 API 사용
self.addEventListener('fetch', () => {
  const xhr = new XMLHttpRequest(); // ❌ 동기 XHR 안 됨!
  localStorage.setItem('key', 'value'); // ❌ localStorage 안 됨!
});

// 3. waitUntil 빠뜨리기
self.addEventListener('install', event => {
  caches.open('v1'); // ❌ event.waitUntil() 없으면 중간에 종료될 수 있음!
});

✅ 올바른 예시:

// 1. 메시지 통신으로 DOM 제어
self.addEventListener('message', event => {
  event.ports[0].postMessage({action: 'updateUI'});
});

// 2. 비동기 API 사용
self.addEventListener('fetch', event => {
  event.respondWith(
    fetch(event.request) // ✅ 비동기 Fetch API
      .then(response => caches.put(event.request, response))
  );
});

// 3. waitUntil 사용
self.addEventListener('install', event => {
  event.waitUntil( // ✅ Promise가 완료될 때까지 대기
    caches.open('v1').then(cache => cache.addAll(FILES))
  );
});

실무 완성 코드 예시

제가 실제 프로젝트에서 쓰는 템플릿:

const CACHE_NAME = 'my-app-v1.0.0';
const RUNTIME_CACHE = 'runtime';

const PRECACHE_URLS = [
  '/',
  '/styles/main.css',
  '/scripts/app.js',
  '/offline.html'
];

// 설치: 정적 리소스 캐싱
self.addEventListener('install', event => {
  event.waitUntil(
    caches.open(CACHE_NAME)
      .then(cache => cache.addAll(PRECACHE_URLS))
      .then(() => self.skipWaiting())
  );
});

// 활성화: 오래된 캐시 삭제
self.addEventListener('activate', event => {
  event.waitUntil(
    caches.keys().then(cacheNames => {
      return Promise.all(
        cacheNames
          .filter(name => name !== CACHE_NAME && name !== RUNTIME_CACHE)
          .map(name => caches.delete(name))
      );
    }).then(() => self.clients.claim())
  );
});

// Fetch: 하이브리드 전략
self.addEventListener('fetch', event => {
  const { request } = event;
  const url = new URL(request.url);

  // API 요청: Network First
  if (url.pathname.startsWith('/api/')) {
    event.respondWith(
      fetch(request)
        .then(response => {
          const clone = response.clone();
          caches.open(RUNTIME_CACHE)
            .then(cache => cache.put(request, clone));
          return response;
        })
        .catch(() => caches.match(request))
    );
    return;
  }

  // 정적 리소스: Cache First
  event.respondWith(
    caches.match(request)
      .then(response => response || fetch(request))
      .catch(() => caches.match('/offline.html'))
  );
});

Using Service Workers

이 문서는 서비스 워커를 시작하는 방법에 대한 정보를 제공해요. 기본 아키텍처, 서비스 워커 등록하기, 새로운 서비스 워커의 설치 및 활성화 과정, 서비스 워커 업데이트하기, 캐시 제어와 커스텀 응답 등을 모두 오프라인 기능을 갖춘 앱의 맥락에서 다루고 있어요.


💡 강사 팁

여러분, 이 문서는 Service Worker 완전 정복 가이드예요! 🎯

MDN에서 제공하는 가장 실용적인 Service Worker 튜토리얼 중 하나인데요, 이론만 다루는 게 아니라 실제로 동작하는 오프라인 앱을 만드는 전체 과정을 다뤄요.

이 문서에서 배울 내용

문서에서 언급한 내용들을 정리하면:

주제설명
Basic architectureService Worker의 기본 구조와 동작 원리
Registering서비스 워커를 브라우저에 등록하는 방법
Installation & Activation설치/활성화 생명주기 이해
Updating새 버전 배포와 업데이트 처리
Cache control캐시 전략과 관리 방법
Custom responses네트워크 요청 가로채서 커스텀 응답 만들기

실무 활용도

제 경험상 이 순서대로 배우시면 가장 효과적이에요:

  1. 등록(Register) → 가장 먼저 해야 할 일
  2. 설치(Install) → 필요한 파일 캐싱
  3. 활성화(Activate) → 이전 버전 정리
  4. Fetch 처리 → 실제 오프라인 기능 구현
  5. 업데이트 → 배포 후 관리

Service Worker 사용하기

Service Worker의 전제

여러분, 웹 사용자들이 수년간 겪어온 가장 큰 문제 중 하나가 바로 연결 끊김이에요. 세상에서 가장 훌륭한 웹 앱이라도 다운로드할 수 없으면 끔찍한 사용자 경험을 제공하게 되죠. 이 문제를 해결하기 위한 다양한 시도들이 있었고, 일부 문제들은 해결되었어요. 하지만 가장 큰 문제는 자산 캐싱과 커스텀 네트워크 요청을 위한 전반적인 제어 메커니즘이 없다는 거였어요.

Service Worker가 바로 이런 문제들을 해결해줘요. Service Worker를 사용하면 앱이 캐시된 자산을 먼저 사용하도록 설정할 수 있어서, 오프라인 상태에서도 기본적인 경험을 제공한 다음, 이후에 네트워크에서 더 많은 데이터를 가져올 수 있어요 (일반적으로 "오프라인 우선(offline first)"이라고 불러요). 이건 이미 네이티브 앱에서 사용 가능한 기능이고, 네이티브 앱이 웹 앱보다 자주 선택되는 주요 이유 중 하나죠.

Service Worker는 프록시 서버처럼 동작해서, 요청과 응답을 수정하고 자체 캐시의 항목으로 대체할 수 있게 해줘요.

💡 강사 팁: 실무에서 Service Worker는 PWA(Progressive Web App)를 만들 때 핵심 기술이에요. 특히 모바일 환경에서 네트워크가 불안정할 때 사용자 경험을 크게 개선할 수 있답니다. 제 경험상 Service Worker를 제대로 구현한 웹앱은 네이티브 앱과 거의 차이가 없는 수준의 오프라인 경험을 제공할 수 있어요.

Service Worker를 가지고 놀기 위한 설정

Service Worker는 모든 최신 브라우저에서 기본적으로 활성화되어 있어요. Service Worker를 사용하는 코드를 실행하려면, 코드를 HTTPS를 통해 제공해야 해요 — Service Worker는 보안상의 이유로 HTTPS를 통해서만 실행되도록 제한되어 있거든요. HTTPS를 지원하는 서버가 필요해요. 실험을 호스팅하려면 GitHub, Netlify, Vercel 등과 같은 서비스를 사용할 수 있어요. 로컬 개발을 용이하게 하기 위해, localhost도 브라우저에서 안전한 출처로 간주돼요.

💡 강사 팁: 로컬 개발할 때는 localhost를 사용하면 되지만, 실제 배포 시에는 반드시 HTTPS를 사용해야 해요. Let's Encrypt 같은 무료 SSL 인증서를 사용하면 쉽게 HTTPS를 적용할 수 있어요. 개발 단계에서는 GitHub Pages나 Netlify를 사용하면 자동으로 HTTPS가 적용되니까 편리하답니다.

기본 아키텍처

Service Worker를 사용할 때, 기본 설정을 위해 일반적으로 다음 단계들이 수행돼요:

  1. Service Worker 코드를 가져온 다음 serviceWorkerContainer.register()를 사용하여 등록해요. 성공하면, Service Worker는 ServiceWorkerGlobalScope에서 실행돼요; 이것은 기본적으로 특별한 종류의 워커 컨텍스트로, 메인 스크립트 실행 스레드에서 벗어나 실행되며, DOM 접근 권한이 없어요. Service Worker는 이제 이벤트를 처리할 준비가 됐어요.

  2. 설치가 진행돼요. install 이벤트는 항상 Service Worker에게 전송되는 첫 번째 이벤트예요 (이것은 IndexedDB를 채우고 사이트 자산을 캐싱하는 프로세스를 시작하는 데 사용될 수 있어요). 이 단계에서, 애플리케이션은 오프라인에서 사용 가능하도록 모든 것을 준비해요.

  3. install 핸들러가 완료되면, Service Worker가 설치된 것으로 간주돼요. 이 시점에서 이전 버전의 Service Worker가 활성화되어 열린 페이지들을 제어하고 있을 수 있어요. 같은 Service Worker의 서로 다른 두 버전이 동시에 실행되는 것을 원하지 않기 때문에, 새 버전은 아직 활성화되지 않아요.

  4. 이전 버전의 Service Worker가 제어하던 모든 페이지가 닫히면, 이전 버전을 폐기하는 것이 안전하고, 새로 설치된 Service Worker가 activate 이벤트를 받아요. activate의 주요 용도는 이전 버전의 Service Worker에서 사용된 리소스를 정리하는 거예요. 새 Service Worker는 skipWaiting()을 호출해서 열린 페이지가 닫히기를 기다리지 않고 즉시 활성화되도록 요청할 수 있어요. 그러면 새 Service Worker가 즉시 activate를 받고 열려있는 모든 페이지를 인수받게 돼요.

  5. 활성화 후, Service Worker는 이제 페이지들을 제어하지만, register()가 성공한 후에 열린 페이지들만 제어해요. 다시 말해, 문서가 실제로 제어되려면 다시 로드되어야 해요. 왜냐하면 문서는 Service Worker가 있거나 없이 생명을 시작하고 평생 그것을 유지하기 때문이에요. 이 기본 동작을 무시하고 열린 페이지들을 채택하려면, Service Worker가 clients.claim()을 호출할 수 있어요.

  6. Service Worker의 새 버전을 가져올 때마다, 이 사이클이 다시 발생하고 이전 버전의 잔여물은 새 버전의 활성화 중에 정리돼요.

lifecycle diagram

여기 사용 가능한 Service Worker 이벤트들의 요약이에요:

💡 강사 팁: Service Worker의 생명주기는 처음에는 복잡해 보일 수 있지만, 이전 버전과 새 버전이 충돌하지 않도록 하는 안전 메커니즘이에요. 개발 중에는 Chrome DevTools의 Application 탭에서 "Update on reload"를 체크하면 매번 새로고침할 때마다 새 버전이 즉시 활성화되어 개발이 훨씬 편해져요. 하지만 실제 배포 시에는 이 옵션을 사용하지 않으니 주의하세요!

데모

Service Worker를 등록하고 설치하는 아주 기본적인 것들을 보여주기 위해, simple service worker라는 데모를 만들었어요. 이것은 간단한 스타워즈 레고 이미지 갤러리예요. Promise 기반 함수를 사용해서 JSON 객체에서 이미지 데이터를 읽고 fetch()를 사용하여 이미지를 로드한 다음, 페이지 아래로 한 줄로 이미지를 표시해요. 지금은 정적으로 유지했어요. 또한 Service Worker를 등록, 설치 및 활성화해요.

The words Star Wars followed by an image of a Lego version of the Darth Vader character

GitHub에서 소스 코드를 보실 수 있고, simple service worker 실행 중인 것도 볼 수 있어요.

Worker 등록하기

우리 앱의 JavaScript 파일 — app.js — 의 첫 번째 코드 블록은 다음과 같아요. 이것이 Service Worker를 사용하는 진입점이에요.

const registerServiceWorker = async () => {
  if ("serviceWorker" in navigator) {
    try {
      const registration = await navigator.serviceWorker.register("/sw.js", {
        scope: "/",
      });
      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();
  1. if 블록은 Service Worker가 지원되는지 확인하기 위한 기능 감지 테스트를 수행한 다음 등록을 시도해요.

  2. 다음으로, ServiceWorkerContainer.register() 함수를 사용하여 이 사이트의 Service Worker를 등록해요. Service Worker 코드는 우리 앱 내부에 있는 JavaScript 파일이에요 (참고: 이것은 origin에 상대적인 파일의 URL이지, 그것을 참조하는 JS 파일이 아니에요.)

  3. scope 매개변수는 선택 사항이고, Service Worker가 제어하길 원하는 콘텐츠의 하위 집합을 지정하는 데 사용될 수 있어요. 이 경우, '/'를 지정했는데, 이는 앱의 origin 아래의 모든 콘텐츠를 의미해요. 생략하면 어차피 이 값이 기본값이 되지만, 설명 목적으로 여기에 명시했어요.

이것은 Service Worker를 등록하는데, Worker 컨텍스트에서 실행되므로 DOM 접근 권한이 없어요.

단일 Service Worker는 많은 페이지를 제어할 수 있어요. scope 내의 페이지가 로드될 때마다, Service Worker가 해당 페이지에 설치되어 작동해요. 따라서 Service Worker 스크립트의 전역 변수를 조심해야 한다는 점을 명심하세요: 각 페이지마다 고유한 Worker가 있는 게 아니거든요.

💡 강사 팁: scope 설정은 매우 중요해요! 예를 들어 /js/sw.js에 Service Worker를 두면 기본적으로 /js/ 경로만 제어할 수 있어요. 전체 사이트를 제어하려면 루트에 Service Worker 파일을 두거나 Service-Worker-Allowed 헤더를 사용해야 합니다. 실무에서는 보통 루트에 sw.js 파일을 두는 게 가장 간단하고 명확해요.

참고:
위에서 보여드린 것처럼 기능 감지를 사용하면 한 가지 좋은 점이 있어요. Service Worker를 지원하지 않는 브라우저는 일반적으로 예상되는 방식으로 앱을 온라인에서 사용할 수 있다는 거예요.

왜 내 Service Worker 등록이 실패하나요?

Service Worker가 다음 이유 중 하나로 등록에 실패해요:

  • 애플리케이션을 안전한 컨텍스트 (HTTPS를 통해)에서 실행하고 있지 않아요.
  • Service Worker 파일의 경로가 올바르지 않아요.
    경로는 앱의 루트 디렉토리가 아닌 origin에 상대적이어야 해요.
    우리 예제에서 Worker는 https://bncb2v.csb.app/sw.js에 있고, 앱의 루트는 https://bncb2v.csb.app/이므로, Service Worker는 /sw.js로 지정되어야 해요.
  • Service Worker 경로가 앱과 다른 origin의 Service Worker를 가리키고 있어요.
  • Service Worker 등록에 Worker 경로에서 허용되는 것보다 넓은 scope 옵션이 포함되어 있어요.
    Service Worker의 기본 scope는 Worker가 위치한 디렉토리예요.
    다시 말해, 스크립트 sw.js/js/sw.js에 있다면, 기본적으로 /js/ 경로에 있는 (또는 중첩된) URL만 제어할 수 있어요.
    Service Worker의 scope는 Service-Worker-Allowed 헤더로 넓힐 수 있어요 (또는 좁힐 수도 있어요).
  • 모든 쿠키 차단, 비공개 브라우징 모드, 종료 시 자동 쿠키 삭제 등과 같은 브라우저별 설정이 활성화되어 있어요.
    자세한 정보는 serviceWorker.register() 브라우저 호환성을 참조하세요.

💡 강사 팁: 등록 실패의 가장 흔한 원인은 HTTPS를 사용하지 않는 것과 경로 문제예요. 개발 중에는 반드시 브라우저의 개발자 도구 콘솔을 열어두고 에러 메시지를 확인하세요. Chrome의 경우 Application 탭에서 Service Worker 상태를 실시간으로 확인할 수 있어서 디버깅에 정말 유용해요!

설치 및 활성화: 캐시 채우기

Service Worker가 등록된 후, 브라우저는 페이지/사이트에 대해 Service Worker를 설치한 다음 활성화하려고 시도할 거예요.

install 이벤트는 Service Worker 설치 또는 업데이트 시 발생하는 첫 번째 이벤트예요.
등록이 성공적으로 완료된 직후 한 번만 발생하며, 일반적으로 오프라인에서 앱을 실행하는 데 필요한 자산으로 브라우저의 오프라인 캐싱 기능을 채우는 데 사용돼요. 이를 위해 Service Worker의 저장소 API인 cache를 사용해요 — Service Worker에서 응답으로 전달된 자산을 저장하고 요청으로 키를 지정할 수 있는 전역 객체예요. 이 API는 브라우저의 표준 캐시와 유사한 방식으로 작동하지만, 도메인에 특정적이에요. 캐시의 내용은 직접 지울 때까지 유지돼요.

우리 Service Worker가 install 이벤트를 어떻게 처리하는지 보세요:

const addResourcesToCache = async (resources) => {
  const cache = await caches.open("v1");
  await cache.addAll(resources);
};

self.addEventListener("install", (event) => {
  event.waitUntil(
    addResourcesToCache([
      "/",
      "/index.html",
      "/style.css",
      "/app.js",
      "/image-list.js",
      "/star-wars-logo.jpg",
      "/gallery/bountyHunters.jpg",
      "/gallery/myLittleVader.jpg",
      "/gallery/snowTroopers.jpg",
    ]),
  );
});
  1. 여기서 Service Worker에 install 이벤트 리스너를 추가하고 (따라서 self), 그런 다음 이벤트에 ExtendableEvent.waitUntil() 메서드를 연결해요 — 이것은 waitUntil() 내부의 코드가 성공적으로 발생할 때까지 Service Worker가 설치되지 않도록 보장해요.

  2. addResourcesToCache() 내부에서 caches.open() 메서드를 사용하여 v1이라는 새 캐시를 생성해요. 이것은 우리 사이트 리소스 캐시의 버전 1이 될 거예요. 그런 다음 생성된 캐시에서 addAll() 함수를 호출하는데, 매개변수로 캐시하려는 모든 리소스의 URL 배열을 받아요. URL은 Worker의 location에 상대적이에요.

  3. Promise가 거부되면 설치가 실패하고 Worker는 아무것도 하지 않아요. 이건 괜찮아요. 코드를 수정한 다음 다음 번 등록이 발생할 때 다시 시도할 수 있거든요.

  4. 성공적인 설치 후, Service Worker가 활성화돼요. Service Worker가 처음 설치/활성화될 때는 이게 별로 유용하지 않지만, Service Worker가 업데이트될 때는 더 의미가 있어요 (나중에 Service Worker 업데이트하기 섹션을 보세요.)

참고:
Web Storage API (localStorage)는 Service Worker 캐시와 유사한 방식으로 작동하지만, 동기식이므로 Service Worker에서 허용되지 않아요.

참고:
필요한 경우 데이터 저장을 위해 IndexedDB를 Service Worker 내부에서 사용할 수 있어요.

💡 강사 팁: 캐시할 파일 목록을 작성할 때는 신중해야 해요. 목록의 파일 중 하나라도 실패하면 전체 설치가 실패해요. 실무에서는 핵심 파일만 install 이벤트에서 캐싱하고, 추가 리소스는 fetch 이벤트에서 동적으로 캐싱하는 전략을 많이 사용해요. 또한 버전 관리(v1, v2 등)를 통해 캐시를 관리하면 나중에 업데이트할 때 훨씬 편리합니다!

요청에 대한 사용자 정의 응답

이제 사이트 자산을 캐시했으니, Service Worker에게 캐시된 콘텐츠로 무언가를 하도록 알려줘야 해요. 이것은 fetch 이벤트로 쉽게 할 수 있어요.

  1. fetch 이벤트는 Service Worker가 제어하는 리소스를 가져올 때마다 발생해요. 여기에는 지정된 scope 내의 문서와 해당 문서에서 참조되는 모든 리소스가 포함돼요 (예를 들어 index.html이 이미지를 삽입하기 위해 cross-origin 요청을 하면, 그것도 여전히 Service Worker를 통과해요.)

  2. Service Worker에 fetch 이벤트 리스너를 연결한 다음, 이벤트에서 respondWith() 메서드를 호출하여 HTTP 응답을 가로채고 자신의 콘텐츠로 업데이트할 수 있어요.

self.addEventListener("fetch", (event) => {
  event.respondWith(/* custom content goes here */);
});
  1. 각 경우에 네트워크 요청의 URL과 일치하는 리소스로 응답하는 것부터 시작할 수 있어요:
self.addEventListener("fetch", (event) => {
  event.respondWith(caches.match(event.request));
});

caches.match(event.request)는 네트워크에서 요청된 각 리소스를 캐시에서 사용 가능한 동등한 리소스와 일치시켜줘요. 일치하는 것이 있으면요. 일치는 일반 HTTP 요청처럼 URL과 다양한 헤더를 통해 수행돼요.

Fetch event diagram

💡 강사 팁: fetch 이벤트는 Service Worker의 가장 핵심적인 기능이에요. 여기서 캐시 전략을 구현하게 되는데, Cache First, Network First, Stale While Revalidate 등 다양한 전략이 있어요. 프로젝트의 요구사항에 맞는 전략을 선택하는 게 중요해요. 예를 들어 정적 자산은 Cache First, API 요청은 Network First 전략을 사용하는 식으로 리소스 타입별로 다른 전략을 적용할 수 있답니다.

실패한 요청 복구하기

그래서 caches.match(event.request)는 Service Worker 캐시에 일치하는 항목이 있을 때는 훌륭하지만, 일치하는 항목이 없을 때는 어떻게 할까요? 어떤 종류의 실패 처리도 제공하지 않으면, Promise가 undefined로 resolve되고 아무것도 반환되지 않을 거예요.

캐시의 응답을 테스트한 후, 일반 네트워크 요청으로 폴백할 수 있어요:

const cacheFirst = async (request) => {
  const responseFromCache = await caches.match(request);
  if (responseFromCache) {
    return responseFromCache;
  }
  return fetch(request);
};

self.addEventListener("fetch", (event) => {
  event.respondWith(cacheFirst(event.request));
});

리소스가 캐시에 없으면, 네트워크 요청을 통해 요청돼요.

더 정교한 전략을 사용하면, 네트워크에서 리소스를 요청할 뿐만 아니라 캐시에도 저장하여 해당 리소스에 대한 이후 요청도 오프라인에서 검색할 수 있어요. 이는 스타워즈 갤러리에 추가 이미지가 추가되면, 우리 앱이 자동으로 그것들을 가져와서 캐시할 수 있다는 의미예요. 다음 스니펫은 그러한 전략을 구현해요:

const putInCache = async (request, response) => {
  const cache = await caches.open("v1");
  await cache.put(request, response);
};

const cacheFirst = async (request, event) => {
  const responseFromCache = await caches.match(request);
  if (responseFromCache) {
    return responseFromCache;
  }
  const responseFromNetwork = await fetch(request);
  event.waitUntil(putInCache(request, responseFromNetwork.clone()));
  return responseFromNetwork;
};

self.addEventListener("fetch", (event) => {
  event.respondWith(cacheFirst(event.request, event));
});

요청 URL이 캐시에서 사용할 수 없으면, await fetch(request)로 네트워크 요청에서 리소스를 요청해요. 그 후, 응답의 복제본을 캐시에 넣어요. putInCache() 함수는 caches.open('v1')cache.put()을 사용하여 리소스를 캐시에 추가해요. 원본 응답은 브라우저로 반환되어 호출한 페이지에 전달돼요.

응답을 복제하는 것이 필요한 이유는 요청 및 응답 스트림은 한 번만 읽을 수 있기 때문이에요. 응답을 브라우저에 반환하고 캐시에 넣기 위해 복제해야 해요. 그래서 원본은 브라우저로 반환되고 복제본은 캐시로 전송돼요. 각각 한 번씩 읽혀요.

조금 이상해 보일 수 있는 것은 putInCache()가 반환하는 Promise를 await하지 않는다는 거예요. 그 이유는 응답 복제본이 캐시에 추가될 때까지 기다렸다가 응답을 반환하고 싶지 않기 때문이에요. 하지만, Service Worker가 캐시가 채워지기 전에 종료되지 않도록 Promise에 대해 event.waitUntil()을 호출해야 해요.

지금 우리가 가진 유일한 문제는 요청이 캐시의 어떤 것과도 일치하지 않고 네트워크를 사용할 수 없을 때, 우리 요청은 여전히 실패한다는 거예요. 무슨 일이 있어도 사용자가 최소한 무언가를 얻을 수 있도록 기본 폴백을 제공해봐요:

const putInCache = async (request, response) => {
  const cache = await caches.open("v1");
  await cache.put(request, response);
};

const cacheFirst = async ({ request, fallbackUrl, event }) => {
  // First try to get the resource from the cache
  const responseFromCache = await caches.match(request);
  if (responseFromCache) {
    return responseFromCache;
  }

  // Next try to get the resource from the network
  try {
    const responseFromNetwork = await fetch(request);
    // response may be used only once
    // we need to save clone to put one copy in cache
    // and serve second one
    event.waitUntil(putInCache(request, responseFromNetwork.clone()));
    return responseFromNetwork;
  } catch (error) {
    const fallbackResponse = await caches.match(fallbackUrl);
    if (fallbackResponse) {
      return fallbackResponse;
    }
    // when even the fallback response is not available,
    // there is nothing we can do, but we must always
    // return a Response object
    return new Response("Network error happened", {
      status: 408,
      headers: { "Content-Type": "text/plain" },
    });
  }
};

self.addEventListener("fetch", (event) => {
  event.respondWith(
    cacheFirst({
      request: event.request,
      fallbackUrl: "/gallery/myLittleVader.jpg",
      event,
    }),
  );
});

실패할 가능성이 있는 유일한 업데이트가 새 이미지인데, 그 외의 모든 것은 앞서 본 install 이벤트 리스너에서 설치를 위해 의존하기 때문에 이 폴백 이미지를 선택했어요.

💡 강사 팁: 폴백 전략은 정말 중요해요! 실무에서는 각 리소스 타입별로 다른 폴백을 준비하는 게 좋아요. 예를 들어 이미지는 placeholder 이미지, API는 캐시된 데이터나 오프라인 메시지를 반환하는 식이죠. 사용자 경험을 위해 "네트워크 오류"보다는 의미있는 오프라인 페이지를 보여주는 것도 좋은 방법이에요. 제 경험상 잘 디자인된 오프라인 페이지는 사용자에게 앱이 여전히 작동하고 있다는 신뢰를 줄 수 있어요.

Service Worker 네비게이션 프리로드

활성화된 경우, 네비게이션 프리로드 기능은 fetch 요청이 이루어지자마자 리소스 다운로드를 시작하고, Service Worker 활성화와 병렬로 진행해요. 이것은 Service Worker가 활성화될 때까지 기다려야 하는 것이 아니라, 페이지로 네비게이션할 때 즉시 다운로드가 시작되도록 보장해요. 그 지연은 상대적으로 드물게 발생하지만, 발생하면 피할 수 없고 상당할 수 있어요.

먼저 registration.navigationPreload.enable()을 사용하여 Service Worker 활성화 중에 기능을 활성화해야 해요:

self.addEventListener("activate", (event) => {
  event.waitUntil(self.registration?.navigationPreload.enable());
});

그런 다음 event.preloadResponse를 사용하여 fetch 이벤트 핸들러에서 프리로드된 리소스가 다운로드를 완료할 때까지 기다려요.

이전 섹션의 예제를 계속하면, 캐시 확인 후, 그리고 성공하지 않으면 네트워크에서 가져오기 전에 프리로드된 리소스를 기다리는 코드를 삽입해요.

새 프로세스는:

  1. 캐시 확인
  2. cacheFirst() 함수에 preloadResponsePromise로 전달된 event.preloadResponse를 기다려요. 반환되면 결과를 캐시해요.
  3. 둘 다 정의되지 않으면 네트워크로 이동해요.
const addResourcesToCache = async (resources) => {
  const cache = await caches.open("v1");
  await cache.addAll(resources);
};

const putInCache = async (request, response) => {
  const cache = await caches.open("v1");
  await cache.put(request, response);
};

const cacheFirst = async ({
  request,
  preloadResponsePromise,
  fallbackUrl,
  event,
}) => {
  // First try to get the resource from the cache
  const responseFromCache = await caches.match(request);
  if (responseFromCache) {
    return responseFromCache;
  }

  // Next try to use (and cache) the preloaded response, if it's there
  const preloadResponse = await preloadResponsePromise;
  if (preloadResponse) {
    console.info("using preload response", preloadResponse);
    event.waitUntil(putInCache(request, preloadResponse.clone()));
    return preloadResponse;
  }

  // Next try to get the resource from the network
  try {
    const responseFromNetwork = await fetch(request);
    // response may be used only once
    // we need to save clone to put one copy in cache
    // and serve second one
    event.waitUntil(putInCache(request, responseFromNetwork.clone()));
    return responseFromNetwork;
  } catch (error) {
    const fallbackResponse = await caches.match(fallbackUrl);
    if (fallbackResponse) {
      return fallbackResponse;
    }
    // when even the fallback response is not available,
    // there is nothing we can do, but we must always
    // return a Response object
    return new Response("Network error happened", {
      status: 408,
      headers: { "Content-Type": "text/plain" },
    });
  }
};

// Enable navigation preload
const enableNavigationPreload = async () => {
  if (self.registration.navigationPreload) {
    await self.registration.navigationPreload.enable();
  }
};

self.addEventListener("activate", (event) => {
  event.waitUntil(enableNavigationPreload());
});

self.addEventListener("install", (event) => {
  event.waitUntil(
    addResourcesToCache([
      "/",
      "/index.html",
      "/style.css",
      "/app.js",
      "/image-list.js",
      "/star-wars-logo.jpg",
      "/gallery/bountyHunters.jpg",
      "/gallery/myLittleVader.jpg",
      "/gallery/snowTroopers.jpg",
    ]),
  );
});

self.addEventListener("fetch", (event) => {
  event.respondWith(
    cacheFirst({
      request: event.request,
      preloadResponsePromise: event.preloadResponse,
      fallbackUrl: "/gallery/myLittleVader.jpg",
      event,
    }),
  );
});

이 예제에서 리소스가 "정상적으로" 다운로드되든 프리로드되든 동일한 데이터를 다운로드하고 캐싱한다는 점에 주목하세요. 대신 프리로드 시 다른 리소스를 다운로드하고 캐시하도록 선택할 수 있어요. 더 많은 정보는 NavigationPreloadManager > Custom responses를 참조하세요.

💡 강사 팁: 네비게이션 프리로드는 성능 최적화의 숨은 보석이에요! 특히 Service Worker가 처음 활성화될 때나 업데이트될 때의 지연을 줄여줘요. 하지만 모든 경우에 필요한 건 아니에요. 앱이 이미 빠르게 로드되고 있다면 추가적인 복잡성이 필요 없을 수 있어요. Performance API를 사용해서 실제로 성능 개선이 있는지 측정한 후 적용하는 걸 추천해요. 제 경험상 큰 SPA(Single Page Application)에서는 눈에 띄는 성능 향상이 있었지만, 작은 정적 사이트에서는 차이가 미미했어요.

profile
프론트에_가까운_풀스택_개발자

0개의 댓글