서비스 워커 수명 주기

Jake Archibald
Jake Archibald

서비스 워커의 수명 주기는 가장 복잡한 부분입니다. AI가 의도하는 것이 무엇인지, 어떤 이점이 있는지 모른다면 싸움을 하는 것처럼 느껴질 수 있습니다. 하지만 작동 방식을 알면 최고의 웹 및 네이티브 패턴을 혼합하여 사용자에게 원활하고 방해되지 않는 업데이트를 제공할 수 있습니다.

자세히 살펴보겠지만 각 섹션의 시작 부분에 있는 글머리기호가 알아야 할 대부분의 내용을 다루고 있습니다.

인텐트

수명 주기의 목적은 다음과 같습니다.

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

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

첫 번째 서비스 워커는

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

  • 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가 요청되더라도 요청이 처리되지 않고 여전히 반려견 이미지가 표시됩니다. 기본값은 consistency이며, 페이지가 서비스 워커 없이 로드되면 하위 리소스도 로드되지 않습니다. 데모를 다시 로드하면 (즉, 페이지를 새로고침) 제어됩니다. 페이지와 이미지가 모두 fetch 이벤트를 거치고 대신 고양이가 표시됩니다.

clients.claim

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

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

서비스 워커를 사용하여 네트워크를 통해 로드하는 것과 다른 방식으로 페이지를 로드하는 경우, 서비스 워커가 페이지 없이 로드되는 일부 클라이언트를 서비스 워커가 제어하게 되므로 clients.claim()가 번거로울 수 있습니다.

서비스 워커 업데이트

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

  • 다음 중 하나가 발생하면 업데이트가 트리거됩니다.
    • 범위 내 페이지로의 탐색
    • pushsync와 같은 기능 이벤트(이전 24시간 이내에 업데이트 검사가 없는 경우)
    • 서비스 워커 URL이 변경된 경우에만 .register() 호출 그러나 작업자 URL 변경은 피해야 합니다.
  • Chrome 68 이상을 비롯한 대부분의 브라우저는 등록된 서비스 워커 스크립트의 업데이트를 확인할 때 캐싱 헤더를 무시하도록 기본 설정됩니다. importScripts()를 통해 서비스 워커 내에서 로드된 리소스를 가져올 때는 여전히 캐싱 헤더를 고려합니다. 서비스 워커를 등록할 때 updateViaCache 옵션을 설정하여 이 기본 동작을 재정의할 수 있습니다.
  • 서비스 워커는 브라우저에 이미 있는 것과 바이트가 다른 경우 업데이트된 것으로 간주됩니다. (이를 확장하여 가져온 스크립트/모듈도 포함할 것입니다.)
  • 업데이트된 서비스 워커는 기존 서비스 워커와 함께 시작되며 자체 install 이벤트를 가져옵니다.
  • 새 worker가 비정상 상태 코드 (예: 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.jsindex.html를 캐시하고 제공하므로 오프라인 상태에서 먼저 작동합니다.
  3. 새롭고 멋진 sw-v2.js를 등록하도록 index.html를 업데이트합니다.

위의 작업을 실행하면 sw-v1.js가 캐시에서 이전 버전의 index.html를 제공하므로 사용자는 sw-v2.js를 가져오지 않습니다. 서비스 워커를 업데이트하기 위해 서비스 워커를 업데이트해야 하는 상황에 처했습니다. 어.

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

손쉬운 개발

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

새로고침 시 업데이트

제가 제일 좋아하는 거예요.

&#39;Update on refresh(새로고침 시 업데이트)&#39;를 표시하는 DevTools

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

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

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

대기 건너뛰기

&#39;Skip pending(대기 중 건너뛰기)&#39;를 표시하는 DevTools

작업자가 대기 중인 경우 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.
});

수명 주기는 계속하여

보시다시피, 서비스 워커 수명 주기를 이해하는 것이 유익하며, 이를 이해하면 서비스 워커의 행동이 보다 논리적이고 신비로워 보일 것입니다. 이 지식을 통해 서비스 워커를 배포하고 업데이트할 때 더 자신감을 가질 수 있습니다.