서비스 워커 사고방식

서비스 워커를 생각할 때 고려해야 할 사항

서비스 워커는 강력하며 반드시 알아야 할 기술입니다. 이를 통해 사용자에게 완전히 새로운 차원의 환경을 제공할 수 있습니다. 사이트가 즉시 로드될 수 있습니다. 오프라인에서도 작동할 수 있습니다. 웹의 도달범위와 자유를 갖춘 플랫폼별 앱으로 설치할 수 있으며, 웹의 도달범위와 자유를 갖춘 플랫폼별 앱으로 설치할 수 있으며,

하지만 서비스 워커는 대부분의 웹 개발자가 익숙한 것과는 다릅니다. 습득하기가 쉽지 않고 주의해야 할 몇 가지 문제가 있습니다.

Google 개발자와 저는 최근 서비스 워커를 이해하기 위한 무료 게임인 Service Workies 프로젝트를 공동으로 진행했습니다. 빌드하고 서비스 워커의 복잡한 내부 작동 방식을 다루는 동안 몇 가지 문제가 발생했습니다. 가장 도움이 된 것은 묘사적인 비유를 생각해 내는 것이었습니다. 이 게시물에서는 이러한 멘탈 모델을 살펴보고 서비스 워커를 까다롭고도 멋지게 만드는 역설적인 특성을 알아봅니다.

동일하지만 다름

서비스 워커를 코딩하는 동안 많은 부분이 익숙하게 느껴질 것입니다. 좋아하는 새로운 JavaScript 언어 기능을 사용할 수 있습니다. UI 이벤트와 마찬가지로 수명 주기 이벤트를 수신 대기합니다. 평소와 같이 프로미스를 사용하여 제어 흐름을 관리합니다.

하지만 다른 서비스 워커 동작은 혼란스러워 머리를 긁적거리게 만듭니다. 특히 페이지를 새로고침해도 코드 변경사항이 적용되지 않는 경우

새 레이어

일반적으로 사이트를 빌드할 때는 클라이언트와 서버라는 두 가지 레이어만 고려하면 됩니다. 서비스 워커는 중간에 있는 완전히 새로운 레이어입니다.

서비스 워커는 클라이언트와 서버 간의 중간 레이어 역할을 합니다.

서비스 워커는 사이트에서 사용자의 브라우저에 설치할 수 있는 일종의 브라우저 확장 프로그램이라고 생각하면 됩니다. 설치된 서비스 워커는 강력한 미들 레이어를 사용하여 사이트의 브라우저를 확장합니다. 이 서비스 워커 레이어는 사이트에서 실행하는 모든 요청을 가로채고 처리할 수 있습니다.

서비스 워커 레이어에는 브라우저 탭과는 별개인 자체 수명 주기가 있습니다. 페이지 새로고침만으로는 서비스 워커를 업데이트할 수 없습니다. 서버에 배포된 코드가 페이지 새로고침으로 업데이트되지 않는 것과 마찬가지입니다. 각 레이어에는 업데이트에 관한 고유한 규칙이 있습니다.

Service Workies 게임에서는 서비스 워커 수명 주기에 관한 다양한 세부정보를 다루고 이를 활용하는 방법을 다수 연습할 수 있습니다.

강력하지만 제한적

사이트에 서비스 워커를 사용하면 놀라운 이점을 얻을 수 있습니다. 사이트에서 다음 작업을 할 수 있습니다.

  • 사용자가 오프라인 상태일 때도 원활하게 작동
  • 캐싱을 통해 성능을 크게 개선합니다.
  • 푸시 알림을 사용합니다.
  • PWA로 설치됩니다.

서비스 워커는 할 수 있는 일이 많지만 설계상 제한사항이 있습니다. 사이트와 동기식으로 또는 동일한 스레드에서 작업할 수는 없습니다. 즉, 다음 항목에 액세스할 수 없습니다.

  • localStorage
  • DOM

다행히 페이지에서 서비스 워커와 통신할 수 있는 방법은 몇 가지가 있습니다. 직접 postMessage, 일대일 메시지 채널, 일대다 브로드캐스트 채널 등이 있습니다.

장기 지속이지만 단기 지속

