서비스 워커 수명 주기

Jake Archibald
Jake Archibald

수명 주기는 서비스 워커의 가장 복잡한 부분입니다. 서비스 워커가 무엇을 하려고 하는지, 어떤 이점이 있는지 모른다면 전쟁을 하는 것 같은 느낌이 들 수 있습니다. 그러나 작동 방식을 알게 되면 웹과 기본 패턴의 장점을 혼합하여 사용자에게 원활하고 지나치지 않은 업데이트를 제공할 수 있습니다.

심도 있게 살펴보겠지만, 각 섹션의 시작 부분에 있는 글머리 기호에서 알아야 할 대부분의 항목을 다루고 있습니다.

인텐트

수명 주기는 다음을 목적으로 합니다.

  • 오프라인 우선을 가능하게 합니다.
  • 현재 서비스 워커를 중단하지 않고 새로운 서비스 워커를 준비할 수 있게 합니다.
  • 범위 내 페이지가 모두 동일한 서비스 워커로 제어됩니다 (또는 제어하는 서비스 워커가 없음).
  • 한 번에 한 버전의 사이트만 실행합니다.

마지막 목적은 매우 중요합니다. 서비스 워커가 없는 경우 사용자가 탭에 사이트를 로드한 다음, 다른 탭에 사이트를 새로 열 수 있습니다. 이 경우 두 버전의 사이트가 동시에 실행될 수 있습니다. 문제가 없는 경우도 있지만 저장소를 다루는 경우 두 개의 탭이 공유 저장소를 관리하는 방법에 대해 매우 다른 의견을 가질 수 있습니다. 이로 인해 오류가 발생하거나 최악의 경우에는 데이터 손실이 발생할 수 있습니다.

첫 번째 서비스 워커

간단히 말하면 다음과 같습니다.

  • install 이벤트는 서비스 워커가 받는 첫 번째 이벤트이며 한 번만 발생합니다.
  • installEvent.waitUntil()에 전달된 프라미스는 설치 기간과 설치 성공 또는 실패를 알립니다.
  • 서비스 워커는 설치가 완료되고 '활성' 상태가 될 때까지 fetchpush와 같은 이벤트를 수신하지 않습니다.
  • 페이지 요청 자체가 서비스 워커를 거치지 않는 한 기본적으로 페이지 가져오기는 서비스 워커를 거치지 않습니다. 따라서 서비스 워커의 효과를 보려면 페이지를 새로고침해야 합니다.
  • clients.claim()은 이 기본값을 재정의하고 제어되지 않는 페이지를 제어할 수 있습니다.

다음 HTML을 사용해 봅시다.

<!DOCTYPE html>
An image will appear here in 3 seconds:
<script>
  navigator.serviceWorker.register('/sw.js')
    .then(reg => console.log('SW registered!', reg))
    .catch(err => console.log('Boo!', err));

  setTimeout(() => {
    const img = new Image();
    img.src = '/dog.svg';
    document.body.appendChild(img);
  }, 3000);
</script>

이 코드는 서비스 워커를 등록하고 3초 후에 개 이미지를 추가합니다.

다음은 해당 서비스 워커인 sw.js입니다.

self.addEventListener('install', event => {
  console.log('V1 installing…');

  // cache a cat SVG
  event.waitUntil(
    caches.open('static-v1').then(cache => cache.add('/cat.svg'))
  );
});

self.addEventListener('activate', event => {
  console.log('V1 now ready to handle fetches!');
});

self.addEventListener('fetch', event => {
  const url = new URL(event.request.url);

  // serve the cat SVG from the cache if the request is
  // same-origin and the path is '/dog.svg'
  if (url.origin == location.origin && url.pathname == '/dog.svg') {
    event.respondWith(caches.match('/cat.svg'));
  }
});

이는 고양이 이미지를 캐시하고 /dog.svg 요청이 있을 때마다 고양이 이미지를 제공합니다. 그러나 위의 예시를 실행하면 페이지를 처음으로 로드할 때 개가 표시됩니다. 새로고침하면 고양이가 표시됩니다.

범위 및 제어

서비스 워커 등록의 기본 범위는 스크립트 URL에 상대적인 ./입니다. 즉, //example.com/foo/bar.js에 서비스 워커를 등록하면 기본 범위는 //example.com/foo/입니다.

