이전 포스트에서 프로그레시브 웹앱(Progressive Web Apps)은 웹과 네이티브 앱이 가진 단점을 개선하는 새로운 형태의 웹앱이라고 설명드렸습니다.
그렇다면, 어떻게 PWA를 개발할 수 있을까요..?
어떠한 웹앱이 PWA가 되기 위해선 여러가지 조건을 충족하여야 합니다. 특히 웹앱 매니페스트(Web App Manifest)와 서비스워커(Service Worker)는 PWA에 필수적으로 포함되어야하는 요소이죠.
이번 포스트에서는 웹앱 매니페스트와 서비스워커를 소개하려고 합니다. 특히, 서비스워커 파트에서는 간단한 실습을 통해서 조금 더 이해하기 쉽도록 설명드려 보겠습니다. 이를 위해 create-react-app으로 프로젝트를 생성하여 진행하겠습니다.React 개발환경을 쉽게 구성할 수 있는 create-react-app에는 이미 웹 매니페스트, 서비스워커 등이 상당 부분 구현되어 있어서 이해하기가 훨씬 수월할 것입니다.
create-react-app 프로젝트 생성
먼저 create-react-app으로 react-pwa라는 이름의 프로젝트를 생성합니다.
1 | create-react-app react-pwa |
위 명령어를 실행하고 나면 react-pwa 폴더가 생성됩니다. 그리고 이 폴더를 열어보면 다음과 같이 프로젝트가 구성되어 있는데요. 여기서 주목해야 할 것은 manifest.json
과 registerServiceWorker.js
입니다.
먼저 웹앱 매니페스트(Web App Manifest)부터 알아보겠습니다.
웹앱 매니페스트(Web App Manifest)
웹앱 매니페스트란 앱에 대한 정보를 담고 JSON 파일입니다. 배경색은 어떠한 색인지, 앱의 이름은 무엇인지, 홈스크린 화면에 추가할 때 아이콘은 어떤 것인지 등의 정보를 담고 있죠. 웹앱 매니페스트는 manifest.json
파일명을 대부분 사용합니다.
manifest.json
1 | { |
항목별 설명
- short_name: 사용자 홈 화면에서 아이콘 이름으로 사용
- name: 웹앱 설치 배너에 사용
- icons: 홈 화면에 추가할때 사용할 이미지
- start_url: 웹앱 실행시 시작되는 URL 주소
- display: 디스플레이 유형(fullscreen, standalone, browser 중 설정)
- theme_color: 상단 툴바의 색상
- background_color: 스플래시 화면 배경 색상
- orientation: 특정 방향을 강제로 지정(landscape, portrait 중 설정)
서비스워커(Service Worker)
서비스워커는 브라우저의 백그라운드에서 실행되는 자바스크립트 워커입니다. PWA는 네이티브 앱처럼 오프라인 상태에서도 사용가능하고, 푸시 알림(Notification) 기능도 사용할 수 있는데요. 이런 기능을 할 수 있도록 도와주는 것이 바로 서비스워커입니다. 향후에는 서비스워커에 지오펜싱(Geofencing) 기능도 추가될 예정이라고 하네요.
생명주기(Life Cycle)
서비스워커는 다음과 같은 생명주기를 가집니다.
서비스워커를 설치하기 위해서는 먼저 등록을 해야하는데요. 서비스워커 등록 코드는 registerServiceWorker.js
에 이미 구현이 되어있습니다. 등록을 하게 되면 브라우저가 백그라운드에서 서비스워커 설치를 시작하게 됩니다. 설치단계 동안에는 정적자원을 캐싱하는 작업을 진행하게 되고, 모든 정적자원의 캐싱이 완료되고 나면 비로소 서비스워커 설치가 완료됩니다. 이렇게 서비스워커가 설치 되고, 활성화되고 나면 비로소 서비스워커가 기능을 할 수 있게 되는 것이죠.
서비스워커 등록 과정
registerServiceWorker.js
에 구현되어 있는 register()
, registerValidSW()
함수입니다.
registerServiceWorker.js1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64(...)
export default function register() {
if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) {
// The URL constructor is available in all browsers that support SW.
const publicUrl = new URL(process.env.PUBLIC_URL, window.location);
if (publicUrl.origin !== window.location.origin) {
// Our service worker won't work if PUBLIC_URL is on a different origin
// from what our page is served on. This might happen if a CDN is used to
// serve assets; see https://github.com/facebookincubator/create-react-app/issues/2374
return;
}
window.addEventListener('load', () => {
const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`;
if (isLocalhost) {
// This is running on localhost. Lets check if a service worker still exists or not.
checkValidServiceWorker(swUrl);
// Add some additional logging to localhost, pointing developers to the
// service worker/PWA documentation.
navigator.serviceWorker.ready.then(() => {
console.log(
'This web app is being served cache-first by a service ' +
'worker. To learn more, visit https://goo.gl/SC7cgQ'
);
});
} else {
// Is not local host. Just register service worker
registerValidSW(swUrl);
}
});
}
}
function registerValidSW(swUrl) {
navigator.serviceWorker
.register(swUrl)
.then(registration => {
registration.onupdatefound = () => {
const installingWorker = registration.installing;
installingWorker.onstatechange = () => {
if (installingWorker.state === 'installed') {
if (navigator.serviceWorker.controller) {
// At this point, the old content will have been purged and
// the fresh content will have been added to the cache.
// It's the perfect time to display a "New content is
// available; please refresh." message in your web app.
console.log('New content is available; please refresh.');
} else {
// At this point, everything has been precached.
// It's the perfect time to display a
// "Content is cached for offline use." message.
console.log('Content is cached for offline use.');
}
}
};
};
})
.catch(error => {
console.error('Error during service worker registration:', error);
});
}
(...)
register()
, registerValidSW()
두 함수에 서비스워커 등록이 잘 되는지 파악하기 위해 console.log()
함수를 각각 추가해보겠습니다.
1 | (...) |
수정이 완료되고 나면 다음 명령어로 프로젝트를 실행해봅니다.
1 | yarn start |
성공적으로 실행하고 나면, 크롬 브라우저를 열고 localhost:3000
에 접속한 뒤, console 창을 확인합니다. 그러면 development 레벨이기 때문에 서비스워커를 등록할 수 없다는 메시지를 확인할 수 있습니다.
프로젝트를 production 레벨로 실행하기 위하여 yarn build
혹은 npm run build
명령어로 빌드를 하고 파이어베이스로 호스팅해보겠습니다.(과정 생략)
호스팅을 한 링크로 접속하고 console 창을 확인해보면 다음과 같이 서비스워커가 등록되었다는 메시지를 확인할 수 있습니다.(링크 - https://react-pwa-altenull.firebaseapp.com/)
실제 서비스워커는 개발자도구(F12)의 Application 탭에서 Service Workers 메뉴에서 확인가능 합니다.
정적 자원 캐싱
서비스워커 설치단계에서는 정적 자원을 캐싱한다고 설명드렸습니다. 캐싱이 완료되고 나면 어떠한 일이 벌어지는지 알아보겠습니다.
최초 접속
파이어베이스로 호스팅을 하고 난 뒤, 최초 접속했을 때에 Network 탭 화면입니다. 번들링된 css, js 파일 등을 서버로부터 다운로드하는데요. Size와 Time부분을 주목해주세요.
재접속
다음은 새로고침(F5)을 하고 나서 캡처한 Network 탭 화면입니다. Size와 Time이 이전과 많이 달라졌네요. 시간은 크게 줄어들었고, 특히 Size가 (from ServiceWorker)로 바뀌었습니다.
서비스워커는 설치되고 나면 브라우저와 서버 중간에 위치해 브라우저에서 보내는 요청들을 감시합니다. 이 때 요청에 대한 응답, 즉 자원이 캐싱되어 있다면 굳이 서버에서 받을 필요 없이 캐싱된 자원을 리턴합니다. 그렇기 때문에 응답시간도 현저히 줄어든 것입니다.
Network Disconnect
이번에는 네트워크 연결을 해제하고 접속해보겠습니다. 사용중인 와이파이를 꺼주시거나, 개발자도구(F12) - Application - Service Workers에서 offline을 체크하고 다시 접속해봅니다.
그러면 404 Not Found 에러가 발생하지 않고 네트워크가 연결되어 있을 때와 동일한 페이지를 보실 수 있으실 겁니다. 왜냐하면 지금 보여지고 있는 자원들은 서버에서 받은 자원이 아닌 서비스워커로부터 받은 캐싱된 자원이기 때문입니다.
간단한 실습을 통해 어떻게 PWA가 서비스워커를 통해서 오프라인 상태에서도 동작이 가능한지 알아보았습니다. 주의해야 하실 점은 최초 접속 시에는 네트워크가 연결된 상태에서 서비스워커를 등록하고 설치해야한다는 점입니다.
마치며
이번 포스트에서는 웹앱 매니페스트(Web App Manifest)와 서비스워커(Service Worker)에 대해서 알아보았는데요. 설명이 부족하기도 했고 더 많은 기능을 소개드리지 못했지만, PWA를 처음 접하시는 분들에게 조금이나마 도움이 되셨으면 좋겠습니다.
참고
- Google Developers - Service Workers
- Google Developers - Web App Manifest
- Google Developers - App Install Banners