활성 상태의 서비스 워커는 사용자가 사이트를 나가거나 탭을 닫은 후에도 계속 실행됩니다. 브라우저는 사용자가 다음에 사이트를 다시 방문할 때 준비되도록 이 서비스 워커를 유지합니다. 첫 번째 요청이 이루어지기 전에 서비스 워커는 요청을 가로채 페이지를 제어할 수 있습니다. 이를 통해 사이트가 오프라인에서 작동할 수 있습니다. 서비스 워커는 사용자가 인터넷에 연결되어 있지 않더라도 페이지 자체의 캐시된 버전을 제공할 수 있습니다.

Service Workies에서는 Kolohe (친절한 서비스 워커)가 요청을 가로채고 처리하는 방식으로 이 개념을 시각화합니다.

중지됨

서비스 워커는 영원히 실행되는 것처럼 보이지만 거의 언제든지 중지될 수 있습니다. 브라우저는 현재 아무것도 하지 않는 서비스 워커에 리소스를 낭비하지 않으려고 합니다. 중지되는 것은 종료되는 것과는 다릅니다. 서비스 워커는 설치되고 활성화된 상태로 유지됩니다. 절전 모드로 전환됩니다. 다음에 필요하면 (예: 요청을 처리하기 위해) 브라우저에서 다시 깨웁니다.

waitUntil

서비스 워커는 언제든지 절전 모드로 전환될 수 있으므로 중요한 작업을 하고 있고 낮잠을 자고 싶지 않다고 브라우저에 알릴 방법이 필요합니다. 여기서 event.waitUntil()가 사용됩니다. 이 메서드는 사용되는 수명 주기를 확장하여 준비될 때까지 수명 주기가 중지되거나 수명 주기의 다음 단계로 넘어가지 않도록 합니다. 이렇게 하면 캐시를 설정하고 네트워크에서 리소스를 가져오는 등의 작업을 할 수 있습니다.

이 예에서는 assets 캐시가 생성되고 검의 그림으로 채워질 때까지 서비스 워커 설치가 완료되지 않았다고 브라우저에 알립니다.

self.addEventListener("install", event => {
  event.waitUntil(
    caches.open("assets").then(cache => {
      return cache.addAll(["/weapons/sword/blade.png"]);
    })
  );
});

전역 상태 주의

이러한 시작/중지가 발생하면 서비스 워커의 전역 범위가 재설정됩니다. 따라서 서비스 워커에서 전역 상태를 사용하지 않도록 주의하세요. 그렇지 않으면 다음에 서비스 워커가 다시 시작될 때 예상과 다른 상태가 되어 곤란해질 수 있습니다.

전역 상태를 사용하는 다음 예를 살펴보세요.

const favoriteNumber = Math.random();
let hasHandledARequest = false;

self.addEventListener("fetch", event => {
  console.log(favoriteNumber);
  console.log(hasHandledARequest);
  hasHandledARequest = true;
});

이 서비스 워커는 요청이 있을 때마다 0.13981866382421893과 같은 숫자를 기록합니다. hasHandledARequest 변수도 true로 변경됩니다. 이제 서비스 워커가 잠시 유휴 상태가 되므로 브라우저에서 이를 중지합니다. 다음에 요청이 있으면 서비스 워커가 다시 필요하므로 브라우저가 이를 깨웁니다. 스크립트가 다시 평가됩니다. 이제 hasHandledARequestfalse로 재설정되고 favoriteNumber는 완전히 다른 값인 0.5907281835659033이 됩니다.

서비스 워커에서는 저장된 상태를 사용할 수 없습니다. 또한 메시지 채널과 같은 항목의 인스턴스를 만들면 버그가 발생할 수 있습니다. 서비스 워커가 중지/시작될 때마다 새 인스턴스가 생성되기 때문입니다.

서비스 워커 3장에서는 중지된 서비스 워커가 깨어나기를 기다리는 동안 모든 색상을 잃는 것으로 시각화합니다.

중지된 서비스 워커의 시각화

함께, 하지만 별개

페이지는 한 번에 하나의 서비스 워커에서만 제어할 수 있습니다. 하지만 한 번에 두 개의 서비스 워커를 설치할 수 있습니다. 서비스 워커 코드를 변경하고 페이지를 새로고침해도 실제로는 서비스 워커를 수정하지 않습니다. 서비스 워커는 immutable. 대신 새 계정을 만들고 있습니다. 이 새 서비스 워커 (SW2라고 함)는 설치되지만 아직 활성화되지는 않습니다. 현재 서비스 워커 (SW1)가 종료될 때까지 (사용자가 사이트를 떠날 때) 대기해야 합니다.

