service worker 많은 훌륭한 기능을 가지고 있다. 인터넷이 안되는 환경에서도 사용할 수 있게 하고 푸시 알림, 백그라운드 동기화와 같은 차세대 애플리케이션 기능을 사용할 수 있게 한다.
Service Worker는 무엇인가?
일반적인 javascript부터 이야기 해보자. HTML 페이지에는 기본적으로 로딩되는 javascript 파일이 있다. javascript는 싱글스레드로 동작한다. 사용자가 웹페이지에 방문하면 HTML을 리턴하고 그 페이지에 있는 javascript는 하나의 스레드에서 동작한다는 것이다.
만일 여러개의 javascript 파일이 동일 페이지에서 로딩된다고 할지라도 하나의 스레드에서 코드가 실행된다.
페이지에서 로딩된 javascript 파일은 DOM에 접근할 수 있다. alert, prompt, DOM 변경 및 조작 등 javascript로 다양한 작업을 할 수 있다.
여러분이 알고 있는 React, Angular, Vue 같은 프레임워크도 이와 같이 동작한다.
그러면 Service Worker는 어떻게 동작할까? Service Worker도 javascript 파일이다. 하지만 다른 접근을 위한 특징을 가지고 있다. Service Worker도 싱글 스레드로 동작한다. 하지만 기본 javascript와 동일 스레드를 사용하지 않는다. Service Worker는 백그라운드에서 동작하기 때문에 개별 스레드를 가지고 있다.
또한 Service Worker는 브라우저의 모든 페이지가 닫혀있다라도 여전히 동작한다. 예를 들면 모바일 페이지에서 브라우저를 닫아도 백그라운드 프로세스로 동작을 한다. Service Worker는 백그라운드 프로세스로 동작하므로 일반적인 javascript와는 완전히 다르다. DOM에 접근할 수 없고 페이지에서 실행될 수도 없다.
또 Service Worker로 무엇을 할 수 있을까? 특정 이벤트를 수신할 수 있다. Javascript 페이지, HTML 코드 혹은 웹 푸시같이 다른 서버로부터 이벤트를 수신한다.
"수신하는" 이벤트 (Service Worker에서)
service worker는 백그라운드 프로세스이고 이벤트를 받을 수 있다. 어떤 이벤트를 받을 수 있을까?
Fetch - 브라우저 혹은 페이지 관련 javascript가 index.html이나 HTML 페이지에서 로딩되면 HTTP request인 fetch가 실행된다. image가 있을 경우 브라우저가 이미지를 표시하기 위해 fetch 요청이 실행된다. service worker에서 fetch 요청을 수신할 수 있이서 service worker를 network proxy 역할로 생각할 수도 있다. 모든 요청, css, javascript 파일을 로딩할 때마다 fetch가 실행된다. 하지만, Ajax 요청, 즉 XMLHttpRequest는 fetch 이벤트를 실행하지 않는다.
Push Notification - push 알림은 다른 서버에서 전송된다. chrome의 Google과 firefox의 Mozilla는 자신만의 push 서버를 가지고 있다. 서버에서 푸시발송을 요청하면 브라우저 벤더에서 푸시 알림을 클라이언트에게 전송한다. 그리고 service worker에서 푸시 이벤트를 수신할 수 있다.
그러면 왜 이러한 푸시수신을 웹페이지에 있는 javascript에서 하지 않고 service worker에서 해야 할까? service worker는 백그라운드에서 실행되고 페이지가 닫혀있더라도 실행된다. 푸시 알림은 사용자로 하여금 앱으로 다시 돌아오게 한다. 네이티브 앱같이 소리, 진동같은 알림을 받을 수 있다. 푸시 알림을 받으면 alert나 알림을 표시할 수 있다.
Notification Interaction - 푸시 알림을 받았을 때 특정 페이지를 표시하거나 캐쉬에서 데이터를 로딩할 수 있다.
Background Sync - 인터넷 연결이 좋지 않은 환경에서 글을 작성한다고 해보자. 연결상태가 좋지 않아 지금은 동작할 수 없지만 인터넷이 재연결되면 실행될 수 있는 상황이 있을 수 있다. 이런 상황에서 특정 이벤트를 발행할 수 있다. 또한 브라우저가 닫혀 있는 상황에서도 가능하다.
Service Worker Lifecycle - service worker 생명주기와 관련된 이벤트가 있다. installation, activation 등. 이런 lifecycle 단계에서 특정 코드를 실행할 수 있다.
Service Worker Lifecycle
app.js 파일을 로딩하는 index.html 파일이 있다고 하자. 이런 형태가 전통적인 웹 애플리케이션이다.
app.js에서 service worker를 등록하기 위한 몇 개의 javascript 코드를 실행할 수 있다. 이 코드는 브라우저에게 sw.js는 javascript 파일이고 백그라운드로 등록하라고 하는 것이다.
등록과정에서 2가지 단계가 실행된다. 첫 번째는 service worker를 설치(install)하는 것이다. 그 과정에서 install event를 통해 특정 코드를 실행할 수 있다. 예를 들면 특정 리소스를 캐싱을 할 수 있다.
다음은 설치가 완료될 때 실행되는 이벤트이다. service worker가 activate 될 때 설치가 완료되고 나서 항상 곧바로 activate가 되는 것은 아니다. 예전 service worker가 실행중인지 여부에 따라 activate가 달라질 수 있다. 이전의 실행 중인 service worker가 없다면 곧바로 active 상태로 된다. 새 버전의 service worker를 설치하기 위해 기존 탭을 닫아야 한다.
브라우저가 activate된다고 판단되면 activate 이벤트가 발행된다.
service worker가 activate가 되면 백그라운드에서 idle 상태가 된다. 이벤트가 없으면 아무것도 하지 않는다. 일정시간동안 idle 상태가 되면 terminated된다. 삭제나 등록해제가 된다는 것을 의미하는 것은 아니다. 단지 유휴상태가 된다는 것이다. 즉, Sleeping 상태가 된다는 것이다. 특정 이벤트를 수신하면 다시 동작한다.
Service Worker 등록
service worker를 등록하기 위해서는 개발서버가 필요하다. 여기서는 http-server를 통해 실행한다.
npm install http-server
package.json
"scripts": {
"start": "http-server -c-1"
},
"dependencies": {
"http-server": "^14.1.1"
}
-c-1은 브라우저 캐시를 사용하지 않겠다는 의미이다.
service worker는 어떻게 등록할 수 있을까? index.html에서 사용하는 app.js에 코드를 추가함으로써 등록할 수 있다.
service woker는 일반적으로 root 폴더에 등록한다. 일단 등록이 되면 새 버전으로 변경되지 않는 한 기존 버전을 유지한다.
[index.html]
<script src="static/app.js"></script>
[app.js]
if ('serviceWorker' in navigator) {
navigator.serviceWorker
.register('/sw.js')
.then(function() {
console.log('Service worker registered')
});
}
navigator에 serviceWorker가 있는지 체크하고 service worker를 등록한다.
그러면 화면에 아래와 같이 표시가 된다.
Service worker registered
등록할 때 특정 리소스에 대해서 제한하려면 register의 두 번째 파라미터에 scope을 지정하면 된다.
navigator.serviceWorker
.register('/sw.js', { score: '/help'}) // /help 경로에 대해서만 service worker를 등록한다는 의미
service worker는 localhost를 제외하고 https
에서만 동작한다. 즉, 외부 서비스에서는 https를 사용해야 한다는 뜻이다.
sw.js에서 아래와 같이 install, activate 이벤트를 추가해보자.
self.addEventListener('install', function(event) {
console.log('[Service Worker] Installing Service Worker...', event);
});
self.addEventListener('activate', function(event) {
console.log('[Service Worker] Activating Service Worker...', event);
return self.clients.claim();
});
그러면 아래와 같은 메시지가 표시된다.
Service worker registered
sw.js:2 [Service Worker] Installing Service Worker... InstallEvent {isTrusted: true, type: 'install', target: ServiceWorkerGlobalScope, currentTarget: ServiceWorkerGlobalScope, eventPhase: 2, …}
하지만 activate 이벤트는 동작을 하지 않는 것을 볼 수 있다.
chrome developer tools의 Application > Service wokers에서 service worker를 unregister를 한 다음 다시 페이지를 리로드 하면 아래와 같이 표시된다.
sw.js:2 [Service Worker] Installing Service Worker... InstallEvent {isTrusted: true, type: 'install', target: ServiceWorkerGlobalScope, currentTarget: ServiceWorkerGlobalScope, eventPhase: 2, …}
app.js:3 Service worker registered
sw.js:6 [Service Worker] Activating Service Worker... ExtendableEvent {isTrusted: true, type: 'activate', target: ServiceWorkerGlobalScope, currentTarget: ServiceWorkerGlobalScope, eventPhase: 2, …}
Updating & Activating Service Workers
위의 코드에서 궁금한 점은 2가지이다.
- activate 로그가 표시되지 않는다는 것이다.
- service worker 등록 메시지가 먼저 표시되었냐는 것이다. (Service worker registered)
1번째는 브라우저 탭이 여러 개 떠있으면 activate 되지 않는다. 기존에 떠있는 브라우저를 닫아야 activate된다.
2번째는 등록 메시지의 순서를 보장하지 않는다. 등록되는 순서는 chrome의 상황에 따라 달라질 수가 있다는 것이다.
skipWaiting 메시지
service worker 소스(sw.js)를 일부 변경을 하고 다시 페이지를 새로고침 해보자. 그리고 Application > service workers를 한번 보자.
위에서 Service worker가 activate를 대기한다는 메시지를 볼 수 있다. 아직은 activate되지 않았다는 뜻이다.
기존 탭이 열려있거나 이미 열려있는 윈도우가 있다면 install은 되지만 activate 되지 않는다.
그 이유는 페이지가 기존 service worker와 이미 통신을 하고 있는 중에 새로운 service worker로 인해 잘못된 정보를 가져올지도 모르기 때문이다.
새로운 service worker를 등록하기 위해서는 기존 탭을 닫고 새창을 띄우면 된다. 그러면 다시 등록 및 activate가 된다. 또한 unregister로 등록해제를 하고 새로고침을 해도 된다.
Non-Lifecycle 이벤트
fetch는 브라우저에서 fetch를 할 때 발생한다. fetch는 HTML 페이지에서 특정 리소스를 로딩할 때 발생한다. (javascript, css, image 등 )
fetch 이벤트를 추가해보자.
self.addEventListener('fetch', function(event) {
console.log('[Service Worker] Fetching something', event);
});
아래와 같이 로그가 표시된다.
[Service Worker] Fetching something FetchEvent {isTrusted: true, request: Request, clientId: '3473dea6-3fdf-4a13-bf02-20994cdb734d', resultingClientId: '', isReload: false, …}
sw.js:11 [Service Worker] Fetching something FetchEvent {isTrusted: true, request: Request, clientId: '3473dea6-3fdf-4a13-bf02-20994cdb734d', resultingClientId: '', isReload: false, …}
sw.js:11 [Service Worker] Fetching something FetchEvent {isTrusted: true, request: Request, clientId: '3473dea6-3fdf-4a13-bf02-20994cdb734d', resultingClientId: '', isReload: false, …}
sw.js:11 [Service Worker] Fetching something FetchEvent {isTrusted: true, request: Request, clientId: '3473dea6-3fdf-4a13-bf02-20994cdb734d', resultingClientId: '', isReload: false, …}
...
...
index.html에서 가져오는 리소스 파일의 정보가 표시되는 것을 확인할 수 있다. (모든 javascript, css, image 등)
fetch 이벤트에서 응답값으로 특정 값을 넘길 수도 있다.
self.addEventListener('fetch', function(event) {
console.log('[Service Worker] Fetching something', event);
event.respondWith(null);
});
이렇게 지정하면 모든 fetch의 응답값으로 null을 넘기는 것으로 모든 리소스를 제대로 가져오지 못하는 결과를 초래한다.
정상적으로 응답하기 위해서 아래와 같이 해야 한다.
self.addEventListener('fetch', function(event) {
console.log('[Service Worker] Fetching something', event);
event.respondWith(fetch(event.request));
});
이것을 활용하여 인터넷이 안되는 환경에서 특정 데이터를 호출하는 식으로 사용할 수 있다.
App 설치 배너 버튼 만들기
앱이 다음과 같은 기준을 만족하면 배너를 자동으로 표시한다.
- web app manifest의 기준
- short_name (홈화면에 사용될 이름)
- name (배너에 표시될 이름)
- 144x144 아이콘 (image/png 유형)
- start_url (시작 url)
- service worker가 등록되어 함
- HTTPS를 사용
- 적어도 두 번째 방문. 적어도 5분 간격
참고: https://developer.chrome.com/blog/app-install-banners-native?hl=ko
배너 설치
사용자가 두 번째 방문하고 배너 클릭을 누르면 배너를 설치할 수 있도록 해보자.
[app.js]
window.addEventListener("beforeinstallprompt", function (event) {
console.log("beforeinstallprompt fired");
event.preventDefault();
deferredPrompt = event;
return false;
});
그리고 화면에 설치 버튼을 하나 만들고 클릭 이벤트를 생성하자.
[index.html]
function installBanner() {
if (deferredPrompt) {
deferredPrompt.prompt(); // prompt가 배너를 표시하지는 않는다.
deferredPrompt.userChoice.then(function(choiceResult) {
console.log(choiceResult.outcome);
if (choiceResult.outcome === 'dismissed') {
console.log('User cancelled installation');
} else {
console.log('User add to home screen');
}
});
deferredPrompt = null;
}
}
<a class="ui primary button" onclick="javascript:installBanner()">설치</a>
그리고 설치 버튼을 클릭하면 아래와 같이 설치 prompt가 표시되는 것을 확인할 수 있다.
아래는 모바일에서 실행한 화면이다.
설치를 누르면 홈 화면에 PWA demo
앱이 설치된 것을 확인할 수 있다.
참고
https://www.udemy.com/course/progressive-web-app-pwa-the-complete-guide