서비스 워커 캐싱으로 성능 및 오프라인 기능 향상
Olivia Novak
Dev Intern · Leapcell

소개
오늘날 빠르게 변화하는 디지털 세계에서 사용자는 이상적이지 않은 네트워크 조건에서도 웹사이트가 즉각적으로 응답하고 접근 가능하기를 기대합니다. 로딩 시간이 느리면 불만, 이탈률 증가, 전반적인 사용자 경험 저하로 이어질 수 있습니다. 웹 애플리케이션의 경우, 초기 로딩 시간의 상당 부분은 종종 HTML, CSS, JavaScript, 이미지와 같은 정적 에셋과 API 데이터를 가져오는 데 소요됩니다. 이러한 리소스가 가져와진 후에는 후속 방문에서 다시 필요할 가능성이 높습니다. 바로 여기서 네트워크 캐싱의 개념이 매우 중요해집니다. 속도 외에도 웹 애플리케이션이 오프라인에서 안정적으로 작동하는 능력은 더 이상 사치가 아니라 점점 늘어나는 사용자 기대치입니다. 지하철에서 좋아하는 뉴스 사이트를 확인하거나, 인터넷 연결이 일시적으로 끊겼을 때 레시피에 액세스하는 것을 상상해 보세요. 이러한 일반적인 시나리오는 강력한 오프라인 지원의 필요성을 강조합니다. 이 글에서는 서비스 워커가 이러한 문제를 해결하기 위한 강력한 메커니즘을 제공하여, 반복 방문자의 성능을 획기적으로 향상시키는 공격적인 캐싱 전략을 활성화하고 원활한 오프라인 기능을 지원하는 방법을 자세히 살펴봅니다.
네트워크 캐싱을 위한 서비스 워커 이해
캐싱을 위한 서비스 워커의 힘을 완전히 이해하려면 먼저 몇 가지 필수 개념을 명확히 해보겠습니다.
서비스 워커(Service Worker): 서비스 워커는 웹 워커의 일종으로, 기본 브라우저 스레드와 분리되어 백그라운드에서 실행되는 JavaScript 파일입니다. 웹 페이지와 네트워크(및/또는 캐시) 간의 프로그래밍 가능한 프록시 역할을 합니다. 이를 통해 네트워크 요청을 가로채고, 리소스를 캐싱하고, 푸시 알림을 제공하는 등의 작업을 수행할 수 있습니다. 결정적으로 서비스 워커는 UI와 독립적으로 작동하며 브라우저 탭이 닫혀 있어도 계속 실행될 수 있어 정교한 백그라운드 작업을 가능하게 합니다.
캐시 스토리지 API(Cache Storage API): 이 API는 Request 및 Response 객체의 쌍에 대한 영구 스토리지 메커니즘을 제공합니다. 서비스 워커가 캐시된 네트워크 응답을 저장하고 검색하는 기본 방법입니다. 각 출처는 자체 캐시 스토리지를 가지며, 데이터는 명시적으로 삭제되거나 사용자가 지우기 전까지 유지됩니다.
캐싱 전략(Cache Strategies): 이는 캐시 스토리지 API를 사용하여 네트워크 요청을 처리하는 방법을 결정하는 데 서비스 워커가 사용하는 패턴입니다. 일반적인 전략은 다음과 같습니다.
- 캐시 우선(Cache First): 캐시에서 제공하려고 시도하고, 없을 경우 네트워크로 이동합니다.
- 네트워크 우선(Network First): 네트워크에서 가져오려고 시도하고, 실패하면 캐시로 대체합니다.
- 재검증 중 오래된 것(Stale While Revalidate): 캐시에서 즉시 제공하지만, 나중에 요청할 캐시를 업데이트하기 위해 백그라운드에서 네트워크에서 가져옵니다.
- 캐시만(Cache Only): 네트워크로 절대 이동하지 않고 항상 캐시에서 제공합니다 (앱 셸 에셋에 유용).
- 네트워크만(Network Only): 캐시를 절대 사용하지 않고 항상 네트워크로 이동합니다 (캐싱이 바람직하지 않은 실시간 데이터에 유용).
네트워크 캐싱을 위해 서비스 워커를 사용하는 핵심 원리는 모든 네트워크 요청을 가로챌 수 있는 능력입니다. 서비스 워커의 제어를 받는 페이지에서 발생하는 요청입니다. 요청이 이루어지면 서비스 워커는 정의된 캐싱 전략에 따라 캐시에서 응답을 제공할지, 네트워크에서 가져올지, 또는 둘 다를 조합할지 결정할 수 있습니다. 이 가로채기 능력은 리소스 제공을 제어하는 데 매우 강력합니다.
서비스 워커로 네트워크 캐싱 구현하기
실제 예제를 통해 이를 설명해 보겠습니다. 애플리케이션의 "앱 셸"(HTML, CSS, JS)과 일부 이미지를 캐싱하기 위한 기본 서비스 워커를 설정하고, 정적 에셋에 대한 "캐시 우선, 그 다음 네트워크" 전략과 페이지에 대한 "오프라인 대체"를 시연해 보겠습니다.
먼저 서비스 워커를 등록해야 합니다. 프로젝트의 루트 디렉토리에 service-worker.js 파일을 만들고, 다음을 기본 JavaScript 파일(예: app.js)에 추가하세요.
// app.js if ('serviceWorker' in navigator) { window.addEventListener('load', () => { navigator.serviceWorker.register('/service-worker.js') .then(registration => { console.log('Service Worker registered with scope:', registration.scope); }) .catch(error => { console.error('Service Worker registration failed:', error); }); }); }
이제 service-worker.js 파일을 작성해 보겠습니다. 이 파일에는 캐싱 로직이 포함됩니다.
// service-worker.js const CACHE_NAME = 'my-app-cache-v1'; // 캐시 버전 지정 const dynamicCacheName = 'dynamic-assets-cache-v1'; // 동적으로 캐시되는 에셋용 const urlsToCache = [ '/', // 우리 앱의 홈페이지 '/index.html', '/styles.css', '/app.js', '/images/logo.png', '/offline.html' // 오프라인 시 제공할 페이지 ]; // 1. 설치 이벤트: 정적 에셋 캐싱 self.addEventListener('install', event => { console.log('Service Worker: Installing...'); event.waitUntil( caches.open(CACHE_NAME) .then(cache => { console.log('Service Worker: Caching App Shell assets'); return cache.addAll(urlsToCache); }) ); }); // 2. 활성화 이벤트: 이전 캐시 정리 self.addEventListener('activate', event => { console.log('Service Worker: Activating...'); event.waitUntil( caches.keys().then(cacheNames => { return Promise.all( cacheNames.map(cacheName => { if (cacheName !== CACHE_NAME && cacheName !== dynamicCacheName) { console.log('Service Worker: Deleting old cache', cacheName); return caches.delete(cacheName); } }) ); }) ); // 클라이언트가 새로운 서비스 워커를 즉시 제어하도록 함 return self.clients.claim(); }); // 3. 가져오기(fetch) 이벤트: 네트워크 요청 가로채기 self.addEventListener('fetch', event => { // 요청이 네비게이션(HTML 페이지)인지 확인 if (event.request.mode === 'navigate') { event.respondWith( caches.match(event.request) .then(response => { return response || fetch(event.request) .catch(() => caches.match('/offline.html')); // 네트워크 실패 시 오프라인 페이지 제공 }) ); return; // 네비게이션 요청에 대해 일반 캐싱 진행하지 않음 } // 기타 에셋(CSS, JS, 이미지 등) - 캐시 우선, 그 다음 네트워크 event.respondWith( caches.match(event.request).then(cachedResponse => { // 캐시된 응답이 있으면 반환 if (cachedResponse) { return cachedResponse; } // 그렇지 않으면 네트워크에서 가져옴 return fetch(event.request) .then(networkResponse => { // 응답이 유효하면 동적으로 캐싱 if (networkResponse && networkResponse.status === 200 && networkResponse.type === 'basic') { const responseClone = networkResponse.clone(); // 응답은 한 번만 소비 가능하므로 복제 caches.open(dynamicCacheName).then(cache => { cache.put(event.request, responseClone); }); } return networkResponse; }) .catch(error => { console.log('Fetch failed, trying cache for:', event.request.url, error); // 네트워크 및 초기 캐시(있는 경우) 모두 실패하는 경우 대체 // 동적 콘텐츠에도 네트워크가 실패하면 사용 가능한 이전 버전을 대체하려고 시도보장 return caches.match(event.request); }); }) ); });
이 서비스 워커에서:
install이벤트: 서비스 워커가 설치될 때 발생합니다.event.waitUntil을 사용하여 지정된urlsToCache(앱 셸)가CACHE_NAME에 추가될 때까지 설치가 완료되지 않도록 합니다.activate이벤트: 설치 후에 발생합니다. 이전 캐시를 정리하기 좋은 장소로, 사용자가 서비스 워커 업데이트 시 항상 최신 캐시된 에셋을 받도록 보장합니다.self.clients.claim()은 새 서비스 워커가 이미 열린 페이지를 즉시 제어하도록 합니다.fetch이벤트: 캐싱의 핵심입니다. 서비스 워커의 제어를 받는 페이지에서 발생하는 모든 네트워크 요청이 이 이벤트를 트리거합니다.- 네비게이션 요청(예: HTML 페이지 로딩)의 경우, 먼저
CACHE_NAME에서 로드하려고 시도합니다. 실패하면 네트워크를 시도합니다. 네트워크도 실패하면 미리 캐시된/offline.html페이지를 제공하여 우아한 대체 기능을 제공합니다. - 기타 에셋(CSS, JS, 이미지 등)의 경우, "캐시 우선, 그 다음 네트워크, 그 다음 캐시 업데이트" 전략을 구현합니다. 에셋이 캐시(
CACHE_NAME또는dynamicCacheName)에서 발견되면 즉시 제공됩니다. 그렇지 않으면 네트워크로 요청이 이동합니다. 성공하면 네트워크 응답이dynamicCacheName에 동적으로 추가되어 향후 사용됩니다. 네트워크도 실패하면 마지막 수단으로 사용 가능한 캐시에 대해 일치하는지 시도합니다.
- 네비게이션 요청(예: HTML 페이지 로딩)의 경우, 먼저
애플리케이션 시나리오
서비스 워커 캐싱의 힘은 다양한 사용 사례로 확장됩니다.
- 프로그레시브 웹 앱(PWA): 서비스 워커는 PWA의 초석으로, 오프라인 액세스, 빠른 로딩, "홈 화면에 추가"와 같은 기능을 활성화합니다.
- 정적 사이트 사전 캐싱: 대부분 정적인 콘텐츠를 가진 웹사이트의 경우, 첫 방문 시 모든 중요 에셋을 사전 캐싱하여 후속 방문 시 거의 즉시 로딩할 수 있습니다.
- 오프라인 우선 애플리케이션: 캐시에서 콘텐츠 제공을 우선시하고, 필요할 때만(예: 업데이트 또는 새 데이터용) 네트워크에서 가져오는 애플리케이션을 구축합니다.
- API 데이터 캐싱: 자주 변경되지 않는 데이터의 경우 API 엔드포인트의 응답을 캐싱하여 서버 부하를 줄이고 응답성을 향상시킵니다. "재검증 중 오래된 것"과 같은 전략은 여기서 완벽하며, 오래된 데이터를 빠르게 표시하지만 백그라운드에서 업데이트합니다.
결론
서비스 워커는 웹 애플리케이션이 네트워크와 상호 작용하는 방식을 혁신하여 리소스 가져오기 및 제공에 대한 탁월한 제어 수준을 제공합니다. 캐싱 메커니즘을 전략적으로 구현함으로써 반복 방문을 크게 가속화하여 사용자에게 더 빠르고 유연한 경험을 제공할 수 있습니다. 더 중요한 것은 서비스 워커가 네트워크 조건이 불안정하거나 전혀 없어도 안정적인 액세스를 제공하는 견고한 웹 애플리케이션을 구축할 수 있도록 하여 웹과 네이티브 애플리케이션 기능 간의 격차를 해소합니다. 궁극적으로 서비스 워커는 성능이 뛰어나고 강력하며 사용자 중심적인 웹 경험을 구축하는 데 중요한 도구입니다.