다른 서비스 워커의 캐시를 조작함

설치하는 동안 SW2는 일반적으로 캐시를 만들고 채우는 등의 설정을 할 수 있습니다. 단, 이 새 서비스 워커는 현재 서비스 워커가 액세스할 수 있는 모든 항목에 액세스할 수 있습니다. 주의하지 않으면 대기 중인 새 서비스 워커가 현재 서비스 워커를 완전히 망칠 수 있습니다. 다음과 같은 경우 문제가 발생할 수 있습니다.

  • SW2는 SW1에서 활발하게 사용 중인 캐시를 삭제할 수 있습니다.
  • SW2가 SW1에서 사용 중인 캐시의 콘텐츠를 수정하면 SW1이 페이지에서 예상하지 못한 애셋으로 응답할 수 있습니다.

skipWaiting 건너뛰기

서비스 워커는 위험한 skipWaiting() 메서드를 사용하여 설치가 완료되는 즉시 페이지를 제어할 수도 있습니다. 버그가 있는 서비스 워커를 의도적으로 교체하려는 경우가 아니라면 일반적으로 좋지 않은 방법입니다. 새 서비스 워커가 현재 페이지에서 예상하지 못한 업데이트된 리소스를 사용하고 있을 수 있으므로 오류와 버그가 발생할 수 있습니다.

클린 시작

서비스 워커가 서로 충돌하지 않도록 하는 방법은 서로 다른 캐시를 사용하는 것입니다. 이를 실행하는 가장 쉬운 방법은 사용하는 캐시 이름의 버전을 지정하는 것입니다.

const version = 1;
const assetCacheName = `assets-${version}`;

self.addEventListener("install", event => {
  caches.open(assetCacheName).then(cache => {
    // confidently do stuff with your very own cache
  });
});

새 서비스 워커를 배포할 때는 이전 서비스 워커와 완전히 별개의 캐시로 필요한 작업을 실행하도록 version를 범프합니다.

캐시 시각화

깔끔한 마무리

서비스 워커가 activated 상태에 도달하면 서비스 워커가 인계되었음을 알 수 있으며 이전 서비스 워커는 중복됩니다. 이 시점에서 이전 서비스 워커를 정리하는 것이 중요합니다. 사용자의 캐시 저장용량 한도를 준수할 뿐만 아니라 의도하지 않은 버그도 방지할 수 있습니다.

caches.match() 메서드는 일치하는 항목이 있는 모든 캐시에서 항목을 검색하는 데 자주 사용되는 바로가기입니다. 하지만 생성된 순서대로 캐시를 반복합니다. 예를 들어 assets-1assets-2라는 두 가지 캐시에서 스크립트 파일 app.js의 두 가지 버전이 있다고 가정해 보겠습니다. 페이지에서 assets-2에 저장된 최신 스크립트를 예상합니다. 하지만 이전 캐시를 삭제하지 않은 경우 caches.match('app.js')assets-1에서 이전 캐시를 반환하며 사이트가 다운될 가능성이 큽니다.

이전 서비스 워커를 정리하는 데 필요한 작업은 새 서비스 워커에 필요하지 않은 캐시를 삭제하는 것뿐입니다.

const version = 2;
const assetCacheName = `assets-${version}`;

self.addEventListener("activate", event => {
  event.waitUntil(
    caches.keys().then(cacheNames => {
      return Promise.all(
        cacheNames.map(cacheName => {
          if (cacheName !== assetCacheName){
            return caches.delete(cacheName);
          }
        });
      );
    });
  );
});

서비스 워커가 서로 충돌하지 않도록 하는 데는 약간의 노력과 규율이 필요하지만 그만한 가치가 있습니다.

서비스 워커 사고방식

서비스 워커를 생각할 때 올바른 사고방식을 갖추면 자신 있게 빌드할 수 있습니다. 이를 숙지하면 사용자에게 멋진 환경을 제공할 수 있습니다.

게임을 플레이하여 이 모든 것을 이해하고 싶다면 좋은 소식이 있습니다. Service Workies를 플레이하여 오프라인 괴물을 처치하기 위한 서비스 워커의 방법을 알아보세요.