서비스 워커의 삶

서비스 워커의 수명 주기를 이해하지 않고는 서비스 워커가 무엇을 하고 있는지 알기 어렵습니다. 내부 작동 방식은 불투명해 보입니다. 심지어 임의적으로 보일 수도 있습니다. 다른 브라우저 API와 마찬가지로 서비스 워커 동작은 잘 정의되고 지정되며 오프라인 애플리케이션을 가능하게 하는 동시에 사용자 환경을 방해하지 않고 업데이트를 용이하게 한다는 점을 기억하면 도움이 됩니다.

Workbox를 본격적으로 살펴보기 전에 서비스 워커 수명 주기를 이해하여 Workbox가 의미가 무엇인지 파악하는 것이 중요합니다.

용어 정의

서비스 워커 수명 주기를 알아보기 전에 수명 주기 작동 방식에 대한 용어를 정의하는 것이 좋습니다.

관리 및 범위

서비스 워커의 작동 방식을 이해하려면 제어의 개념이 중요합니다. 서비스 워커가 제어하는 것으로 설명된 페이지는 서비스 워커가 대신 네트워크 요청을 가로채도록 허용하는 페이지입니다. 서비스 워커가 있고 지정된 범위 내에서 페이지에 대한 작업을 수행할 수 있습니다.

범위

서비스 워커의 범위는 웹 서버상의 서비스 워커 위치에 따라 결정됩니다. 서비스 워커가 /subdir/index.html에 있는 페이지에서 실행되고 /subdir/sw.js에 있는 경우 서비스 워커의 범위는 /subdir/입니다. 실제 범위 개념은 다음 예를 확인하세요.

  1. https://service-worker-scope-viewer.glitch.me/subdir/index.html로 이동합니다. 페이지를 제어하는 서비스 워커가 없다는 메시지가 표시됩니다. 하지만 이 페이지는 https://service-worker-scope-viewer.glitch.me/subdir/sw.js에서 서비스 워커를 등록합니다.
  2. 페이지를 새로고침합니다. 서비스 워커가 등록되었으며 현재 활성 상태이므로 페이지를 제어하고 있습니다. 서비스 워커의 범위, 현재 상태, URL이 포함된 양식이 표시됩니다. 참고: 페이지를 새로고침해야 하는 것은 범위와는 관련이 없으며 서비스 워커 수명 주기와 관련이 있습니다. 이는 나중에 설명하겠습니다.
  3. 이제 https://service-worker-scope-viewer.glitch.me/index.html로 이동합니다. 서비스 워커가 이 출처에 등록되었지만 현재 서비스 워커가 없다는 메시지가 여전히 표시됩니다. 페이지가 등록된 서비스 워커의 범위에 속하지 않기 때문입니다.

범위는 서비스 워커가 제어하는 페이지를 제한합니다. 이 예에서 /subdir/sw.js에서 로드된 서비스 워커는 /subdir/ 또는 하위 트리에 있는 페이지만 제어할 수 있습니다.

위의 내용은 기본적으로 범위 지정이 작동하는 방식이지만 Service-Worker-Allowed 응답 헤더를 설정하고 scope 옵션register 메서드에 전달하여 허용되는 최대 범위를 재정의할 수 있습니다.

서비스 워커 범위를 출처의 하위 집합으로 제한할 만한 타당한 이유가 없다면 웹 서버의 루트 디렉터리에서 서비스 워커를 로드하여 범위를 최대한 넓히고 Service-Worker-Allowed 헤더에 관해서는 걱정하지 마세요. 그렇게 하면 모든 사람에게 훨씬 더 간단합니다.

클라이언트

서비스 워커가 페이지를 제어한다고 하면, 실제로는 클라이언트를 제어하는 것입니다. 클라이언트는 해당 서비스 워커의 범위 내에 URL이 포함되는 열린 페이지를 의미합니다. 구체적으로는 WindowClient의 인스턴스입니다.

새 서비스 워커의 수명 주기

서비스 워커가 페이지를 제어하려면 먼저 페이지가 존재해야 합니다. 활성 서비스 워커가 없는 웹사이트에 새로운 서비스 워커를 배포하면 어떤 일이 발생하는지부터 시작하겠습니다.

등록

등록은 서비스 워커 수명 주기의 초기 단계입니다.

<!-- In index.html, for example: -->
<script>
  // Don't register the service worker
  // until the page has fully loaded
  window.addEventListener('load', () => {
    // Is service worker available?
    if ('serviceWorker' in navigator) {
      navigator.serviceWorker.register('/sw.js').then(() => {
        console.log('Service worker registered!');
      }).catch((error) => {
        console.warn('Error registering service worker:');
        console.warn(error);
      });
    }
  });
</script>

