메뉴

문서정보

목차

PWA에 대해서

PWA(Progressive Web App)은 HTML, CSS, JavaScript를 포함한 일반적인 웹 기술을 사용하여 개발된 응용 프로그램 소프트웨어다. 표준적인 웹 브라우저를 포함한 모든 플랫폼에서 작동한다. 이렇게 봐서는 일반적인 웹 애플리케이션과 무슨 차이가 있느냐 하겠지만, 오프라인 작업, 푸시 알람, 데스크톱 및 모바일로의 설치(네이티브 응용 프로그램과 유사한 사용자 환경)가 가능하다. 웹 애플리케이션이기 때문에 App Store, Google Play와 같은 배포시스템이 필요 없다.

웹 애플리케이션은 물론 모바일 장치에서도 (웹 브라우저를 이용해서) 접근 할 수 있었지만 느리고, 기능이 부족하며, 찾기가 힘들었기 때문에 사용성이 떨어지는 측면이 있다. 하지만 오프라인에서 작업이 가능하고 모바일 장치에서 직접 실행되는 PWA의 기능을 이용해서 네이티브 앱과의 격차를 해소 할 수 있다.

PC에서 시작한 인터넷 환경은 모바일 디바이스로 이동하고 있으며 이에 따라 모바일 웹과 네이티브 앱으로 불리는 두 가지 종류의 앱이 경쟁하고 있다. 아래 정보는 미국 기준이다.

앱은 웹에 비해서 기능적인 장점도 가지고 있다. 기능과 성능상의 이점, 절대적인 사용시간으로 웹 사용이 줄어들 것으로 예상되지만 그럴 것 같지는 않다. 고유의 장점이 있기 때문이다. 웹의 문제는 재방문율의 문제라고 할 수 있을 것이다. 홈 화면에 아이콘이 설치되고 클릭만 하면 되는 앱에 비해서, 웹의 경우 URL을 기억해야 해서 재 방문하기가 쉽지 않다. 북마크라는 도구가 있기는 하지만 앱의 편리함에 비교할 바가 아니다. 스마트폰에서 북마크를 이용해서 웹에 접근하는 경우를 본 적이 없다.

PWA는 아래와 같은 해법을 제시하고 있다.

도입 사례

 도입사례

.

테스트 전에

PWA를 쉽게 만들 수 있을 것 같아서 S3 Static Web hosting방식을 선택했는데, 쓸는 고생을 했다.

테스트 환경

PWA는 웹 애플리케이션이다. HTTPS를 기본으로 하고 있기 때문에, 적당한 개발환경을 만들어줘야 한다. 나는 S3의 static wab hosting을 이용해서 PWA를 테스트하기로 했다. 서비스 도메인은 pwa.joinc.co.kr이다.

S3에 pwa.joinc.co.kr bucket를 만든다. S3 버킷을 static web hosting 하면, AWS 도메인이 만들어진다. 이 도메인으로는 서비스 할 수 없으므로 퍼블릭 도메인과 연결해야 하는데, 반드시 도메인이름과 버킷이름이 일치해야 한다.

 테스트 환경

 Static website hosting

Static website hosting 설정을 한다.

 Static website hosting 설정

 Static website hosting 설정

"Use this bucket to host a website"를 체크하면 Static website hosting을 활성화 할 수 있다. index document 와 Error document는 옵션이다. Endpoint 주소로 접근 할 수 있다.

테스트를 위해서 S3 버킷에 파일을 올렸다.
# aws s3 cp index.html s3://pwa.joinc.co.kr/

