서비스 워커 캐싱 전략

지금까지는 Cache 인터페이스의 멘션과 아주 작은 코드 스니펫만 있었습니다. 서비스 워커를 효과적으로 사용하려면 캐싱 전략을 하나 이상 채택해야 하며, 이를 위해서는 Cache 인터페이스를 어느 정도 숙지해야 합니다.

캐싱 전략은 서비스 워커의 fetch 이벤트와 Cache 인터페이스 간의 상호작용입니다. 캐싱 전략의 작성 방법은 다릅니다. 예를 들어 정적 애셋에 대한 요청을 문서와 다르게 처리하는 것이 좋을 수 있으며, 이는 캐싱 전략 구성 방식에 영향을 미칩니다.

전략을 자세히 알아보기 전에 잠시 동안 Cache 인터페이스가 아닌지, 무엇인지, 그리고 서비스 워커 캐시를 관리하는 데 사용되는 몇 가지 메서드에 대해 간략히 살펴보겠습니다.

Cache 인터페이스와 HTTP 캐시 비교

이전에 Cache 인터페이스를 사용해 본 적이 없다면 HTTP 캐시와 동일하거나 최소한 HTTP 캐시와 관련이 있다고 생각하고 싶을 수 있습니다. 호출은 건너뛸 수 없습니다.

  • Cache 인터페이스는 HTTP 캐시와 완전히 분리된 캐싱 메커니즘입니다.
  • HTTP 캐시에 영향을 미치는 데 사용하는 Cache-Control 구성은 Cache 인터페이스에 저장되는 애셋에 영향을 미치지 않습니다.

브라우저 캐시를 계층으로 생각하는 것이 좋습니다. HTTP 캐시는 HTTP 헤더에 표현된 지시어가 있는 키-값 쌍으로 구동되는 하위 수준 캐시입니다.

이에 반해 Cache 인터페이스는 JavaScript API에 의해 구동되는 상위 수준 캐시입니다. 비교적 단순한 HTTP 키-값 쌍을 사용할 때보다 유연성이 높으며, 이러한 방식은 캐싱 전략을 가능케 하는 요소 중 하나이기도 합니다. 서비스 워커 캐시와 관련하여 중요한 API 메서드는 다음과 같습니다.

예시는 극히 일부에 불과합니다. 다른 유용한 메서드도 있지만 이것들은 이 가이드의 뒷부분에서 사용하게 될 기본적인 메서드들입니다.

겸손한 fetch 이벤트

캐싱 전략의 나머지 절반은 서비스 워커의 fetch 이벤트입니다. 지금까지 이 문서에서 '네트워크 요청 가로채기'에 관해 조금 알아봤습니다. 서비스 워커 내의 fetch 이벤트에서 이러한 상황이 발생합니다.

// Establish a cache name
const cacheName = 'MyFancyCacheName_v1';

self.addEventListener('install', (event) => {
  event.waitUntil(caches.open(cacheName));
});

self.addEventListener('fetch', async (event) => {
  // Is this a request for an image?
  if (event.request.destination === 'image') {
    // Open the cache
    event.respondWith(caches.open(cacheName).then((cache) => {
      // Respond with the image from the cache or from the network
      return cache.match(event.request).then((cachedResponse) => {
        return cachedResponse || fetch(event.request.url).then((fetchedResponse) => {
          // Add the network response to the cache for future visits.
          // Note: we need to make a copy of the response to save it in
          // the cache and use the original as the request response.
          cache.put(event.request, fetchedResponse.clone());

          // Return the network response
          return fetchedResponse;
        });
      });
    }));
  } else {
    return;
  }
});

이는 장난감 예이며 실제로 직접 볼 수 있는 사례이지만 서비스 워커가 할 수 있는 작업을 엿볼 수 있습니다. 위의 코드는 다음을 수행합니다.

  1. 요청의 destination 속성을 검사하여 이미지 요청인지 확인합니다.
  2. 이미지가 서비스 워커 캐시에 있으면 거기에서 제공합니다. 그렇지 않은 경우 네트워크에서 이미지를 가져오고 응답을 캐시에 저장한 후 네트워크 응답을 반환합니다.
  3. 다른 모든 요청은 캐시와의 상호작용 없이 서비스 워커를 통해 전달됩니다.

가져오기의 event 객체에는 각 요청의 유형을 식별하는 데 유용한 몇 가지 유용한 정보인 request 속성이 포함되어 있습니다.

  • url: 현재 fetch 이벤트에서 처리 중인 네트워크 요청의 URL입니다.
  • method: 요청 메서드 (예: GET 또는 POST)를 사용하려고 합니다.
  • mode - 요청의 모드를 설명합니다. 'navigate' 값은 HTML 문서에 대한 요청을 다른 요청과 구별하는 데 자주 사용됩니다.
  • destination: 요청된 애셋의 파일 확장자를 사용하지 않는 방식으로 요청되는 콘텐츠의 유형을 설명합니다.