이 코드는 기본 스레드에서 실행되며 다음을 실행합니다.

  1. 사용자의 첫 웹사이트 방문은 등록된 서비스 워커 없이 발생하므로 페이지가 완전히 로드될 때까지 기다린 후에 서비스 워커를 등록합니다. 이렇게 하면 서비스 워커가 무엇이든 사전 캐시하는 경우 대역폭 경합이 방지됩니다.
  2. 서비스 워커가 잘 지원되지만 서비스 워커가 지원되지 않는 브라우저에서 빠른 점검을 실행하면 오류를 방지할 수 있습니다.
  3. 페이지가 완전히 로드되고 서비스 워커가 지원되는 경우 /sw.js를 등록합니다.

다음은 이해해야 할 주요 사항입니다.

  • 서비스 워커는 HTTPS 또는 localhost를 통해서만 사용할 수 있습니다.
  • 서비스 워커의 콘텐츠에 구문 오류가 있으면 등록이 실패하고 서비스 워커가 삭제됩니다.
  • 알림: 서비스 워커는 범위 내에서 작동합니다. 여기서 범위는 루트 디렉터리에서 로드된 전체 출처입니다.
  • 등록이 시작되면 서비스 워커 상태는 'installing'로 설정됩니다.

등록이 완료되면 설치가 시작됩니다.

설치

서비스 워커는 등록 후 install 이벤트를 실행합니다. install는 서비스 워커당 한 번만 호출되며 업데이트될 때까지 다시 실행되지 않습니다. install 이벤트의 콜백은 addEventListener를 사용하여 작업자의 범위에 등록할 수 있습니다.

// /sw.js
self.addEventListener('install', (event) => {
  const cacheKey = 'MyFancyCacheName_v1';

  event.waitUntil(caches.open(cacheKey).then((cache) => {
    // Add all the assets in the array to the 'MyFancyCacheName_v1'
    // `Cache` instance for later use.
    return cache.addAll([
      '/css/global.bc7b80b7.css',
      '/css/home.fe5d0b23.css',
      '/js/home.d3cc4ba4.js',
      '/js/jquery.43ca4933.js'
    ]);
  }));
});

이렇게 하면 새 Cache 인스턴스를 만들고 애셋을 사전 캐시합니다. 사전 캐싱에 대해서는 나중에 많은 기회가 있으므로 event.waitUntil의 역할에 초점을 맞춰 보겠습니다. event.waitUntil는 프로미스를 수락하고 해당 프로미스가 해결될 때까지 기다립니다. 이 예시에서 해당 프로미스는 두 가지 비동기 작업을 실행합니다.

  1. 'MyFancyCache_v1'라는 새 Cache 인스턴스를 만듭니다.
  2. 캐시가 생성되면 애셋 URL 배열이 비동기 addAll 메서드를 사용하여 사전 캐시됩니다.

event.waitUntil에 전달된 프로미스가 거부되면 설치가 실패합니다. 이런 일이 발생하면 서비스 워커는 폐기됩니다.

프로미스가 resolve되면 설치가 성공하고 서비스 워커의 상태가 'installed'로 변경된 후 활성화됩니다.

실행

등록과 설치가 성공하면 서비스 워커가 활성화되고 상태가 'activating'가 됩니다. 서비스 워커의 activate 이벤트에서 활성화 중에 작업을 수행할 수 있습니다. 이 이벤트의 일반적인 작업은 오래된 캐시를 정리하는 것이지만, 완전히 새로운 서비스 워커의 경우 지금은 관련이 없으며 서비스 워커 업데이트에 대해 설명할 때 확장될 예정입니다.

새 서비스 워커의 경우 activateinstall가 성공한 직후 실행됩니다. 활성화가 완료되면 서비스 워커의 상태가 'activated'가 됩니다. 기본적으로 새 서비스 워커는 다음 탐색이나 페이지를 새로고침할 때까지 페이지 제어를 시작하지 않습니다.

서비스 워커 업데이트 처리

첫 번째 서비스 워커가 배포되면 나중에 업데이트해야 할 가능성이 높습니다 예를 들어 요청 처리 또는 사전 캐싱 로직에 변경사항이 발생하면 업데이트가 필요할 수 있습니다.

업데이트 시점

브라우저는 다음과 같은 경우 서비스 워커 업데이트를 확인합니다.

업데이트 진행 방식

브라우저가 서비스 워커를 업데이트하는 시점을 아는 것도 중요하지만 '방법'도 중요합니다. 서비스 워커의 URL 또는 범위가 변경되지 않았다고 가정하면 현재 설치된 서비스 워커는 콘텐츠가 변경된 경우에만 새 버전으로 업데이트됩니다.

브라우저는 다음과 같은 몇 가지 방법으로 변경사항을 감지합니다.

  • 해당하는 경우 importScripts에서 요청한 스크립트의 바이트 단위 변경사항입니다.
  • 서비스 워커의 최상위 코드 변경사항(브라우저에서 생성한 디지털 지문에 영향을 줌)

여기에서는 브라우저가 많은 힘든 작업을 수행합니다. 브라우저가 서비스 워커 콘텐츠의 변경사항을 안정적으로 감지하는 데 필요한 모든 것을 갖추려면 HTTP 캐시에 콘텐츠를 보관하도록 지시하지 말고 파일 이름도 변경하지 마세요. 서비스 워커의 범위 내에 있는 새 페이지로의 탐색이 있을 때 브라우저는 자동으로 업데이트 검사를 수행합니다.

