PWA - 서비스 워커 웹 캐싱 (Web Caching)
Progressive Web App의 시대가 왔다. Client단에서 모든 static 파일을 브라우저에 캐시를 할 수 있고 웹을 앱처럼 오프라인에서 작업하게 할 수 있다. 그 첫번째로 캐싱에 대해 알아보자.
준비
Promise, Fetch, Worker 및 Javascript 의 실행 구조, DOM 에 대한 사전지식이 필히 있어야 한다. HTTP Cache 를 걸어봤다면 이해가 쉬울 듯 싶다.
ServiceWorker
서비스 워커는 브라우저가 백그라운드에서 실행하는 스크립트이며, 클라이언트에 설치되는 프록시다. 이 개념은 중요해서 외워야한다. 브라우저 백그라운드에서 네트워크를 가로채는 Thread 라고 보면 된다.
웹 캐싱 뿐만 아니라 백그라운드 동기화, 웹 푸쉬 등의 기능을 처리할 수 있다. 궁금하다면 여기를 들어가보자. (포스팅을 다 읽고 들어가보는 걸 추천한다.)
웹 캐싱은 CacheStorage를 사용한다. Sqlite 같은 클라이언트 데이터베이스인데, Key:value 로 구성된 데이터베이스라고 보면 된다.
주의
https 환경 또는 localhost 도메인에서만 이 기능을 사용할 수 있다.
브라우저별 지원상황은 다음과 같다 1803기준 Safari에도 Shipped가 되었다!
1712 기준 Safari가 아직도 Developement 상태인게 조금 아쉽다
세팅
sw.js
란 파일을 public
폴더(index.html이 있는)에 생성하자.
그리고 메인 script파일에 다음과 같이 service-worker를 불러오는 구문을 추가한다.
// navigator (브라우저)에 serviceWorker 기능이 있는지 확인
if ("serviceWorker" in navigator) {
// 서비스워커 설치시 DOM 블로킹을 막아준다.
window.addEventListener("load", function () {
// 서비스워커를 register 하면 promise를 반환한다.
navigator.serviceWorker
.register("/sw.js")
.then(() => {
console.log("서비스 워커가 등록되었다.");
})
.catch((error) => {
console.log(error);
});
});
}
개발자도구를 열고 Application > ServiceWorkers 탭으로 가면 설치가 된 것을 확인할 수 있다.
이렇게 뜨면 설치된 것이다.
Basic
서비스워커에서는 self
키워드로 자기 자신을 접근할 수 있다. 몇 가지 static 파일들을 캐싱처리 해보자.
모던 브라우저에서만 지원이 되므로 arrow function
을 사용해도 된다.
const PRE_CACHE_NAME = "캐시-스토리지1";
// 캐시하고 싶은 리소스
const urlsToCache = [
"/public/image/image1.png",
"/public/css/font-awesome.min.css",
];
// 서비스워커가 설치될 때
self.addEventListener("install", (event) => {
// 캐시 등록 이벤트가 끝날 때까지 기다려
event.waitUntil(
// '캐시-스토리지1'을 연다.
// @return {Promise} 연결된 Cache Database를 반환한다.
caches
.open(PRE_CACHE_NAME)
.then((cache) => {
console.log("캐시 디비와 연결됨");
// addAll 메소드로 내가 캐싱할 리소스를 다 넣어주자.
return cache.addAll(urlsToCache);
})
.then(() => {
// 설치 후에 바로 활성화 단계로 들어갈 수 있게 해준다.
return self.skipWaiting();
}),
);
});
이렇게 추가해주고 개발자도구의 Application 탭으로 가서 좌측 메뉴의 Cache Storage 를 새로고침 하면 방금 추가한 캐시-스토리지1에 내 이미지와 css가 등록된 걸 확인할 수 있다.
그리고 개발자도구의 Network 탭에서 호출하는 이미지의 size에 **(from ServiceWorker)**가 보인다.
Intermediate
내용이 업데이트 되야하는 페이지나 리소스에 대해서는 동적으로 캐싱 처리를 해야한다.
Dynamic caching
const DYNAMIC_CACHE_NAME = "다이나믹-캐시-스토리지1";
// fetch event는 어딘가에서 리소스를 가져올 때 모두 실행된다.
// js를 가져오거나 이미지를 가져오거나 페이지를 가져오거나 등등
self.addEventListener("fetch", (event) => {
event.respondWith(
caches.match(event.request).then((response) => {
// 캐시에 있으면 repsonse를 그대로 돌려준다.
if (response) {
return response;
}
// 여기서 request를 복사해준다.
// request는 스트림으로 fetch 당 한 번만 사용해야하기 때문이다.
// 근데 event.request로 받아도 실행은 된다
const fetchRequest = event.request.clone();
// if (response) return response 구문을 하나로 합칠 수도 있다.
// return response || fetch(fetchRequest)
return fetch(fetchRequest).then((response) => {
// 응답이 제대로 왔는지 체크한다.
// 구글 문서에는 다음과 같이 처리하라고 되어있는데
// 이 경우 Cross Site Request에 대해 캐싱 처리를 할 수가 없다.
// if(!response || response.status !== 200 || response.type !== 'basic') {
if (!response) {
return response;
}
// 응답은 꼭 복사 해줘야한다.
const responseToCache = response.clone();
// 캐시 스토리지를 열고 정말 캐싱을 해준다.
caches.open(DYNAMIC_CACHE_NAME).then((cache) => {
cache.put(event.request, responseToCache);
});
// 여기서 response를 내보내줘야 캐싱 처리 후에 리소스를 반환한다.
return response;
});
}),
);
});
오래된 캐시 삭제
캐시 스토리지명을 바꿔 캐시의 버전을 올리면 기존 캐시 스토리지는 삭제해줘야한다.
// 서비스 워커가 활성화 될 때
self.addEventListener("activate", (event) => {
// 영구적으로 가져갈 캐시 스트리지 화이트리스트
const cacheWhiteList = [PRE_CACHE_NAME, DYNAMIC_CACHE_NAME];
event.waitUntil(
// 캐시 스토리지의 모든 스토리지명을 가져온다.
caches.keys().then((cacheNames) => {
// 캐시를 삭제하는 건 Promise를 반환하므로 Promise.all을 사용해 끝날 시점을 잡아야한다.
return Promise.all(
// 이 결과는 [Promise, Promise...] 형태가 되시겠다.
cacheNames.map((cacheName) => {
// 각각의 캐시 스토리지명이 화이트 리스트와 같지 않을 경우
if (cacheWhitelist.indexOf(cacheName) === -1) {
// 캐시를 삭제하는 Promise를 배열에 추가한다.
return caches.delete(cacheName);
}
}),
);
}),
);
// activate 시에는 clients claim 메소드를 호출해서
// 브라우저에 대한 제어권을 가져와야한다.
return self.clients.claim();
});
결과적으로 DYNAMIC_CACHE_NAME
이 아닌 캐시 스토리지는 삭제된다.