페이지, 워커, 공유 워커를 clients라고 부릅니다. 서비스 워커는 범위 내에 있는 클라이언트만 제어할 수 있습니다. 클라이언트가 '제어되면' 가져오기는 범위 내 서비스 워커를 거칩니다. navigator.serviceWorker.controller를 통해 클라이언트의 제어 여부를 탐지할 수 있으며, null 또는 서비스 워커 인스턴스 중 하나로 나타납니다.

다운로드, 파싱, 실행

.register()를 호출하면 첫 번째 서비스 워커가 다운로드됩니다. 스크립트가 다운로드 또는 파싱하지 못하거나 초기 실행에서 오류가 발생하는 경우 레지스터 프라미스가 거부되고 서비스 워커가 삭제됩니다.

Chrome의 DevTools는 콘솔과 애플리케이션 탭의 서비스 워커 섹션에 오류를 표시합니다.

서비스 워커 DevTools 탭에 표시된 오류

설치

서비스 워커가 가져오는 첫 번째 이벤트는 install입니다. 해당 이벤트는 서비스 워커가 실행되는 즉시 트리거되고 서비스 워커당 한 번만 호출됩니다. 서비스 워커 스크립트를 변경하면 브라우저에서 다른 서비스 워커로 간주되며 고유한 install 이벤트를 가져옵니다. 업데이트 내용은 나중에 자세히 다루겠습니다.

install 이벤트는 클라이언트를 제어하기 전에 필요한 모든 것을 캐시할 수 있는 기회입니다. event.waitUntil()에 전달한 프라미스는 설치 완료 시점과 성공 여부를 브라우저에 알립니다.

프라미스가 거부되면 설치가 실패했다는 것을 알리고 브라우저가 서비스 워커를 버립니다. 이것은 클라이언트를 제어하지 않습니다. 즉, fetch 이벤트의 캐시에 있는 cat.svg에 의존할 수 있습니다. 이는 종속 항목입니다.

활성화

서비스 워커가 클라이언트를 제어하고 pushsync와 같은 함수 이벤트를 처리할 준비가 되면 activate 이벤트가 발생합니다. 그러나 .register()를 호출한 페이지가 제어된다는 의미는 아닙니다.

데모를 처음 로드할 때는 서비스 워커가 활성화된 지 한참 후에 dog.svg를 요청하더라도 요청을 처리하지 않고 여전히 개 이미지가 나타납니다. 기본값은 일관성이며, 페이지가 서비스 워커 없이 로드되면 하위 리소스의 경우에도 마찬가지입니다. 데모를 다시 로드하면 (즉, 페이지를 새로고침하면) 데모가 제어됩니다. 페이지와 이미지가 모두 fetch 이벤트를 거치고, 고양이가 대신 표시됩니다.

clients.claim

서비스 워커가 활성화되면 서비스 워커 내에서 clients.claim()를 호출하여 제어되지 않은 클라이언트를 제어할 수 있습니다.

다음은 activate 이벤트에서 clients.claim()를 호출하는 위 데모의 변형입니다. 처음에는 고양이가 보여야 합니다. 타이밍이 중요하기 때문에 '해야'라고 말합니다. 이미지가 로드를 시도하기 전에 서비스 워커가 활성화되고 clients.claim()이 적용될 경우에만 고양이를 보게 됩니다.

네트워크를 통한 로드와 다른 방식으로 페이지를 로드하도록 서비스 워커를 사용하는 경우, 서비스 워커 없이 로드한 일부 클라이언트를 서비스 워커가 제어하기 때문에 clients.claim()이 번거로울 수 있습니다.

서비스 워커 업데이트

