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를 불러오는 구문을 추가한다.
if ("serviceWorker" in navigator) {
window.addEventListener("load", function () {
navigator.serviceWorker
.register("/sw.js")
.then(() => {
console.log("서비스 워커가 등록되었다.");
})
.catch((error) => {
console.log(error);
});
});
}
개발자도구를 열고 Application > ServiceWorkers 탭으로 가면 설치가 된 것을 확인할 수 있다.
이렇게 뜨면 설치된 것이다.
Basic
서비스워커에서는 self
키워드로 자기 자신을 접근할 수 있다. 몇 가지 static 파일들을 캐싱처리 해보자.
모던 브라우저에서만 지원이 되므로 arrow function
을 사용해도 된다.
sw.js
const PRE_CACHE_NAME = "캐시-스토리지1";
const urlsToCache = [
"/public/image/image1.png",
"/public/css/font-awesome.min.css",
];
self.addEventListener("install", (event) => {
event.waitUntil(
caches
.open(PRE_CACHE_NAME)
.then((cache) => {
console.log("캐시 디비와 연결됨");
return cache.addAll(urlsToCache);
})
.then(() => {
return self.skipWaiting();
}),
);
});
이렇게 추가해주고 개발자도구의 Application 탭으로 가서 좌측 메뉴의 Cache Storage 를 새로고침 하면 방금 추가한 캐시-스토리지1에 내 이미지와 css가 등록된 걸 확인할 수 있다.
그리고 개발자도구의 Network 탭에서 호출하는 이미지의 size에 **(from ServiceWorker)**가 보인다.
내용이 업데이트 되야하는 페이지나 리소스에 대해서는 동적으로 캐싱 처리를 해야한 다.
Dynamic caching
sw.js
const DYNAMIC_CACHE_NAME = "다이나믹-캐시-스토리지1";
self.addEventListener("fetch", (event) => {
event.respondWith(
caches.match(event.request).then((response) => {
if (response) {
return response;
}
const fetchRequest = event.request.clone();
return fetch(fetchRequest).then((response) => {
if (!response) {
return response;
}
const responseToCache = response.clone();
caches.open(DYNAMIC_CACHE_NAME).then((cache) => {
cache.put(event.request, responseToCache);
});
return response;
});
}),
);
});
오래된 캐시 삭제
캐시 스토리지명을 바꿔 캐시의 버전을 올리면 기존 캐시 스토리지는 삭제해줘야한다.
sw.js
self.addEventListener("activate", (event) => {
const cacheWhiteList = [PRE_CACHE_NAME, DYNAMIC_CACHE_NAME];
event.waitUntil(
caches.keys().then((cacheNames) => {
return Promise.all(
cacheNames.map((cacheName) => {
if (cacheWhitelist.indexOf(cacheName) === -1) {
return caches.delete(cacheName);
}
}),
);
}),
);
return self.clients.claim();
});
결과적으로 DYNAMIC_CACHE_NAME
이 아닌 캐시 스토리지는 삭제된다.
offline fallback
캐시된 페이지와 리소스는 오프라인에서도 접근이 가능하다.
여기서 오류가 발생하면 offline.html 같은 페이지로 떨어지게 할 수 있다. (마치 404 오류 페이지처럼)
sw.js
self.addEventListener("fetch", (event) => {
event.respondWith(
caches
.match(event.request)
.then((response) => {
})
.catch((error) => {
return caches.open(CACHE_NAME).then((cache) => {
if (event.request.headers.get("accept").includes("text/html")) {
return cache.match("/offline.html");
}
});
}),
);
});
Advanced
서비스워커에 static, dynamic cache를 첨가했는데 소스가 너무 지저분하다.
직관적으로 만들어보자.
리팩토링
sw.js
(() => {
const STATIC_CACHE_NAME = "STATIC_CACHE_VERSION_1";
const DYNAMIC_CACHE_NAME = "DYNAMIC_CACHE_VERSION_1";
const WEB_CACHE = {
init() {
self.addEventListener("install", this.staticCacheStrategy.bind(this));
self.addEventListener("activate", this.deleteOldCache.bind(this));
self.addEventListener("fetch", this.dynamicCacheStrategy.bind(this));
},
staticCacheStrategy(event) {
},
deleteOldCache(event) {
},
dynamicCacheStrategy(event) {
},
};
WEB_CACHE.init();
})();
IIFE를 사용해 좀 더 예쁘게 변했다.
cross domain request
서비스워커에서 fetch로 외부 리소스를 가지고오면 opaque
response가 반환된다.
cors 정책이 설정되어 있지 않아 아무 정보도 가지고 올 수 없는 건데, 이런 리소스만 골라서 캐싱처리를 하고 싶다면 request url이나 response content-type을 가지고 처리할 수 있다.
sw.js
const dynamicCacheStrategy = (event) => {
const cacheContentsTypes = [
"image/png",
"image/gif",
"image/jpeg",
"application/font-woff",
];
event.respondWith(
caches.match(event.request).then((response) => {
const fetchRequest = event.request.clone();
return (
response ||
fetch(fetchRequest)
.then((response) => {
if (!response) {
return response;
}
if (
cacheContentsTypes.indexOf(
response.headers.get("content-type"),
) !== -1 ||
event.request.url.indexOf("external.url") !== -1
) {
caches.open(DYNAMIC_CACHE_NAME).then((cache) => {
cache.put(event.request, response.clone());
});
}
return response;
})
.catch((error) => console.log(error))
);
}),
);
};
WorkBox
이제 클라이언트 캐시가 어떻게 돌아가는지 확인했으니 구글에서 만든 멋진 라이브러리를 사용해보자.