수동으로 업데이트 검사 트리거

업데이트와 관련된 등록 로직은 일반적으로 변경되지 않습니다. 그러나 웹사이트에서 세션이 오래 지속되는 경우 한 가지 예외가 있을 수 있습니다. 이는 탐색 요청이 드물게 발생하는 단일 페이지 애플리케이션에서 발생할 수 있습니다. 일반적으로 애플리케이션의 수명 주기가 시작될 때 하나의 탐색 요청을 만나기 때문입니다. 이러한 상황에서는 기본 스레드에서 수동 업데이트가 트리거될 수 있습니다.

navigator.serviceWorker.ready.then((registration) => {
  registration.update();
});

기존 웹사이트의 경우 또는 사용자 세션의 수명이 길지 않은 경우 수동 업데이트를 트리거하지 않아도 됩니다.

설치

번들러를 사용하여 정적 애셋을 생성하는 경우 애셋의 이름에 framework.3defa9d2.js와 같은 해시가 포함됩니다. 이러한 애셋 중 일부가 나중에 오프라인 액세스를 위해 사전 캐시된다고 가정해 보겠습니다. 업데이트된 애셋을 사전 캐시하려면 서비스 워커를 업데이트해야 합니다.

self.addEventListener('install', (event) => {
  const cacheKey = 'MyFancyCacheName_v2';

  event.waitUntil(caches.open(cacheKey).then((cache) => {
    // Add all the assets in the array to the 'MyFancyCacheName_v2'
    // `Cache` instance for later use.
    return cache.addAll([
      '/css/global.ced4aef2.css',
      '/css/home.cbe409ad.css',
      '/js/home.109defa4.js',
      '/js/jquery.38caf32d.js'
    ]);
  }));
});

앞서 살펴본 첫 번째 install 이벤트 예와는 두 가지가 다릅니다.

  1. 키가 'MyFancyCacheName_v2'인 새 Cache 인스턴스가 생성됩니다.
  2. 사전 캐시된 애셋 이름이 변경되었습니다.

한 가지 유의해야 할 점은 업데이트된 서비스 워커가 이전 서비스 워커와 함께 설치된다는 점입니다. 즉, 이전 서비스 워커가 여전히 열린 페이지를 제어하고 있으며 설치 후에는 새 서비스 워커가 활성화될 때까지 대기 상태로 전환됩니다.

기본적으로 새 서비스 워커는 이전 서비스 워커에 의해 제어되는 클라이언트가 없을 때 활성화됩니다. 이는 관련 웹사이트의 열려 있는 모든 탭이 닫힌 경우에 발생합니다.

실행

업데이트된 서비스 워커가 설치되고 대기 단계가 종료되면 서비스 워커가 활성화되고 이전 서비스 워커는 삭제됩니다. 업데이트된 서비스 워커의 activate 이벤트에서 수행되는 일반적인 작업은 오래된 캐시를 프루닝하는 것입니다. caches.keys를 사용하여 열려 있는 모든 Cache 인스턴스의 키를 가져오고 caches.delete를 사용하여 정의된 허용 목록에 없는 캐시를 삭제하여 이전 캐시를 삭제합니다.

self.addEventListener('activate', (event) => {
  // Specify allowed cache keys
  const cacheAllowList = ['MyFancyCacheName_v2'];

  // Get all the currently active `Cache` instances.
  event.waitUntil(caches.keys().then((keys) => {
    // Delete all caches that aren't in the allow list:
    return Promise.all(keys.map((key) => {
      if (!cacheAllowList.includes(key)) {
        return caches.delete(key);
      }
    }));
  }));
});

오래된 캐시는 정리되지 않습니다. 그렇지 않으면 스토리지 할당량을 초과할 위험이 있습니다. 첫 번째 서비스 워커의 'MyFancyCacheName_v1'이 오래되었으므로 캐시 허용 목록이 'MyFancyCacheName_v2'을 지정하도록 업데이트되어 이름이 다른 캐시를 삭제합니다.

activate 이벤트는 이전 캐시가 삭제된 후 완료됩니다. 이 시점에서 새 서비스 워커가 페이지를 제어하고 마침내 이전 페이지를 대체합니다.

수명 주기는 계속됩니다

서비스 워커 배포 및 업데이트를 처리하는 데 Workbox를 사용하든 Service Worker API를 직접 사용하든 서비스 워커 수명 주기를 이해하는 것이 중요합니다. 이러한 사실을 이해하면 서비스 워커의 동작이 신비롭기보다는 더 논리적으로 느껴져야 합니다.

이 주제에 관해 자세히 알아보려면 Jake Archibald가 작성한 기사를 참고하세요. 서비스 수명 주기와 관련된 전체 춤이 진행되는 방식에는 수많은 미묘한 차이가 있지만, 이는 알 수 있습니다. Workbox를 사용하면 이러한 지식은 훨씬 더 발전할 수 있습니다.