테스트해보자.
# curl  http://pwa.joinc.co.kr.s3-website.ap-northeast-2.amazonaws.com -I
HTTP/1.1 403 Forbidden
x-amz-error-code: AccessDenied
x-amz-error-message: Access Denied
x-amz-request-id: 40168CAAA931BC7B
x-amz-id-2: mEj3jkYtvwFzIPJmPfAQeXvkRE1BNQ4YmaXXIHULbmCMObwwE/goeJV/9gNyLCPTU8YfN17JB2g=
Transfer-Encoding: chunked
Date: Sat, 09 May 2020 13:02:05 GMT
Server: AmazonS3
HTTP 오류가 떨어지는 걸 보면 인터넷에서 접근가능하다는 걸 알수 있다. 이제 문제원인을 찾아서 해결하면 된다. 403 오류이면 권한 오류일 것이다. Permissions > Bucket Policy에서 버킷에 대한 정책을 설정하면 된다.

 권한 설정

 권한 설정

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Sid": "PublicReadGetObject",
            "Effect": "Allow",
            "Principal": "*",
            "Action": "s3:GetObject",
            "Resource": "arn:aws:s3:::pwa.joinc.co.kr/*"
        }
    ]
}

다시 테스트.
# curl  http://pwa.joinc.co.kr.s3-website.ap-northeast-2.amazonaws.com
<!doctype html>
<html lang="en">
<body class="fullscreen">
<h1>Hello World!</h1>
</body>
</html>

성공. 이제 도메인을 pwa.joinc.co.kr로 바꾸자. 현재 나는 joinc.co.kr 호스트 존을 관리하고 있다. 아래와 같이 A 레코드를 추가했다.

 A 레코드 추가

테스트해보자.
# curl http://pwa.joinc.co.kr
<!doctype html>
<html lang="en">
<body class="fullscreen">
<h1>Hello World!</h1>
</body>
</html>

ACM(SSL) 연동

PWA는 SSL을 필요로 한다. S3 Static webserver hosting은 SSL을 지원하지 않는다. 클라우드프론트(CloudFront)를 이용해서 HTTPS를 서비스 하기로 했다.(왠지 배보다 배꼽이 더 커진 것 같다..)

joinc.co.kr 도메인에 대한 SSL을 ACM으로 관리하고 있으므로 연동만 시켜주면 될 것 같지만 그렇지 않다. ACM은 리전단위 서비스인데, 클라우드프론트를 위한 ACM은 버지니아 리전(us-eas-1)에 있는 것만을 사용 할 수(이 것 때문에 꽤나 헷갈렸다) 있다. 버지니아 리전 ACM을 이용해서 pwa.joinc.co.kr 인증서를 만들었다.

CloudFront > Create Distribution을 선택한다.

g

g

중요 설정만 설명한다. Yes, Edit를 눌러서 저장한다. 저장을 끝내면 "xxxx.cloudfront.net" 패턴의 CloudFront 도메인 이름이 나온다. 이 이름을 pwa.joinc.co.kr 레코드의 CNAME으로 설정한다.

 CloudFront와 Route 53 설정

curl로 테스트해보자.
# curl https://pwa.joinc.co.kr
<!doctype html>
<html lang="en">
<body>
    <h1 class="title">Hello World!</h1>
</body>
</html>

이렇게 환경 설정이 끝났다. 정말 배보다 배꼽이 더 컸다. 이제 PWA 애플리케이션을 만들어보자.

디렉토리 구성

애플리케이션 디렉토리는 아래와 같이 구성된다.
├── css
│   └── style.css
├── favicon.ico
├── images
│   ├── joinc-icon-128.png
│   ├── joinc-icon-144.png
│   ├── joinc-icon-152.png
│   ├── joinc-icon-192.png
│   ├── joinc-icon-256.png
│   └── joinc-icon-512.png
├── index.html
├── js
│   └── main.js
├── manifest.json
└── sw.js

코드를 분석하기 전에 작동을 먼저 확힌해보자.

영상을 만들기 쉬워서 안드로이드 애뮬레이터로 테스트를 했다. https://pwa.joinc.co.kr 로 접근하면 페이지 하단에 애플리케이션을 홈에 설치 할 것인지를 묻는다. 설치하고 나면 홈 화면에 앱 아이콘이 생긴다. 실행화면도 네이티브 앱과 비슷하다.