이 게임의 이름은 asynchrony입니다. install 이벤트는 프로미스를 취하고 문제가 해결될 때까지 기다린 후 활성화를 계속 진행하는 event.waitUntil 메서드를 제공합니다. fetch 이벤트는 비동기 fetch 요청의 결과 또는 Cache 인터페이스의 match 메서드에서 반환된 응답을 반환하는 데 사용할 수 있는 유사한 event.respondWith 메서드를 제공합니다.

캐싱 전략

이제 Cache 인스턴스와 fetch 이벤트 핸들러에 익숙해졌으므로 몇 가지 서비스 워커 캐싱 전략을 살펴볼 차례입니다. 가능성은 사실상 무한하지만 이 가이드에서는 Workbox와 함께 제공되는 전략을 설명하므로 Workbox 내부에서 어떤 일이 일어나는지 파악할 수 있습니다.

캐시 전용

페이지에서 서비스 워커로, 그리고 캐시로의 흐름을 보여줍니다.

'캐시 전용'이라는 간단한 캐싱 전략으로 시작해 보겠습니다. 단지 서비스 워커가 페이지를 제어할 때 일치하는 요청은 캐시로만 이동합니다. 즉, 캐시된 애셋을 사전 캐시해야 패턴을 사용할 수 있으며, 이러한 애셋은 서비스 워커가 업데이트될 때까지 캐시에서 업데이트되지 않습니다.

// Establish a cache name
const cacheName = 'MyFancyCacheName_v1';

// Assets to precache
const precachedAssets = [
  '/possum1.jpg',
  '/possum2.jpg',
  '/possum3.jpg',
  '/possum4.jpg'
];

self.addEventListener('install', (event) => {
  // Precache assets on install
  event.waitUntil(caches.open(cacheName).then((cache) => {
    return cache.addAll(precachedAssets);
  }));
});

self.addEventListener('fetch', (event) => {
  // Is this one of our precached assets?
  const url = new URL(event.request.url);
  const isPrecachedRequest = precachedAssets.includes(url.pathname);

  if (isPrecachedRequest) {
    // Grab the precached asset from the cache
    event.respondWith(caches.open(cacheName).then((cache) => {
      return cache.match(event.request.url);
    }));
  } else {
    // Go to the network
    return;
  }
});

위에서 애셋 배열은 설치 시 사전 캐시됩니다. 서비스 워커가 가져오기를 처리할 때 fetch 이벤트에 의해 처리되는 요청 URL이 사전 캐시된 애셋 배열에 있는지 확인합니다. 이 경우, 캐시에서 리소스를 가져오고 네트워크를 건너뜁니다. 다른 요청은 네트워크로 전달되며 네트워크만 통과됩니다. 이 전략의 실제 동작을 보려면 콘솔을 열어 둔 상태에서 이 데모를 확인하세요.

네트워크 전용

페이지에서 서비스 워커, 네트워크로의 흐름을 보여줍니다.

'캐시 전용'의 반대는 '네트워크 전용'으로, 요청이 서비스 워커 캐시와의 상호작용 없이 서비스 워커를 통해 네트워크로 전달됩니다. 이는 콘텐츠의 최신성을 보장하는 좋은 전략이지만 (마크업을 생각하세요) 사용자가 오프라인일 때는 작동하지 않는다는 단점이 있습니다.

요청이 네트워크로 전달된다는 것은 일치하는 요청에 event.respondWith를 호출하지 않는다는 의미일 뿐입니다. 명시적으로 지정하려면 네트워크에 전달하려는 요청의 fetch 이벤트 콜백에서 빈 return;를 치면 됩니다. 이는 '캐시 전용' 전략 데모에서 사전 캐시되지 않은 요청에 대해 발생하는 상황입니다.

캐시 우선, 네트워크로 복귀

페이지에서 서비스 워커, 캐시, 네트워크로의 흐름을 보여줍니다(캐시에 없는 경우).

이 전략에서는 상황이 조금 더 복잡합니다. 일치하는 요청의 경우 프로세스는 다음과 같습니다.

  1. 요청이 캐시에 도달합니다. 애셋이 캐시에 있으면 거기에서 제공합니다.
  2. 요청이 캐시에 없는 경우 네트워크로 이동합니다.
  3. 네트워크 요청이 완료되면 이를 캐시에 추가한 다음 네트워크의 응답을 반환합니다.

다음은 이 전략의 예시이며 실시간 데모에서 테스트할 수 있습니다.

// Establish a cache name
const cacheName = 'MyFancyCacheName_v1';

self.addEventListener('fetch', (event) => {
  // Check if this is a request for an image
  if (event.request.destination === 'image') {
    event.respondWith(caches.open(cacheName).then((cache) => {
      // Go to the cache first
      return cache.match(event.request.url).then((cachedResponse) => {
        // Return a cached response if we have one
        if (cachedResponse) {
          return cachedResponse;
        }

        // Otherwise, hit the network
        return fetch(event.request).then((fetchedResponse) => {
          // Add the network response to the cache for later visits
          cache.put(event.request, fetchedResponse.clone());

          // Return the network response
          return fetchedResponse;
        });
      });
    }));
  } else {
    return;
  }
});