간단히 말하면 다음과 같습니다.

  • 다음 중 하나가 발생하면 업데이트가 트리거됩니다.
    • 범위 내 페이지로의 탐색
    • 이전 24시간 내에 업데이트 확인이 없는 한 pushsync와 같은 함수 이벤트
    • 서비스 워커 URL이 변경된 경우에만 .register()를 호출합니다. 하지만 작업자 URL을 변경해서는 안 됩니다.
  • Chrome 68 이상을 비롯한 대부분의 브라우저는 등록된 서비스 워커 스크립트의 업데이트를 확인할 때 캐싱 헤더를 무시하는 것이 기본값입니다. importScripts()를 통해 서비스 워커 내에서 로드되는 리소스를 가져올 때는 여전히 캐싱 헤더를 고려합니다. 이 기본 동작은 서비스 워커를 등록할 때 updateViaCache 옵션을 설정하여 재정의할 수 있습니다.
  • 서비스 워커는 브라우저에 이미 있는 것과 바이트 하나만 달라도 업데이트된 것으로 간주됩니다. (이를 확장하여 가져온 스크립트/모듈도 포함할 것입니다.)
  • 업데이트된 서비스 워커는 기존 서비스 워커와 함께 시작되며 고유한 install 이벤트를 가져옵니다.
  • 새 워커가 비정상 상태 코드 (예: 404)이거나 파싱에 실패하거나 실행 중에 오류가 발생하거나 설치 중에 거부되면 새 워커는 버려지고 현재 워커는 활성 상태를 계속 유지합니다.
  • 성공적으로 설치되면 업데이트된 워커는 기존 워커가 제로 클라이언트를 제어할 때까지 wait합니다. (새로고침하는 동안 클라이언트가 중첩됩니다.)
  • self.skipWaiting()은 대기를 방지합니다. 즉, 설치를 마치자마자 서비스 워커가 활성화됩니다.

고양이가 아닌 말 그림으로 응답하도록 서비스 워커 스크립트를 변경했다고 가정해 보겠습니다.

const expectedCaches = ['static-v2'];

self.addEventListener('install', event => {
  console.log('V2 installing…');

  // cache a horse SVG into a new cache, static-v2
  event.waitUntil(
    caches.open('static-v2').then(cache => cache.add('/horse.svg'))
  );
});

self.addEventListener('activate', event => {
  // delete any caches that aren't in expectedCaches
  // which will get rid of static-v1
  event.waitUntil(
    caches.keys().then(keys => Promise.all(
      keys.map(key => {
        if (!expectedCaches.includes(key)) {
          return caches.delete(key);
        }
      })
    )).then(() => {
      console.log('V2 now ready to handle fetches!');
    })
  );
});

self.addEventListener('fetch', event => {
  const url = new URL(event.request.url);

  // serve the horse SVG from the cache if the request is
  // same-origin and the path is '/dog.svg'
  if (url.origin == location.origin && url.pathname == '/dog.svg') {
    event.respondWith(caches.match('/horse.svg'));
  }
});

위의 데모를 확인하세요. 여전히 고양이 이미지가 표시될 것입니다. 그 이유는 다음과 같습니다.

설치

캐시 이름을 static-v1에서 static-v2로 변경했습니다. 즉, 기존 서비스 워커가 계속 사용 중인 현재 캐시를 덮어쓰지 않고 새 캐시를 설정할 수 있다는 것을 의미합니다.

이 패턴은 네이티브 앱이 실행 파일과 함께 번들로 제공하는 자산과 유사한 버전 특정 캐시를 만듭니다. avatars와 같이 버전 불특정 캐시도 있을 수 있습니다.

대기 중

설치 후 업데이트된 서비스 워커는 기존 서비스 워커가 더 이상 클라이언트를 제어하지 않을 때까지 활성화를 지연시킵니다. 이 상태를 '대기 중'이라고 하며, 브라우저가 한 번에 한 버전의 서비스 워커만 실행되도록 보장하는 방법입니다.

업데이트된 데모를 실행한 경우 V2 워커가 아직 활성화되지 않았으므로 고양이 그림이 계속 표시될 것입니다. DevTools의 'Application' 탭에서 대기 중인 새 서비스 워커를 볼 수 있습니다.

새 서비스 워커가 대기 중임을 보여주는 DevTools

데모에 열린 탭이 하나뿐인 경우에도 페이지를 새로고침하는 것만으로는 새 버전으로 전환되지 않습니다. 이는 브라우저 탐색이 작동 방식 때문입니다. 탐색할 때 응답 헤더가 수신될 때까지 현재 페이지가 사라지지 않으며, 응답에 Content-Disposition 헤더가 있으면 현재 페이지가 유지될 수 있습니다. 이 중첩 때문에 현재 서비스 워커는 새로고침하는 동안 항상 클라이언트를 제어합니다.