PWA는 데스크탑에도 설치 할 수 있다. 리눅스 운영체제에서 설치해 봤다.

코드 분석

index.html 부터 분석해보자.
<!doctype html>
<html lang="en">
<head>
  <meta http-equiv="Content-Type" content="text/html;charset=UTF-8" />
  <title>PWA EXAMPLE</title>
  <link rel="manifest" href="manifest.json">
  <link rel="stylesheet" href="css/style.css">
  <link rel="icon" href="favicon.ico" type="image/x-icon" />
  <link rel="apple-touch-icon" href="images/joinc-icon-152.png">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <meta name="theme-color" content="white"/>
  <meta name="apple-mobile-web-app-capable" content="yes">
  <meta name="apple-mobile-web-app-status-bar-style" content="black">
  <meta name="apple-mobile-web-app-title" content="Hello World">
  <meta name="msapplication-TileImage" content="images/hello-icon-144.png">
  <meta name="msapplication-TileColor" content="#FFFFFF">
</head>
<body class="fullscreen">
  <div class="container">
	  <h1 class="title">PWA.JOINC.CO.KR</h1>
  </div>
  <script src="js/main.js"></script>
</body>
</html>
일반적인 HTML 문서와 다를게 없다. 2줄 정도의 코드만 주의해서 살펴보면 된다. manifestservice work는 PWA의 핵심 구성요소다.

manifest.json

manifest.json은 브라우저에게 이 웹 서비스가 PWA이며, 데스크탑 또는 모바일 앱에 설치할 때 어떻게 작동해야 할지를 알려주기 위한 정보들을 포함하고 있다. 예제에서 사용한 manifest.json 내용이다.
{
  "name": "Hello World",
  "short_name": "Hello",
  "icons": [{
    "src": "images/joinc-icon-128.png",
      "sizes": "128x128",
      "type": "image/png"
    }, {
      "src": "images/joinc-icon-144.png",
      "sizes": "144x144",
      "type": "image/png"
    }, {
      "src": "images/joinc-icon-152.png",
      "sizes": "152x152",
      "type": "image/png"
    }, {
      "src": "images/joinc-icon-192.png",
      "sizes": "192x192",
      "type": "image/png"
    }, {
      "src": "images/joinc-icon-256.png",
      "sizes": "256x256",
      "type": "image/png"
    }, {
      "src": "images/joinc-icon-512.png",
      "sizes": "512x512",
      "type": "image/png"
    }],
  "lang": "en-US",
  "start_url": "/index.html",
  "display": "standalone",
  "background_color": "white",
  "theme_color": "white"
}

short_name & name

앱 이름이다. manifest는 short_namename둘 중 하나를 설정해야 한다. 둘 다 설정할 경우 name이 우선사용되며, 화면의 크기등에 따라서 short_name이 사용 된다.

icon

홈 스크린, app launcher, task switcher, 스플래시 화면 등에서 사용 할 아이콘을 정의 할 수 있다. 하나 이상의 아이콘을 설정 할 수 있으며, 각 아이콘은 크기와 이미지 타입 속성을 포함한다. 안드로이드 적응형 아이콘 기능을 사용하려면 icon 속성에 "purpose":"any maskable"를 추가한다.

크롬의 경우 최소한 192x192 와 512x512 픽셀 크기의 아이콘을 제공해야 한다. 이 두 아이콘만 설저오딜 경우, 크롬이 기기에 맞게 아이콘 크기를 조정한다. 기기에 맞는 깔끔한 아이콘이 필요하다면 48dp 단위로 아이콘을 만들면 된다.

start_url

start_url은 필수이며 애플리케이션이 시작할 때, 시작할 페이지위치를 알려준다. start_url을 이용해서 일반 웹에서의 시작점과 PWA 에서의 시작지점을 다르게 할 수 있다.

background_color

PWA가 처음 시작 할 때, 백그라운드 색을 설정한다.

display