이 예에서는 이미지만 다루고 있지만 모든 정적 애셋 (예: CSS, 자바스크립트, 이미지, 글꼴), 특히 해시 버전이 있는 애셋에 적용하기에 좋은 전략입니다. HTTP 캐시가 시작될 수 있는 서버에서 콘텐츠 최신 상태 확인을 피함으로써 변경 불가능한 애셋의 속도를 향상시킵니다. 더 중요한 점은 캐시된 모든 애셋을 오프라인에서 사용할 수 있다는 것입니다.

네트워크 우선, 캐시로 대체

페이지에서 서비스 워커, 네트워크로, 네트워크를 사용할 수 없는 경우 캐시로의 흐름을 보여줍니다.

'캐시 우선, 네트워크 두 번째'를 뒤집으면 다음과 같은 '네트워크 우선, 캐시 두 번째' 전략이 생깁니다.

  1. 먼저 네트워크로 이동하여 요청을 하고 응답을 캐시에 저장합니다.
  2. 나중에 오프라인 상태이면 캐시에서 해당 응답의 최신 버전으로 돌아갑니다.

이 전략은 온라인 상태에서는 최신 버전의 리소스를 원하지만 사용 가능한 최신 버전에 오프라인 액세스를 제공하지 않으려는 경우 HTML 또는 API 요청에 적합합니다. HTML 요청에 적용될 때 다음과 같이 표시될 수 있습니다.

// Establish a cache name
const cacheName = 'MyFancyCacheName_v1';

self.addEventListener('fetch', (event) => {
  // Check if this is a navigation request
  if (event.request.mode === 'navigate') {
    // Open the cache
    event.respondWith(caches.open(cacheName).then((cache) => {
      // Go to the network first
      return fetch(event.request.url).then((fetchedResponse) => {
        cache.put(event.request, fetchedResponse.clone());

        return fetchedResponse;
      }).catch(() => {
        // If the network is unavailable, get
        return cache.match(event.request.url);
      });
    }));
  } else {
    return;
  }
});

데모에서 사용해 볼 수 있습니다. 먼저 페이지로 이동합니다. HTML 응답을 캐시에 배치하기 전에 새로고침해야 할 수도 있습니다. 그런 다음 개발자 도구에서 오프라인 연결을 시뮬레이션하고 다시 로드합니다. 사용 가능한 마지막 버전이 캐시에서 즉시 제공됩니다.

오프라인 기능이 중요하지만 최신 버전의 마크업 또는 API 데이터에 액세스하는 기능과 이 기능의 균형을 유지해야 하는 상황에서는 '네트워크 우선, 캐시 두 번째'가 이러한 목표를 달성하는 견고한 전략입니다.

재검증 중 비활성 상태

페이지에서 서비스 워커, 캐시, 네트워크에서 캐시로의 흐름을 보여줍니다.

지금까지 살펴본 전략 중 'Stale-when-revalidate'가 가장 복잡합니다. 어떤 면에서 마지막 두 가지 전략과 비슷하지만 이 절차는 리소스의 액세스 속도를 우선시하고 백그라운드에서 리소스를 최신 상태로 유지하는 것입니다. 이 전략은 다음과 같습니다.

  1. 첫 번째 애셋 요청 시 네트워크에서 애셋을 가져와 캐시에 배치한 후 네트워크 응답을 반환합니다.
  2. 후속 요청에서는 캐시에서 애셋을 먼저 제공한 다음, '백그라운드에서', 네트워크에서 다시 요청하고 애셋의 캐시 항목을 업데이트합니다.
  3. 그 이후 요청의 경우 이전 단계에서 캐시에 배치한 네트워크에서 가져온 마지막 버전을 수신하게 됩니다.

이는 최신 상태로 유지하는 것이 중요하지만 중요하지 않은 경우에 매우 유용한 전략입니다. 소셜 미디어 사이트의 아바타라고 생각하면 됩니다. 사용자가 업데이트할 때 업데이트되지만, 모든 요청에 최신 버전이 꼭 필요한 것은 아닙니다.

// Establish a cache name
const cacheName = 'MyFancyCacheName_v1';

self.addEventListener('fetch', (event) => {
  if (event.request.destination === 'image') {
    event.respondWith(caches.open(cacheName).then((cache) => {
      return cache.match(event.request).then((cachedResponse) => {
        const fetchedResponse = fetch(event.request).then((networkResponse) => {
          cache.put(event.request, networkResponse.clone());

          return networkResponse;
        });

        return cachedResponse || fetchedResponse;
      });
    }));
  } else {
    return;
  }
});

특히 브라우저의 개발자 도구의 네트워크 탭과 CacheStorage 뷰어 (브라우저의 개발자 도구에 이러한 도구가 있는 경우)에 주의를 기울이면 또 다른 실시간 데모에서 실제 동작을 확인할 수 있습니다.

Workbox로 이동하세요.

이 문서에서는 서비스 워커의 API와 관련 API에 대한 검토를 마무리합니다. 따라서 서비스 워커를 직접 사용하여 Workbox를 조작하는 방법을 충분히 배웠습니다.