업데이트를 가져오려면 현재 서비스 워커를 사용하는 모든 탭을 닫거나 탭에서 벗어난 곳으로 이동합니다. 그런 다음 다시 데모로 이동하면 말이 표시됩니다.

이 패턴은 Chrome의 업데이트 방식과 유사합니다. Chrome 업데이트는 백그라운드에서 다운로드되지만 Chrome이 다시 시작될 때까지 적용되지 않습니다. 그동안 현재 버전을 중단 없이 계속 사용할 수 있습니다. 하지만 개발 단계에서는 고통스러운 작업이지만 DevTools는 이를 훨씬 쉽게 만드는 방법을 제공합니다. 이에 대해서는 이 글의 뒷부분에서 다루겠습니다.

활성화

이전 서비스 워커가 사라지고 새 서비스 워커가 클라이언트를 제어할 수 있는 경우 이 작업이 시작됩니다. 이때가 이전 워커가 아직 사용 중인 동안 할 수 없던 작업(예: 데이터베이스 마이그레이션 및 캐시 지우기)을 수행하기에 이상적인 시간입니다.

위의 데모에서 있을 것으로 예상되는 캐시 목록을 유지하고 activate 이벤트에서 다른 캐시를 제거하여 이전 static-v1 캐시를 제거합니다.

event.waitUntil()에 프라미스를 전달하면 프라미스가 해결될 때까지 함수 이벤트 (fetch, push, sync 등)가 버퍼링됩니다. 따라서 fetch 이벤트가 발생하면 활성화가 완전히 완료된 것입니다.

대기 단계 건너뛰기

대기 단계는 한 번에 하나의 사이트 버전만 실행하고 있음을 의미하지만 해당 기능이 필요하지 않은 경우 self.skipWaiting()를 호출하여 새 서비스 워커를 더 빨리 활성화할 수 있습니다.

그러면 서비스 워커가 현재 진행 중인 워커를 퇴장시키고 대기 단계에 진입하자마자 (또는 이미 대기 단계에 있는 경우 즉시) 활성화됩니다. 이 경우 워커가 설치를 건너뛰는 것이 아니라 단지 대기할 뿐입니다.

대기 중 또는 대기 전에 해당하는 한 skipWaiting() 호출 시간은 별로 중요하지 않습니다. install 이벤트에서 이를 호출하는 것은 매우 흔한 경우입니다.

self.addEventListener('install', event => {
  self.skipWaiting();

  event.waitUntil(
    // caching etc
  );
});

그러나 서비스 워커에 대한 postMessage()의 결과로 이를 호출하기를 원할 수도 있습니다. 현 상태 그대로, 사용자 상호작용에 따라 skipWaiting()을 수행할 수 있습니다.

skipWaiting()를 사용하는 데모 다른 곳을 탐색하지 않아도 소 그림이 표시될 것입니다. clients.claim()과 마찬가지로 이것은 경쟁이므로 페이지가 이미지를 로드하려고 시도하기 전에 새 서비스 워커가 가져와서 설치하고 활성화하는 경우에만 소가 표시됩니다.

수동 업데이트

이전에 언급했듯이 브라우저는 탐색 및 함수 이벤트 후에 자동으로 업데이트를 확인하지만 수동으로 트리거할 수도 있습니다.

navigator.serviceWorker.register('/sw.js').then(reg => {
  // sometime later…
  reg.update();
});

사용자가 다시 로드하지 않고 오랫동안 사이트를 사용할 것으로 예상되는 경우 특정 간격 (예: 매시간)으로 update()를 호출할 수 있습니다.

서비스 워커 스크립트의 URL 변경 금지

캐싱 권장사항에 관한 저자의 게시물을 읽었다면 서비스 워커의 각 버전에 고유한 URL을 제공하는 것이 좋습니다. 이렇게 하지 마세요. 일반적으로 서비스 워커에 좋지 않은 방법입니다. 현재 위치에서 스크립트를 업데이트하면 됩니다.

이 경우 다음과 같은 문제가 발생할 수 있습니다.

  1. index.htmlsw-v1.js를 서비스 워커로 등록합니다.
  2. sw-v1.jsindex.html를 캐시하고 제공하므로 오프라인 우선으로 작동합니다.
  3. 새롭고 멋진 sw-v2.js를 등록하도록 index.html를 업데이트합니다.