앱이 시작될때의 UI를 지정할 수 있다.

scope

사용자가 앱 내에 있다고 간주하는 URL 집합을 정의한다. 설정된 URL 집합을 벗어나면 앱 바깥으로 나가는 것으로 판단한다. 외부 URL 등을 클릭할 경우가 되겠다. start_url은 scope 범위내에 있어야 한다.

manifest를 웹에 추가하기

<link>태그를 이용해서 추가 할 수 있다.
<link rel="manifest" href="manifest.json">

manifest 테스트하기

Chrome Dev 툴의 Application패널에서 manifest가 제대로 작성됐는지를 확인 할 수 있다.

 Chrome Dev

 Chrome Dev

pwa.joinc.co.kr의 manifest 정보다. 이 시점에서는 경고가 하나 있었다. 아이콘의 크기를 실수로 511x512로 해서 발생한 경고였다. 실행에는 문제가 없다.

service worker

 Service worker

서비스 워커(servicer worker)는 백그라운드에서 독립적으로 실행되는 스크립트다. 페이지 캐싱, API 호출 캐싱, 푸시 알림, 백그라운드 동기화 등과 같은 독립적인 앱으로 작동하기 위한 작업을 수행한다.

서비스 워커의 라이프사이클

서비스 워커는 parsed, installing, installed, activating, activated, redundant 의 6가지 상태 중 하나의 상태를 가진다.

 Service worker lifecycle

Parsed & Installing

서비스 워커 등록을 시도하면 사용자 에이전트는 스크립트 구문을 분석한다. 구문 분석에 성공하면(HTTPS를 비롯한 요구 사항이 충족되면) 서비스 워커 등록 객체에 액세스 할 수 있다. 여기에는 서비스 워커의 상태와 scope와 같은 정보가 포함된다. 예제의 js/main.js코드를 살펴보자.
/* js/main.js */
window.onload = () => {
  'use strict';

  if ('serviceWorker' in navigator) {
    navigator.serviceWorker
             .register('./sw.js');
  }
}
브라우저가 Service worker API를 지원한다면 ServiceWorkerContainer.register()메서드를 이용해서 사이트에 등록한다. 서비스 워커의 실제 코드는 sw.js에 있으며, 등록이 성공한 후 실행된다. 이게 서비스 워커를 등록하기 위한 전부다. 나머지 다른 것들은 sw.js에 있다. sw.js 파일의 내용을 보자.
var cacheName = 'hello-pwa';
var filesToCache = [
  '/',
  '/index.html',
  '/css/style.css',
  '/js/main.js'
];

/* Start the service worker and cache all of the app's content */
self.addEventListener('install', function(e) {
  e.waitUntil(
    caches.open(cacheName).then(function(cache) {
      return cache.addAll(filesToCache);
    })
  );
});

/* Serve cached content when offline */
self.addEventListener('fetch', function(e) {
  e.respondWith(
    caches.match(e.request).then(function(response) {
      return response || fetch(e.request);
    })
  );
});
서비스 워커는 주요 이벤트에 대한 이벤트 리스터를 제공한다. 첫번째 이벤트는 install 이벤트다. install 리스너에서 캐시를 초기화하고 오프라인 사용을 위한 파일들을 다운로드 할 수 있다.

캐시를 저장할 변수를 생성하고, 캐시 목록을 정의한다. 위 예제에서 캐시 변수는 cacheName이고, 캐시할 페이지 목록은 fileToCache에 저장했다. 이제 캐시할 파일을 모두 저장 할 때까지 기다리게(waitUntil)된다. 캐시가 모두 끝나면 install 이 끝난다.

패치응답 처리를 위한 fetch이벤트도 설정했다. 이는 앱이 HTTP 요청을 할 때 발생을 한다. fetch로 요청을 가로채서 응답을 만들 수 있어서 매우 유용하다.

정리

참고

  1. A Beginner's Guid to Progressive Web Apps
  2. Amplify Docs - Service Worker