서비스 워커 수명 주기

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라고 합니다. 서비스 워커는 범위 내에 있는 클라이언트만 제어할 수 있습니다. 클라이언트가 '제어되면' 가져오기는 범위 내 서비스 워커를 통해 이루어집니다. 클라이언트가 null 또는 서비스 워커 인스턴스인 navigator.serviceWorker.controller를 통해 제어되는지 감지할 수 있습니다.

다운로드, 파싱, 실행

.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)이거나 파싱에 실패하거나 실행 중에 오류가 발생하거나 설치 중에 거부되면 새 워커는 버려지고 현재 워커는 활성 상태로 유지됩니다.
  • 성공적으로 설치되면 업데이트된 worker는 기존 worker가 제로 클라이언트를 제어할 때까지 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 worker가 아직 활성화되지 않았으므로 고양이 사진이 계속 표시됩니다. DevTools의 'Application' 탭에서 대기 중인 새 서비스 워커를 볼 수 있습니다.

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

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

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

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

활성화

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

위의 데모에서는 있을 것으로 예상되는 캐시 목록을 유지하고 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.js는 오프라인 우선 작동하도록 index.html를 캐시하고 제공합니다.
  3. index.html를 업데이트하여 새롭고 반짝이는 sw-v2.js를 등록하도록 합니다.

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

하지만 위의 데모에서는 서비스 워커의 URL을 변경했습니다. 데모를 위해 버전 간에 전환할 수 있도록 했습니다. 프로덕션에서는 할 일이 아닙니다.

손쉬운 개발 작업

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

새로고침 시 업데이트

내가 가장 좋아하는 선택이야.

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

이렇게 하면 개발자 친화적으로 수명 주기가 변경됩니다. 각 탐색은 다음 작업을 실행합니다.

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

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

대기 건너뛰기

&#39;건너뛰기 대기&#39;를 표시하는 DevTools

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

Shift + 새로고침

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

업데이트 처리

서비스 워커는 확장 가능한 웹의 일부로 설계되었습니다. 개념은 브라우저 개발자인 우리는 웹 개발자보다 웹 개발에 더 능숙하지 않다는 점을 인정하는 것입니다. 따라서 Google에서는 자신이 좋아하는 패턴을 사용하여 특정 문제를 해결하는 좁은 고수준 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.
});

수명 주기는 계속됩니다

보시다시피 서비스 워커 수명 주기를 이해하는 것이 유익합니다. 이러한 이해를 바탕으로 서비스 워커의 행동은 더 논리적이고 덜 신비롭게 보일 것입니다. 이러한 지식을 바탕으로 서비스 워커를 배포하고 업데이트할 때 더 많은 확신을 얻을 수 있습니다.