Recommanded Free YOUTUBE Lecture: <% selectedImage[1] %>

Contents

PWA에 대해서

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

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

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

  • 2018년 기준 모바일 기기의 사용시간은 평균 3시간 35분이며, 연간 11분 이상 증가할 것으로 예상하고 있다.
  • 2019년 기준 사용자는 스마트 폰에서 90% 이상의 시간을 앱으로 소비했다.
  • 앱의 주요 활동은 디지털 오디오이며, 50분 이상을 오디오를 듣는데 소비한다.
  • 소셜 네트워킹은 40분 그 다음으로 모바일 비디오와 게임, 메시징이 있다.
앱은 웹에 비해서 기능적인 장점도 가지고 있다.
  • 카메라, 마이크 등 모바일 기기에서 제공하는 기능/기술을 사용 할 수 있다. : Capability가 뛰어나다.
  • 스마트폰에 설치가 되기 때문에, 느린 네트워크에서도 작동한다.
  • 부드럽고 빠른 화면 전환, 애니메이션을 보여줄 수 있다.
기능과 성능상의 이점, 절대적인 사용시간으로 웹 사용이 줄어들 것으로 예상되지만 그럴 것 같지는 않다. 고유의 장점이 있기 때문이다.
  • 50% 이상의 유저가 한달 동안 한 건의 앱도 다운로드 받지 않는다.
  • 특정 3개의 앱에서 사용시간의 77%를 보내고 있다. 이 중 하나의 앱이 49%를 점유한다.
  • 반면, 모바일 사용자들은 한 달에 평균 100개 이상의 웹 사이트를 방문한다.
  • 웹 사이트에 접근하기 위해서는 URL 링크만 클릭하면 된다. 다른 어떠한 준비 작업도 필요 없다.
웹의 문제는 재방문율의 문제라고 할 수 있을 것이다. 홈 화면에 아이콘이 설치되고 클릭만 하면 되는 앱에 비해서, 웹의 경우 URL을 기억해야 해서 재 방문하기가 쉽지 않다. 북마크라는 도구가 있기는 하지만 앱의 편리함에 비교할 바가 아니다. 스마트폰에서 북마크를 이용해서 웹에 접근하는 경우를 본 적이 없다.

PWA는 아래와 같은 해법을 제시하고 있다.
  • 웹의 도달성은 그대로 제공하자.
  • 스마트폰 홈 화면에 아이콘을 배치하자 : 앱처림 직관적으로 실행 할 수 있다.
  • 리소스의 일부를 스마트폰에 설치하자 : 느린 네트워크 혹은 오프라인에서도 작동 할 수 있다.
  • 백그라운드에서도 작동, Push 등의 기능을 제공한다.
  • 카메라와 마이크로폰을 사용할 수 있게 한다.

도입 사례

 도입사례

  • Treebo : 스마트폰 앱과 비교해서 전년 대비 전환율이 4배 향상됐다. 또한 반복 사용자의 전환율이 3배 증가한 것으로 나타났다. 이는 네이티브 모바일 앱과 비교해서 뛰어난 도달률을 보여준다.
  • Pinterest : 전체 모바일 사이트를 PWA로 재구성했다. 핵심 참여자가 60% 증가했으며, 사용자 광고수익도 44% 증가했다.
  • Tinder : 로딩시간을 11.91초에서 4.69초로 줄였다. PWA는 안드로이드앱보다 90% 작은 크기를 유지한다.
  • Uber : 2G에서도 빠르게 작동하도록 설계했다. 기본앱은 50k이며, 2G 네트워크에서도 3초이내에 로드 할 수 있다.
.

테스트 전에

PWA를 쉽게 만들 수 있을 것 같아서 S3 Static Web hosting방식을 선택했는데, 쓸는 고생을 했다.
  • S3 Static Web hosting은 HTTPS를 지원하지 않는다. 결국 CloudFront까지 붙여야 했다.(PWA는 HTTPS에서 작동한다.
  • Route 53, ACM 까지 만져줘야 했다.
  • CloudFront는 버지니아 리전 ACM만 사용 할 수 있다. 분명히 서울리전에서 ACM 설정했는데, 설정에 보이지 않는 바람에 삽질을 꽤 했다.
  • AWS 가 사용하기 쉽다고 하는데, 손에 익을 정도로 자주 사용하는게 아니라면 꽤나 삽질을 하기 마련이다.
  • S3에 올리면 인스턴스도 필요 없으니 편할 것으로 생각했는데, 그냥 EC2로 할 걸 그랬다.

테스트 환경

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

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

 테스트 환경

  • 버킷 이름은 pwa.joinc.co.kr로 한다.
  • S3 버킷은 기본설정이 Public Access을 막는다. Block all public access설정을 풀어서 인터넷에서 접근 가능하도록 한다.
 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을 선택한다.

중요 설정만 설명한다.
  • Price Class : 배포범위를 설정한다. 넓은 지역에 배포 할 수록 더 많은 비용이 나간다. Use All Edge Locations를 선택했다.
  • Alternate Domain Names(CNAMEs) : 외부에 공개될 도메인 이름이다. pwa.joinc.co.kr로 설정한다.
  • SSL Certificate : pwa.joinc.co.kr도메인에 대한 SSL을 설정해야 한다. 버지니아 리전에 등록한 인증서를 선택한다. 자동으로 리스트가 나오기 때문에 쉽게 설정 할 수 있다.
  • Default Root Object : URL에 페이지를 명시하지 않았을 경우 기본으로 호출할 페이지다. index.html을 설정했다.
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줄 정도의 코드만 주의해서 살펴보면 된다.
  • manifest.json : PWA 의 작동방식과 리소스를 알려주는 JSON 파일이다. 애플리케이션 설정파일인 셈이다.
  • js/main.js : service worker를 정의하고 있다.
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를 지정할 수 있다.
  • fullscreen : 브라우저 UI 없이, 화면 전체를 채운다. 독립적으로 실행되는 네이티브 앱 처럼 보일 것이다.
  • standalone : 조절가능한 창 형태를 가진다. 데스크탑 애플리케이션처럼 보일 것이다.
  • minimal-ui : standalone과 유사하지만 최소한의 UI요소만 가진다.
  • browser : 일반적인 브라우저처럼 실행된다.

scope

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

manifest를 웹에 추가하기

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

manifest 테스트하기

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

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

service worker

 Service worker

서비스 워커(servicer worker)는 백그라운드에서 독립적으로 실행되는 스크립트다. 페이지 캐싱, API 호출 캐싱, 푸시 알림, 백그라운드 동기화 등과 같은 독립적인 앱으로 작동하기 위한 작업을 수행한다.
  • 서비스 워커는 네트워크 요청을 캐시 할 수 있다.
  • 앱이 활성화되지 않은 경우 푸시 알림을 받을 수 있다.
  • 앱을 오프라인에서 작동시키는 데 사용 할 수 있다.
  • 정적 리소스를 캐시 할 수 있다.
  • DOM에 직접 엑세스하거나 상호 작용 할 수 없다.

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

서비스 워커는 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로 요청을 가로채서 응답을 만들 수 있어서 매우 유용하다.

정리

  • 대략 핵심적인 것들은 다룬 것 같다.
  • 캐시삭제, 앱 업데이트 등을 다루어야 한다.
  • Vuejs와 Foundation을 이용해서 앱을 만들어보자.
  • AWS Amplify를 이용해보자.
  • 프로젝트에 적용해 보자.
  • Pinterest 사용해보자. 괜찮다.

참고

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