위의 작업을 수행하면 sw-v1.js가 캐시에서 이전 버전의 index.html을 제공하기 때문에 사용자는 sw-v2.js를 받지 못합니다. 서비스 워커를 업데이트하기 위해 서비스 워커를 업데이트해야 하는 상황에 놓였습니다. 으악.

그러나 위의 데모에서는 서비스 워커의 URL을 변경했습니다. 이렇게 한 것은 버전을 전환하는 데모를 위해서 입니다. 프로덕션에서는 이렇게 하지 않습니다.

간편한 개발

서비스 워커 수명 주기는 사용자를 염두에 두고 작성되었지만 개발 중에는 약간의 어려움이 있습니다. 고맙게도 도움이 되는 몇 가지 도구가 있습니다.

새로고침 시 업데이트

제가 가장 좋아하는 도구입니다.

&#39;새로고침 시 업데이트&#39;를 표시하는 DevTools

이 도구는 개발자 친화적으로 수명 주기를 변경합니다. 각 탐색은 다음을 수행합니다.

  1. 서비스 워커를 다시 가져옵니다.
  2. 바이트 하나하나가 동일한 경우에도 새 버전으로 설치합니다. 즉, install 이벤트가 실행되고 캐시가 업데이트됩니다.
  3. 새로운 서비스 워커가 활성화되도록 대기 단계를 건너뜁니다.
  4. 페이지를 탐색합니다.

즉, 두 번 새로고침하거나 탭을 닫지 않고도 각 탐색 (새로고침 포함)에 대한 업데이트를 받습니다.

대기 건너뛰기

&#39;skip waiting&#39;을 표시하는 DevTools

워커가 대기 중인 경우 DevTools에서 'skip waiting'을 눌러 즉시 '활성' 상태로 승격할 수 있습니다.

Shift-reload

페이지를 강제로 새로고침하면 (Shift-새로고침) 서비스 워커를 완전히 우회합니다. 이는 통제가 안 될 것입니다. 이 기능은 사양에 있으므로 서비스 워커를 지원하는 다른 브라우저에서도 작동합니다.

업데이트 처리

서비스 워커는 확장 가능한 웹의 일부로 설계되었습니다. 일반적으로 우리들, 그러니까 브라우저 개발자들은 웹 개발자에 비해 웹 개발 실력이 떨어진다는 것을 염두에 둔 것입니다. 따라서 브라우저 개발자는 자신이 좋아하는 패턴을 사용하여 특정 문제를 해결하는 협소한 고수준 API를 제공하는 대신, 브라우저의 기능에 대한 액세스를 허용하고 사용자에게 가장 적합한 방식을 고려하여 작업을 허용해야 합니다.

따라서 가능한 한 많은 패턴을 활성화하려면 전체 업데이트 주기를 식별할 수 있어야 합니다.

navigator.serviceWorker.register('/sw.js').then(reg => {
  reg.installing; // the installing worker, or undefined
  reg.waiting; // the waiting worker, or undefined
  reg.active; // the active worker, or undefined

  reg.addEventListener('updatefound', () => {
    // A wild service worker has appeared in reg.installing!
    const newWorker = reg.installing;

    newWorker.state;
    // "installing" - the install event has fired, but not yet complete
    // "installed"  - install complete
    // "activating" - the activate event has fired, but not yet complete
    // "activated"  - fully active
    // "redundant"  - discarded. Either failed install, or it's been
    //                replaced by a newer version

    newWorker.addEventListener('statechange', () => {
      // newWorker.state has changed
    });
  });
});

navigator.serviceWorker.addEventListener('controllerchange', () => {
  // This fires when the service worker controlling this page
  // changes, eg a new worker has skipped waiting and become
  // the new active worker.
});

수명 주기는 계속 진행됩니다.

보시다시피 서비스 워커 수명 주기를 이해하는 것이 좋습니다. 이를 이해하면 서비스 워커 동작이 더 논리적이고 신비롭지 않게 보일 것입니다. 이 지식을 바탕으로 서비스 워커를 배포하고 업데이트할 때 더 자신감을 가질 수 있습니다.