Cloud Firestore Web Codelab

구글의 코드랩에는 좋은 정보가 넘쳐나네요.

Firebase 데이터베이스를 사용해볼까해서 Cloud Firestore Web Codelab 이 부분을 살펴봅니다.

오역,의역이 많으니 참고정도만 하시고 원문으로 공부하세요.

1. Overview

Goals

이 코드랩에서는 Firestore를 이용해 음식점 추천 웹앱을 만들겠습니다. 여기에서 배울점은

    – 웹앱에서 Firestore의 자료를 읽고 쓰기
    – Firestore 자료의 변경사항을 실시간으로 얻기
    – Firebase 인증 및 보안규칙을 사용하여 Firestore 자료 보호
    – 복잡한 Firestore 쿼리 작성

Prerequisites

이 코드랩을 시작하기전에 설치되었는지 확인하세요:

    – Node.js

Node.js는 우리의 앱을 실행하고 테스트하기위해 필요할 뿐 최종 어플리케이션은 Node.js에 종속되지 않습니다.

2. Get the Sample Project

Download the Code

샘플 프로젝트를 복제해 시작합니다.

git clone https://github.com/firebase/friendlyeats-web
cd friendlyeats-web

다음으로 Cloud Firestore가 지원되는 Firebase CLI 버전을 가져와야 합니다:

npm install -g firebase-tools
Create a Project

문서를 참고하여 새로운 Firestore 프로젝트를 만듭니다. 보안 규칙을 선택하라는 메시지가 표시되면 “test mode”로 시작하세요. 코드랩에서 나중에 변경합니다.

프로젝트명은 firestorequickstarts로 해야 진행이 수월했습니다.

Security rules for Cloud Firestore

Set Up Firebase

우리의 웹앱 템플릿은 Firebase Hosting 환경에서 자동으로 프로젝트의 설정을 가져오도록 구성되어 있습니다. 하지만 우리는 프로젝트를 앱과 연동시켜야 합니다.

firebase use --add

현재까지 왔을때 위 명령을 실행하면 인증(자격증명)이 되질 않았으니 로그인을 하라고 나타납니다. Error: Command requires authentication, please run firebase login / firebase login 명령을 내리면 구글 계정으로 인증을 할수 있게 되고 인증처리가 완료되면 작업을 계속 이어나갈수가 있습니다.

이 프로젝트에 별칭을 지정하라는 메시지가 표시됩니다. 이것은 여러 환경(개발, 안정화 기타)을 가지고 있는 경우에 유용합니다. 그렇지만 이 예에서는 “default”라고 이름짓습니다. 이제 우리의 웹앱을 실행하고 어떤 Firebase(and Firestore) 프로젝트가 사용될지 자동으로 알려주기위해 firebase serve를 사용할 수 있습니다.

3. Enable Firebase Authentication

인증은 이 코드랩의 초점이 아니지만 앱에서는 어떤 형태로든 인증을 받는것이 중요합니다. 우리는 익명 로그인을 사용할것입니다. 즉 사용자가 메시지 없이 자동 로그인되는것을 의미합니다.

Firebase 콘솔을 사용해 앱에서의 익명 인증을 사용할 수 있습니다. 자동으로 인증 공급자 구성 페이지로 이동하려면 다음 명령을 실행하세요.

firebase open auth

또는 Firebase 콘솔에서 Authentication > Sign-In Method(로그인 방법)로 가세요.

이 페이지에서 Anonymous(익명)을 클릭한 후 Enable(사용 설정)을 클릭하고 Save(저장)을 클릭하세요.

Anonymous Authentication

4. Run the Local Server

우리의 앱을 실제로 시작할 준비가 되었습니다! Firebase 명령을 사용하여 로컬에서 실행합시다.

firebase serve

이제 브라우저를 열어 localhost:5000을 봅시다. Firebase 프로젝트에 연결된 FriendlyEats의 사본을 볼수 있습니다.

FriendlyEats

앱은 우리의 프로젝트에 자동으로 연결되어 익명 사용자로 자동 로그인됩니다.

5. Write Data to Firestore

이 섹션에서는 Firestore에 데이터를 조금 기록해 앱의 UI에 나타나게 할것입니다. 이 작업은 Firebase 콘솔을 통해 수동으로 수행할 수 있지만 Firestore의 기본적인 기록방법을 보여주기 위해 앱 자체에서 수행할것입니다.

우리 앱의 메인 모델 개체는 음식점입니다. Firestore 데이터는 documents, collections와 subcollections로 나뉩니다. 각 음식점은 “restaurants”라는 최상위 collection의 document로 저장될것입니다. Firestore 데이터 모델에 대해 더 자세히 알고 싶다면 이곳에 있는 documents와 collection을 참고하세요.

scripts/FriendlyEats.Data.js 파일을 열어 FriendlyEats.prototype.addRestaurant 함수를 찾아 전체 함수를 아래에 있는 코드로 바꾸세요.

FriendlyEats.Data.js

FriendlyEats.prototype.addRestaurant = function(data) {
    var collection = firebase.firestore().collection('restaurants');
    return collection.add(data);
};

위 코드는 restaurants collection에 새 document를 추가합니다. document 데이터는 일반 자바스크립트 객체에서 가져옵니다. Firestore collection restaurants에 대한 참조를 얻고 데이터를 add’ing합니다.

Security rules

우리는 Firestore에 documents를 기록하기 전에 Firestore의 보안 규칙을 열고 데이터베이스의 어느 부분을 어떤 사용자가 쓸수 있게 할지를 지정해야 합니다. 현재로서는 인증된 사용자만이 전체 데이터베이스를 읽고 쓸 수 있습니다. 이는 최종전달될 앱에서야 그렇게 해야겠지만 앱을 만드는중에는 좀더 편안하게 사용하길 바랄껍니다. 테스트마다 인증 문제가 계속 발생하는것을 좋아할수는 없겠지요. 이 코드랩의 끝부분에서 보안규칙을 강화하고 의도하지 않은 읽기와 쓰기의 가능성을 제한하는 방법에 대해 설명합니다.

Firebase 콘솔을 열어 Database > Rules in the Firestore tab(규칙)으로 가세요. 기본 규칙은 모든 사용자가 데이터베이스에서 읽거나 쓰지 못하도록 되어 있습니다. 기본규칙을 다음 규칙으로 바꿉니다.

firestore.rules

service cloud.firestore {
    match /databases/{database}/documents {
        match /{document=**} {
            // Only authenticated users can read or write data
            allow read, write: if request.auth != null;
        }
    }
}

간단히 위의 내용을 Firebase 콘솔에 복사-붙여넣기하고 게시하세요. 다른방법으로 커맨드라인 명령을 통해 파일을 사용하여 배포 할수도 있습니다. 이를 실행하려면 다음과 같이 하세요.

firebase deploy --only firestore:rules

위 명령은 규칙이 이미 담겨있는 파일 firestore.rules을 배포합니다.

Note: 보안규칙에 대해 더 알고 싶다면 security rules documentation을 참고하세요. 또한 FriendlyEats에 대한 더 세밀한 접근 제어를 설정할수 있는 firestore.rules 파일을 살펴볼수도 있습니다.

페이지를 새로고침하고 “Add Mock Data” 버튼을 클릭(탭)하면 아직 앱에서는 볼수 없지만 음식점 documents가 일괄적으로 생성됩니다. 이제 데이터 검색을 구현해야 합니다.

다음으로 Firebase 콘솔의 Firestore(데이터) tab을 보면 음식점(restaurants) 컬렉션에 새 항목이 표시됩니다.

Firestore Data tab

웹앱에서 Firestore에 데이터를 기록한걸 축하합니다. 다음 섹션에서 Firestore에서 어떻게 데이터를 가져와서 앱에 표시할수 있을지에 대해 배워보겠습니다.

6. Display Data from Firestore

이 섹션에서는 Firestore에서 데이터를 꺼내 앱에 표시하는것에 대해 알아보겠습니다. 두가지 주요 단계는 쿼리를 만들고 스냅샷 리스너를 추가하는 것입니다. 이 리스너는 실시간으로 업데이트 되는 쿼리에 일치하는 모든 데이터를 통보받습니다.

우선 필터되지 않은 음식점 목록을 기본으로 제공하는 쿼리를 작성합시다. 이 코드를 FriendlyEats.prototype.getAllRestaurants() 메소드에 넣습니다:

FriendlyEats.Data.js

var query = firebase.firestore()
                    .collection('restaurants')
                    .orderBy('avgRating', 'desc')
                    .limit(50);
this.getDocumentsInQuery(query, render);

이 스니펫으로 평균등급(avgRating, 현재 모두 0)으로 정렬된 “restaurants”라는 최상위 collection의 음식점을 최대 50개 검색하는 쿼리를 만듭니다. 선언된 쿼리는 getDocumentsInQuery()라는 데이터 로딩 및 렌더링을 담당하는 메서드에 전달됩니다. 이것은 스냅샷 리스너를 추가하여 수행합니다. 다음 코드를 FriendlyEats.prototype.getDocumentsInQuery() 메소드에 추가하세요:

FriendlyEats.Data.js

query.onSnapshot(function(snapshot) {
    if (!snapshot.size) return render();

    snapshot.docChanges.forEach(function(change) {
        if (change.type === 'addes') {
            render(change.doc);
        }
    });
});

위 코드의 query.onSnapshot은 쿼리 결과가 변경될때마다 콜백을 트리거 합니다. 처음에는 콜백이 Firestore의 전체 restaurants collection과 함께 모든 쿼리의 결과 셋으로 트리거 됩니다. 그런 다음 모든 개별 documents를 render 함수에 전달합니다. change.type도 removed나 changed 할 수 있으므로 새 document를 추가하는 경우에만 렌더링에 관심있음을 명시적으로 말해야 합니다.

이제 두가지 메소드를 모두 구현했으니 앱을 새로고침하고 콘솔에서 본 음식점이 앱에 보여지는지 확인합니다. 이 섹션을 성공적으로 완료했다면 앱이 Cloud Firestore로부터 데이터를 읽고 씁니다!

음식점 목록이 변경되면 리스너는 자동으로 계속 업데이트 됩니다. Firebase Console에서 음식점을 수동으로 추가해보세요 – 사이트에 즉시 적용되는걸 확인할 수 있습니다!

Note: Query.get() 메소드를 사용하면 실시간 업데이트를 위해 리스닝을 하지않고 Firestore로부터 문서를 한번 가져오는것도 가능합니다.

FriendlyEats Lists

7. Get()’ing data

지금까지 onSnapshot을 사용해 실시간으로 업데이트를 반영하는것을 보여줬지만 항상 그걸 원하는건 아닙니다. 때로는 데이터를 한번만 가져오는게 더 합리적입니다.

특정 음식점을 클릭할때 사용되는 FriendlyEats.prototype.getRestaurant() 메소드를 구현해봅시다.

FrieldlyEats.Data.js
FriendlyEats.prototype.getRestaurant = function(id) {
    return firebase.firestore().collection('restaurants').doc(id).get();
};

이 기능을 구현하면 개별 위치에 따른 페이지를 보고 주 목록에 반영될 리뷰를 남길 수 있습니다.

8. Sorting and Filtering data

현재 우리 앱은 음식점의 목록을 표시하긴 하지만 사용자의 필요에 따른 필터링을 할 수 있는 방법이 없습니다. 이 섹션에서는 Firestore의 고급 쿼리를 사용해 필터링을 해보겠습니다.

다음은 모든 딤섬 음식점을 가져오는 간단한 쿼리입니다:

var filteredQuery = query.where('category', '==', 'Dim Sum')

이름을 보면 알수 있듯 where() 메소드는 우리가 설정한 제한을 충족시카는 collection의 멤버만 다운로드하도록 쿼리를 만듭니다. 이 예에서는 category가 “Dim Sum”인 음식점들만 다운로드 할것입니다.

이 앱에서 사용자는 “샌프란시스코에 있는 피자” 또는 “로스엔젤리스에 있는 해산물을 유명세에 따라”와 같은 특정 쿼리를 만들기 위해 여러개의 필터를 연결할 수 있습니다.

FriendlyEats.prototype.getFilteredRestaurants() 메소드를 살펴보세요. 이 메소드에서 우리는 여러 기준에 따른 음식점을 필터링하는 쿼리를 작성합니다.메소드에 다음 코드를 추가하세요.

FriendlyEats.Data.js
var query = firebase.firestore().collection('restaurants');

if (filters.category !== 'Any') {
    query = query.where('category', '==', filters.category);
}

if (filters.city !== 'Any') {
    query = query.where('city', '==', filters.city);
}

if (filters.price !== 'Any') {
    query = query.where('price', '==', filters.price.length);
}

if (filters.sort === 'Rating') {
    query = query.orderBy('avgRating', 'desc');
} else if (filters.sort === 'Reviews') {
    query = query.orderBy('numRatings', 'desc');
}

this.getDocumentsInQuery(query, render);

위 스니펫은 여러가지 where 필터를 추가하고 한개의 orderBy 절은 사용자 입력을 기반으로 복합(compound)쿼리를 작성합니다. 이제 우리의 쿼리는 사용자가 필요로 하는 음식점만을 반환합니다.

프로젝트를 실행하고 가격, 도시와 카테고리별로 필터링 할 수 있는지 확인하세요. 테스팅시 다음과 같은 오류메시지를 logs를 통해 보게 될겁니다.

The query requires an index. You can create it here: https://console.firebase.google.com/project/.../database/firestore/indexes?create_index=...

Firestore는 대부분의 복합쿼리에 인덱스가 필요하기 때문에 나타나는것입니다. 쿼리에 인덱스를 사용하게 되면 Firestore를 빠르게 확장할 수 있습니다. 에러메시지에 나타난 링크를 열면 올바른 매개변수가 채워진 Firebase 콘솔의 UI가 자동으로 열립니다. Firestore의 인덱스에 대해 더 알고 싶다면 이 문서를 참고하세요.

  1. Deploying Indexes

매번 앱의 여러 가능성을 찾아 생성된 링크를 따라 가려는게 아니라면 firebase 명령을 이용해서 여러 색인을 한번에 쉽게 배포할 수 있습니다.

이것은 미리 준비된 파일 중 firestore.indexes.json에서 찾을 수 있습니다.

firestore.indexes.json
{
    "indexes": [
        {
            "collectionId": "restaurants",
            "fields": [
                { "fieldPath": "city", "mode": "ASCENDING" },
                { "fieldPath": "avgRating", "mode": "DESCENDING" }
            ]
        },
        ...
    ]
}

이 파일은 가능한 모든 필터 조합에 필요한 색인을 기술하고 있습니다.

다음과 같이 이 색인을 배포하세요:

firebase deploy --only firestore:indexes

몇분이 지나면 인덱스는 활성화되고 경고 메시지는 나타나지 않을것입니다.

현재 HTTP 500오류가 나타나는데 다른 PC에서도 같은지 알아봐야겠습니다.

10. Writing data in a transaction

이 섹션에서는 사용자가 식당에 리뷰를 쓸수있는 기능을 추가할것입니다. 지금까지 우리가 쓴 작성한것은 기초적이며 비교적 간단합니다. 이 중 하나라도 오류가 발생하면 사용자에게 다시 시도하게 하거나 자동으로 다시 시도하게끔 프롬프트를 보여줄것입니다.

식당에 평가를 추가하려면 여러가지 읽기와 쓰기를 조정해야 합니다. 먼저 리뷰가 제출되면 음식점의 평점 및 평균 평점을 업데이트 해야 합니다. 이들 중 하나가 실패하지만 다른것들은 그렇지 않다면 우리의 데이터베이스의 한 부분은 다른 데이터와 일치하지 않는 일관성없는 상태로 남아있게 될것입니다.

다행히 Firestore는 우리로 하여금 동시 읽기 및 쓰기를 하나의 기초적인 작업으로 처리해 데이터의 일관성을 유지할수 있게 트랜젝션 기능을 제공합니다.

다음 코드를 FriendlyEats.prototype.addRating() 메소드에 추가하세요.

FriendlyEats.Data.js
var collection = firebase.firestore().collection('restaurants');
var document = collection.doc(restaurantID);

return document.collection('ratings').add(rating).then(function() {
    return firebase.firestore().runTransaction(function(transaction) {
        return transaction(get(document).then(function(doc) {
            var data = doc.data();

            var newAverage = 
                (data.numRatings * data.avgRating + rating.rating) / (data.numRatings + 1);

            return transaction.update(document, {
                numRatings: data.numRatings + 1,
                avgRating: newAverage
            });
        });
    });
});

이 블록에선 먼저 리뷰를 음식점의 하위 collection ratings에 추가합니다. 이 쓰기가 성공하면 음식점의 averageRating과 ratingCount의 수치값을 업데이트하는 트랜잭션을 트리거합니다.

Note: 서버에서 트랜잭션이 실패할때 콜백 또한 반복해서 다시 실행됩니다. 절대 트랜잭션 콜백내에 앱의 상태를 수정하는 로직을 배치하지 마세요.

11. Conclusion

이 코드랩에서 당신은 Firestore의 기본 및 고급 읽기, 쓰기 기능과 보안 규칙을 사용하여 데이터 엑세스를 보호하는 방법을 배웠습니다. 전체 솔루션은 quickstarts-js 저장소에서 찾을 수 있습니다.

Firestore에 대해 좀더 알고 싶다면 다음 자료들을 찾아보세요:


계속 공부하며 작성하고 있습니다. 최종 수정일은 2018년 6월 11일 입니다.

Your First Progressive Web App 공부

Your First Progressive Web App

위 링크의 글을 번역해가며 공부하고 있습니다. 내용이 정확하지 않을수도 있습니다.

1. Introduction

Progressive Web Apps은 최고의 웹과 최고의 앱을 결합하여 사용하는 경험을 할수 있습니다. 브라우저 탭을 통해 처음 방문한 사용자에게 유용하며 설치가 필요하지 않습니다. 사용자가 계속해서 시간을 두고 앱과 관계를 형성함에 따라 PWA는 점점 더 강력해 집니다. 신뢰하기 힘든 네트워크에서도 빠르게 로드되어 관련 푸시알림을 보내고 홈화면에 아이콘이 있으며 최상위 수준으로 로드되며 전체화면을 사용할수 있습니다.

What is a Progressive Web App?

Progressive Web App은:

  • Progressive – 브라우저 선택에 상관없이 점진적인 향상을 핵심으로 제작되어 모든 사용자가 이용할 수 있습니다.
  • Responsive – 어떤 폼팩터와도 맞습니다. 데스크탑, 모바일, 태블릿 혹은 그 어떤것과도.
  • Connectivity independent – 서비스워커(service worker)가 오프라인이나 저품질의 네트워크에서도 작업할 수 있도록 향상되었습니다.
  • App-like – 앱과 같이 느껴집니다, because the app shell model separates the application functionality from application content.
  • Fresh – 서비스워커의 업데이트 프로세스 덕에 항상 최신 정보로 유지됩니다.
  • Safe – 스누핑을 방지하고 콘텐츠가 변조되지 않았는지 확인하기 위해 https를 통해 제공됩니다.
  • Discoverable – W3C manifest와 service worker registration scope덕에 “응용프로그램”으로 식별되어 검색엔진에서 찾을 수 있습니다.
  • Re-engageable – 푸시 알림과 같은 기능을 통해 다시 돌아오도록 할 수 있습니다.
  • Installable – 앱스토어의 번거로움 없이 가장 편하게 사용하는 홈화면에 앱을 추가할 수 있습니다.
  • Linkable – URL을 통해 쉽게 애플리케이션을 공유할 수 있으며 복잡한 설치가 필요없습니다.

What you will build

여기서 당신은 기상정보앱을 PWA 기술을 이용하여 만들것입니다. 이 앱은:

  • PWA의 위 원칙을 사용하고 시연합니다.
  • 실시간 기상정보를 이용합니다.
  • 사용자가 도시를 추가할 수 있도록하여 앱과 같은 상호작용을 제공합니다.

What you’ll learn

  • “app shell” 메소드를 이용하여 앱을 디자인하고 제작하는 방법
  • 앱이 오프라인에서 동작하도록 하는 방법
  • 나중에 오프라인에서 사용이 가능하도록 데이타를 보관하는 방법

What you’ll need

  • 최근버전의 크롬(Chrome). 참고로 다른 브라우저에서도 작동하지만 Chrome DevTools의 몇가지 기능을 이용하여 브라우저레벨에서 어떤일이 일어나는지를 보다 잘 이해할 수 있습니다.
  • Web Server for Chrome이나 당신이 보유한 웹서버를 이용할수도 있습니다.
  • 예제 코드
  • A text editor(VSCode or notepad….)
  • HTML, CSS, JavaScript와 Chrome DeveTools에 대한 기초적인 지식

2.Getting set up

Download the Code

여기에서 이용할 코드를 아래 다운로드 링크를 통해 받으세요.

Download source code

다운로드한 zip파일의 압축을 푸세요. 이렇게 하면 여기에서 이용할 각 단계마다 하나의 폴더와 필요한 모든 리소스가 들어있는 루트폴더(your-first-pwapp-master)의 압축이 풀립니다.

step-NN 폴더에는 각 단계에서 만들어낼 최종본이 포함되어 있습니다. 그것들을 참고하도록 하고 우리는 work라는 디렉토리에서 모든 코딩작업을 할 것입니다.

Install and verify web server

자유롭게 사용할 수 있는 자신만의 웹서버를 이용할 수도 있지만 여기서는 Chrome Web Server와 잘 작동하도록 되어 있습니다. 앱을 설치하지 않았다면 Chrome Web Store를 통해 설치할 수 있습니다.

Install Web Server for Chrome

설치 후 북마크바에서 앱 바로가기를 클릭해 들어가 웹서버 아이콘을 클릭합니다. (노란 원에 200 OK! 아이콘)

대화화면이 나타나면 로컬 웹서버를 설정합니다.

CHOOSE FOLDER 버튼을 눌러 work 폴더를 선택합니다. 이제 웹 서버 대화상자에서 강조 표시된 URL을 통해 진행중인 작업을 볼수 있습니다.

옵션 아래쪽 “Automatically show index.html” 박스를 체크해 선택하세요.

이후 “Web Server: STARTED”라고 표시된 토글을 왼쪽으로, 다시 오른쪽으로 슬라이드 해서 서버를 중지했다가 다시 시작하세요.

이제 작업 사이트를 웹 브라우저를 통해 방문(URL을 클릭해도 됩니다)하면 다음과 같은 페이지를 볼 수 있습니다.(Weather PWA 표시줄 아래 흰화면, 작업진행중 표시 반복)

이 앱은 아직 흘미로운게 없습니다. 웹서버 기능을 확인하기 위해 사용하는 스피너가 돌고 있는 최소한의 골격에 불과합니다. 이후 단계에서 기능과 UI등을 추가할것입니다.

3. Architect your App Shell

What is the app shell?

앱(App)의 shell은 PWA의 사용자 인터페이스를 강화하는데 필요한 최소한의 HTML, CSS 및 JavaScript로 된 안정적으로 우수한 성능을 보장하는 하나의 컴퍼넌트입니다. 첫번째 로드는 매우 신속하고 즉시 캐시되어야 합니다. “캐시된다”는건 shell 파일들이 네트워크를 통해 한번 로드된 후 로컬 장치에 저장하는것을 말합니다. 이후 사용자가 앱을 열때마다 캐시된 로컬 장치에서 shell 파일이 로드되므로 시작시 상당히 빠르게 결과를 보여줍니다.

App shell 아키텍처는 코어 어플리케이션 인프라와 UI를 데이터와 분리합니다. 모든 UI와 인프라는 service worker를 통해 내부에 캐시되므로 이후 로드시 PWA는 모든 데이터를 로드하지 않고 필요한 데이터만 받아옵니다.

service worker는 브라우저가 웹페이지와 별도로 백그라운드에서 실행되는 스크립트로 웹페이지나 사용자 상호작용이 필요없는 기능으로 이끌어줍니다.

즉 app shell은 Native 앱을 만들때 앱스토어에 게시할 코드묶음과 비슷합니다. 앱을 다운로드하는데 필요한 핵심 구성요소지만 데이터가 포함되어 있지 않은것 같습니다.

Why use the App Shell architecture?

앱 쉘 아키텍처를 사용하면 속도에 중점을 둘 수 있습니다. PWA을 Native앱과 유사한 속성인 instant 로드 및 일반 업데이트를 앱스토어의 도움을 받지않고 할 수 있습니다.

Design the App Shell

첫번째 단계는 디자인을 코어 콤포넌트로 분할하는 것입니다.

  • 화면에 즉시 나타나야 할것은?
  • 어떤 UI가 앱의 핵심 요소인가?
  • 앱 쉘에 필요한 리소스는? 예로 이미지, 자바스크립트, 스타일 등

첫번째 PWA앱으로 우리는 기상정보 앱을 만들것입니다. 핵심요소는 다음과 같습니다.

  • 타이틀과 추가/리프레쉬 버튼을 가진 헤더
  • 기상정보 카드용 컨테이너
  • 기상정보 카드 템플리트
  • 새로운 도시를 추가하기 위한 대화상자
  • 로딩 인디케이터

When designing a more complex app, content that isn’t needed for the initial load can be requested later and then cached for future use. For example, we could defer the loading of the New City dialog until after we’ve rendered the first run experience and have some idle cycles available.

4. Implement your App Shell

Create the HTML for the App Shell

이제 3.Architect the App Shell에서 다룬 코어 콤포넌트를 추가합니다.

핵심요소들을 잘 기억해 보세요.

index.html 파일은 work 디렉토리에 이미 존재합니다. (원글에 샘플 형태로 이 내용을 요약해 보여주고 있습니다)

기본적으로 로더를 표시합니다. 이렇게 하면 페이지가 로드될 때 사용자가 로더를 보고 내용이 로드되고 있음을 바로 명확하게 알 수 있습니다.

시간을 절약하기 위해 스타일 시트도 이미 만들어 놓았습니다.

Check out the key JavaScript app code

이제 대부분의 UI가 준비되었으니 모든것이 작동되도록 코드를 연결합니다. 앱쉘과 마찬가지로 핵심요소와 나중에 로드될것들에 어떤 코드가 필요할지 고려해야합니다.

work 디렉토리엔 이미 app code(scripts/app.js)가 포함되어 있습니다. 그 내용은

  • 앱에 필요한 핵심정보를 포함하고 있는 app object
  • 헤더의 모든 버튼(add/refresh) 및 도시 추가 대화상자(add/cancel)의 event listeners
  • 기상정보 카드(app.updateForecastCard)의 추가, 업데이트 method
  • Firebase 공공 기상정보 API를 통해 최신 기상정보 데이터를 받아오는 method (app.getForecast)
  • app.getForecast를 통해 최신 기상정보를 받아와 현재 카드에 반복적용하는 method (app.updateForecasts)
  • 어떻게 렌더링되는지를 빠르게 테스트하기 위한 가상 정보 (initialWeatherForecast)

Test it out

이제 HTML, style 및 JavaScript와 같은 핵심이 준비되었으니 앱을 테스트할 시간입니다.

가상 기상정보가 어떻게 덴더링 되는지를 보기위해 index.html파일의 끝에 있는 다음줄에서 주석을 제거합니다.

다음으로 app.js파일의 끝에 있는 다음줄의 주석을 제거합니다.

// app.updateForecastCard(initialWeatherForecast);

앱을 리로드하면 멋지게 형식화된 기상정보 카드가 spinner가 비활성화되어 나타납니다. (날짜에서 알수 있듯 가상정보입니다)

5. Start with a fast first load

PWA는 빠르게 시작되고 즉시 사용가능해야 합니다. 현 상태의 우리 기상정보앱은 빠르게 시작되지만 유용하지 않습니다. 데이타가 없습니다. 그 데이터를 얻기위해 AJAX 요청을 할 수 있지만 추가 요청이 발생하여 초기 로드가 길어집니다. 대신 처음 로드시 실제 데이타를 제공합니다.

Inject the weather forecast data

이 코드랩에서 우리는 위치를 직접 자바스크립트에 주입해 시뮬레이션을 해 제공하지만 실제 앱이 공급될때는 IP주소에 따른 사용자의 지리적 위치를 기준으로 최신 기상정보 데이터를 제공해야 합니다.

코드에는 이미 삽입할 데이터가 포함되어 있습니다. 이전에 사용한 initialWeatherForecast입니다.

Differentiating the first run

그러나 언제 이 정보를 보여줘야 할지 어떻게 알수 있을까요? 기상정보앱이 캐시로부터 미래에 로드될때 관련이 없을 수도 있습니다. 사용자가 다음 방문때 앱을 로드하면 다른도시로 바뀌었을수도 있습니다. 따라서 반드시 처음에 보여준 도시가 아닌 현재 도시의 정보를 로드해야 합니다.

사용자가 소속된 도시목록과 같은 사용자 설정은 IndexedDB나 다른 빠른 저장 메커니즘을 이용하여 로컬에 저장해야 합니다. 여기에서는 가능한 단순화하기 위해 localStorage를 이용하지만 일부 장치에서 잠재적으로 매우 느릴수 있는 블로킹 동기식 저장 매커니즘이기 때문에 실제 적용될 앱에서 이상적이진 않습니다.

먼저 사용자 환경 설정을 저장하는데 필요한 코드를 추가합니다. 코드에서 다음 TODO 주석을 찾으세요.

// TODO add saveSelectedCities function here

그 주석줄 아래 다음 코드를 추가하세요.

// Save list of cities to localStorage.
app.saveSelectedCities = function() {
var selectedCities = JSON.stringify(app.selectedCities);
localStorage.selectedCities = selectedCities;
};

자 사용자에게 저장된 도시가 있는지 확인하고 렌더링된 데이터를 사용하는지 확인할 시작코드를 추가합니다. 다음 주석을 찾으세요.

// TODO add startup code here

그 주석줄 아래 다음코드를 추가하세요.

/************************************************************************
*
* Code required to start the app
*
* NOTE: To simplify this codelab, we’ve used localStorage.
* localStorage is a synchronous API and has serious performance
* implications. It should not be used in production applications!
* Instead, check out IDB (https://www.npmjs.com/package/idb) or
* SimpleDB (https://gist.github.com/inexorabletash/c8069c042b734519680c)
************************************************************************/

app.selectedCities = localStorage.selectedCities;
if (app.selectedCities) {
app.selectedCities = JSON.parse(app.selectedCities);
app.selectedCities.forEach(function(city) {
app.getForecast(city.key, city.label);
});
} else {
/* The user is using the app for the first time, or the user has not
* saved any cities, so show the user some fake data. A real app in this
* scenario could guess the user’s location via IP lookup and then inject
* that data into the page.
*/
app.updateForecastCard(initialWeatherForecast);
app.selectedCities = [
{key: initialWeatherForecast.key, label: initialWeatherForecast.label}
];
app.saveSelectedCities();
}

시작 코드는 로컬 저장소에 저장된 도시가 있는지 확인합니다. 있다면 저장소 데이터를 구문분석해 저장된 각 도시에 대한 기상정보카드를 표시합니다. 없다면 시작코드가 가상 기상정보데이터를 사용하여 이를 기본 도시로 저장합니다.

Save the selected cities

최종적으로 ‘도시 추가’버튼 처리기를 수정해 선택한 도시를 로컬 저장소에 저장해야 합니다. butAddCity 클릭 핸들러를 업데이트해 다음 코드와 일치하도록 합니다.

document.getElementById(‘butAddCity’).addEventListener(‘click’, function() {
// Add the newly selected city
var select = document.getElementById(‘selectCityToAdd’);
var selected = select.options[select.selectedIndex];
var key = selected.value;
var label = selected.textContent;
if (!app.selectedCities) {
app.selectedCities = [];
}
app.getForecast(key, label);
app.selectedCities.push({key: key, label: label});
app.saveSelectedCities();
app.toggleAddDialog(false);
});

The new additions are the initialization of app.selectedCities if it doesn’t exist, and the calls to app.selectedCities.push() and app.saveSelectedCities().

6. Use service workers to pre-cache the App Shell

PWA는 빠르고 설치가능해야 합니다. 즉, 온라인, 오프라인, 간헐적으로 느린 연결상태에서도 작동해야 합니다. 이를 위해 service worker를 사용해 app shell을 캐시해야 하며 그 결과로 항상 신속하고 안정적으로 사용할 수 있습니다. service worker에 익숙하지 않다면 Introduction To Service Workers 이 글에서 하늘일과 수명주기, 작동방법등에 대한 기본을 얻을 수 있습니다. 이 코드 실습을 완료한 후 Debugging Service Workers code lab 이곳을 통해 service worker가 작동되는 방식에 대한 심화된 학습을 확인하세요.

service worker를 통해 제공되는 기능은 점진적으로 향상되어야 하며 브라우저에서 지원하는 경우에만 추가되어야 합니다. 예를 들어 service worker를 통해 app shell 및 데이터를 캐시할 수 있으므로 네트워크가 없는 경우에도 사용할 수 있습니다. service worker가 지원되지 않으면 오프라인코드는 호출되지 않고 사용자는 기본적인 경험만 얻게 됩니다. 기능 감지를 사용한 점진적 향상을 제공하면 오버헤드가 거의 없으며 해당 기능을 지원하지 않는 구형 부라우저에서도 중단되지 않을것입니다.

Remember: Service worker functionality is only available on pages that are accessed via HTTPS (http://localhost and equivalents will also work, to facilitate testing). To learn about the rationale behind this restriction check out Prefer Secure Origins For Powerful New Features from the Chromium team.

Register the service worker if it’s available

우선, 오프라인에서 앱 동작을 만들려면 웹페이지를 열거나 사용자와의 상호작용없이 백그라운드 기능을 허용하는 스크립트를 service worker로 등록하는것입니다.

이것은 간단한 두작업으로 합니다.

  1. 브라우저에게 JavaScript 파일을 service worker로 등록하도록 지시
  2. service worker를 담은 JavaScript를 만듦

우선, 브라우저가 service worker를 지원하는지를 체크하고 지원한다면 service worker를 지시합니다. 다음 코드를 app.js에 추가하세요.(// TODO add service worker code hear 주석줄 아래)

if (‘serviceWorker’ in navigator) {
navigator.serviceWorker
.register(‘./service-worker.js’)
.then(function() { console.log(‘Service Worker Registered’); });
}

Cache the site assets

서비스워커(service worker)가 등록되면 사용자가 처음 페이지에 방문했을때 설치 이벤트가 트리거됩니다. 이 이벤트 핸들러는 앱에 필요한 모든 요소(assets)를 캐시 합니다.

아래의 코드는 실 사용시에 활용되어서는 안됩니다. 이것은 가장 기본적인 사례만 다루며 앱쉘이 업데이트 되지 않는 상태가 되기 쉽습니다. 이 구현에서의 함정들과 피하는 방법을 아래 섹션을 통해 알아보세요.

서비스워커의 동작이 만료되면 caches를 열고 App Shell을 로드하는데 필요로 하는 assets으로 채워야 합니다. 앱의 루트 폴더(여기에서는 your-first-pwapp-master/work)에 service-worker.js라는 파일을 만듭니다. 서비스 워커의 범위는 파일이 상주하는 디렉토리에 의해 정의되기 때문에 이 파일은 앱의 루트에 존재해야 합니다. 다음 코드를 새 service-worker.js 파일에 추가하세요.

var cacheName = ‘weatherPWA-step-6-1’;
var filesToCache = [];

self.addEventListener(‘install’, function(e) {
console.log(‘[ServiceWorker] Install’);
e.waitUntil(
caches.open(cacheName).then(function(cache) {
console.log(‘[ServiceWorker] Caching app shell’);
return cache.addAll(filesToCache);
})
);
});

먼저, caches.open()으로 캐시를 열어 캐시 이름을 제공해야 합니다. 캐시 이름을 제공함으로써 파일을 버전화하거나 app shell에서 데이터를 분리할수 있어 다른 파일에는 영향을 미치지 않고 쉽게 업데이트 할수 있습니다.

일단 캐시가 열리면 URL목록을 취한다음 서버에서 가져와 캐시에 응답을 추가하는 cache.addAll()을 호출할수 있습니다. 불행히도 cache.addAll()은 atomic해서 파일 중 하나라도 실패하면 전체 캐시단계가 실패하게 됩니다!

자, 서비스 워커를 이해하고 debug하기 위해 DevTools를 어떻게 이용해야 할지 접근해봅시다. 페이지를 리로드 하기전에 DevTools를 열고 Application 패널위에 있는 Service Worker 패널로 갑니다.

비어있는 페이지가 보인다면 현재 열려진 페이지는 등록된 서비스워커가 없다는걸 의미합니다.

이제 페이지를 리로드합니다. 서비스워커 판넬이 다음과 같이 보여집니다.

OK, now we’re are going to take a brief detour and demonstrate a gotcha that you may encounter when developing service workers. 시연을 위해 service-worker.js파일의 install 이벤트리스너 아래에 activate 이벤트 리스너를 추가합니다.

self.addEventListener(‘activate’, function(e) {
console.log(‘[ServiceWorker] Activate’);
});

activate 이벤트는 서비스워커가 시작될때 해고됩니다.

DevTools Console을 연후 페이지를 리로드하고 Application 패널의 Service Worker 창으로 전환한 다음 활성화된 서비스워커를 살펴보세요. [ServiceWorker] Activate 메시지가 console에 나타나지만 아무일도 발생하지 않습니다. 서비스워커 창을 확인하면 새로운 서비스워커(that includes the activate event listener)가 “waiting” 상태로 보여집니다.

기본적으로 오래된 서비스워커는 페이지에 대한 탭이 열려있는 한 계속 페이지를 제어합니다. 따라서 페이지를 닫고 다시 열거나 skipWaiting을 클릭하세요. 그러나 장기적인 해결책은 DevTools의 ServiceWorker 창에서 Update on reload 항목을 활성화 하는것입니다. 이 항목을 선택하면 페이지가 다리 로드될때마다 서비스워커가 강제로 업데이트 됩니다.

DevTools의 서비스워커를 검사하고 디버깅하기 위한 준비가 끝났습니다. 다른 트릭들을 후에 알려드리겠습니다. 앱제작으로 돌아갑니다.

캐시를 업데이트하기 위한 약간의 로직을 포함하여 activate 이벤트 리스너를 확장합시다.

self.addEventListener(‘activate’, function(e) {
console.log(‘[ServiceWorker] Activate’);
e.waitUntil(
caches.keys().then(function(keyList) {
return Promise.all(keyList.map(function(key) {
if (key !== cacheName) {
console.log(‘[ServiceWorker] Removing old cache’, key);
return caches.delete(key);
}
}));
})
);
return self.clients.claim();
});

이 코드는 app shell 파일이 변경될때마다 서비스워커가 캐시를 업데이트하도록 보증합니다. 이 작업을 수행하려면 서비스워커 파일의 맨 위에 있는 cacheName 변수를 증가시켜야 합니다.

The last statement fixes a corner-case which you can read about in the (optional) information box below.

앱이 완료되면 self.clients.claim()은 앱이 최신 데이터를 반환하지 않는 corner case를 수정합니다. 아래줄에서 설명한대로 하면 corner case사례를 재현할 수 있습니다. 1)초기 뉴욕시 데이터가 표시되도록 앱을 로드합니다. 2) 앱상단의 리프레쉬버튼을 클릭합니다. 3) 접속을 끊습니다. 4) 앱을 리로드합니다. 최근 뉴욕 데이터가 나오길 기대하지만 실제론 기초 자료가 보여집니다. 이 현상은 서비스워커가 아직 활성화 되지 않았기 때문에 발생합니다. self.clients.claim() 를 통해 서비스워커를 더 빠르게 활성화할 수 있습니다.

최종적으로 app shell에서 필요로 하는 파일 목록을 업데이트합니다. 이 배열에는 이미지, 자바스크립트, 스타일시트등 필요한 모든 파일을 포함해야 합니다. service-worker.js파일의 최상단 근처에 var filesToCache = []; 부분을 교체하세요.

var filesToCache = [
‘/’,
‘/index.html’,
‘/scripts/app.js’,
‘/styles/inline.css’,
‘/images/clear.png’,
‘/images/cloudy-scattered-showers.png’,
‘/images/cloudy.png’,
‘/images/fog.png’,
‘/images/ic_add_white_24px.svg’,
‘/images/ic_refresh_white_24px.svg’,
‘/images/partly-cloudy.png’,
‘/images/rain.png’,
‘/images/scattered-showers.png’,
‘/images/sleet.png’,
‘/images/snow.png’,
‘/images/thunderstorm.png’,
‘/images/wind.png’
];

파일 이름의 모든 순열(permutations)을 포함시켜야합니다. 예를 들어 앱은 index.html로 제공되지만, 루트폴더로 요청 될 때 서버가 index.html을 전송하기 때문에 /로 요청할 수도 있습니다. You could deal with this in the fetch method, but it would require special casing which may become complex.

우리의 앱은 아직까지 오프라인에서 동작하지 않습니다. app shell 콤포넌트를 캐시했지만 여전히 로컬 캐시에서 로드해야 합니다.

Serve the app shell from the cache

Service workers provide the ability to intercept requests made from our Progressive Web App and handle them within the service worker. That means we can determine how we want to handle the request and potentially serve our own cached response.

예를들면

self.addEventListener(‘fetch’, function(event) {
// Do something interesting with the fetch here
})

자 이제 캐시로부터 app shell을 제공합니다. service-worker.js파일의 마지막에 다음 코드를 추가하세요.

self.addEventListener(‘fetch’, function(e) {
console.log(‘[ServiceWorker] Fetch’, e.request.url);
e.respondWith(
caches.match(e.request).then(function(response) {
return response || fetch(e.request);
})
);
});

Stepping from inside, out, caches.match() evaluates the web request that triggered the fetch event, and checks to see if it’s available in the cache. It then either responds with the cached version, or uses fetch to get a copy from the network. The response is passed back to the web page with e.respondWith().

If you’re not seeing the [ServiceWorker] logging in the console, be sure you’ve changed the cacheName variable and that you’re inspecting the right service worker by opening the Service Worker pane in the Applications panel and clicking inspect on the running service worker. If that doesn’t work, see the section on Tips for testing live service workers.

Test it out

이제 앱이 오프라인-가능해졌습니다! 시도해봅시다.

페이지를 리로드한 후 DevTools에서 Application 패널의 Cache Storage 창으로 갑니다. Cache Storage에서 마우스 우클릭으로 Refresh Caches를 선택합니다. 섹션을 확장하면 왼쪽에 나열된 app shell 캐시의 이름이 표시됩니다. app shell 캐시를 클릭하면 현재 캐시된 모든 리소스를 볼 수 있습니다.

이제 오프라인 모드를 테스트해봅시다. DevTools의 Service Worker창으로가서 Offline 체크박스를 활성화합니다. 활성화하면 Network 패널탭에 노란색의 경고 아이콘을 볼 수 있을것입니다. 이 표시는 현재 오프라인임을 나타냅니다.

페이지를 리로드하면…. 동작합니다! 적어도 초기(가상) 날씨 데이터를 로드하는것에 유의하세요.

앱이 가상 데이터를 로드하는 이유를 알고 싶다면 app.getForecast()의 else 부분을 확인하세요.

다음단계는 기상정보를 캐시할수 있도록 서비스워커의 로직을 수정하고 앱이 오프라인일때 캐시를 통해 가장 최근 데이타를 반환하는것입니다.

Tip: 새롭고 깔끔하게 시작하고자 한다면 저장된 데이터(localStorage, indexedDB 데이터, 캐시된 파일)와 모든 서비스워커를 Application 탭의 Clear storage 창을 통해 지우세요.

Beware of the edge cases

앞에서 언급했듯 처리되지 않는 많은 경우때문에 이 코드를 실사용에 쓰면 안됩니다.

Cache depends on updating the cache key for every change

For example this caching method requires you to update the cache key every time content is changed, otherwise, the cache will not be updated, and the old content will be served. So be sure to change the cache key with every change as you’re working on your project!

Requires everything to be redownloaded for every change

Another downside is that the entire cache is invalidated and needs to be re-downloaded every time a file changes. That means fixing a simple single character spelling mistake will invalidate the cache and require everything to be downloaded again. Not exactly efficient.

Browser cache may prevent the service worker cache from updating

There’s another important caveat here. It’s crucial that the HTTPS request made during the install handler goes directly to the network and doesn’t return a response from the browser’s cache. Otherwise the browser may return the old, cached version, resulting in the service worker cache never actually updating!

Beware of cache-first strategies in production

Our app uses a cache-first strategy, which results in a copy of any cached content being returned without consulting the network. While a cache-first strategy is easy to implement, it can cause challenges in the future. Once the copy of the host page and service worker registration is cached, it can be extremely difficult to change the configuration of the service worker (since the configuration depends on where it was defined), and you could find yourself deploying sites that are extremely difficult to update!

How do I avoid these edge cases?

So how do we avoid these edge cases? Use a library like sw-precache, which provides fine control over what gets expired, ensures requests go directly to the network and handles all of the hard work for you.

7. Use service workers to cache the forecast data

데이터에 맞는 캐싱 전략을 선택하는것이 중요하며 앱에서 제공하는 데이터 유형에 따라 다릅니다. 예를 들어 날씨나 주식과 같은 시간에 민감한 데이터는 최대한 신선해야 하며 아마타 이미지나 기사 컨텐츠는 업데이트 빈도가 낮습니다.

cache-first-then-network 전략은 앱에 이상적입니다. 가능한 한 빨리 가져온 데이터를 화면에 표시하고 네트워크로부터 최신 데이터를 받으면 업데이트를 합니다.
network-first-then-cache와 비교하면 사용자는 데이터를 캐시하기 위해 fetch하는 시간을 기다릴 필요가 없습니다.

Cache-first-then-network는 두개의 비동기식 요청(캐시와 네트워크 중 하나)을 시작해야 함을 의미합니다. 앱의 네트워크 요청은 많이 변경할 필요가 없지만 응답을 보내기 전에 캐시하도록 서비스워커(service worker)를 수정해야 합니다.

정상적인 상황에서는 사용가능한 최근 데이터와 캐시된 데이터가 거의 즉시 제공되어 앱에 사용됩니다. 그런 다음 네트워크 요청이 반환되면 네트워크의 최신 데이터를 사용하여 앱이 업데이트 됩니다.

Intercept the network request and cache the response

날씨 API에 대한 요청을 가로채서 그 값을 캐시에 저장해 나중에 쉽게 접근 가능하도록 서비스워커를 수정해야 합니다. cache-then-network 전략에서 우리는 네트워크 응답이 항상 최신 정보를 제공하는 ‘source of truth’라고 믿어야 하기 때문입니다. 그렇지 못하다면 우린 이미 앱에 최종 캐시된 데이터를 제공했으므로 실패한것으로 봐야합니다.

app shell에서 우리의 앱 데이터를 분리하기 위해 서비스워커에 dataCacheName을 추가합시다. app shell이 업데이트되고 오래된 캐시가 삭제될때 우리의 데이터는 변경할 필요가 없이 초고속으로 로드가 가능합니다. 나중에 데이터 형식이 변경된다면 app shell을 유지하고 내용의 동기화를 이어나갈 방법을 찾아야 합니다.(조금 이해를 잘못한듯 하기도….)

service-worker.js파일의 상단에 다음줄을 추가합니다.

var dataCacheName = ‘weatherData-v1’;

그런 다음 app shell 캐시를 정리할때 데이터 캐시가 삭제되지 않도록 activate 이벤트 핸들러를 업데이트 합니다.

if (key !== cacheName && key !== dataCacheName) {

마지막으로 다른 요청과 별도로 데어터 API를 처리할수 있도록 fetch 이벤트 핸들러를 수정합니다.

self.addEventListener(‘fetch’, function(e) {
console.log(‘[Service Worker] Fetch’, e.request.url);
var dataUrl = ‘https://query.yahooapis.com/v1/public/yql’;
if (e.request.url.indexOf(dataUrl) > -1) {
e.respondWith(
caches.open(dataCacheName).then(function(cache) {
return fetch(e.request).then(function(response) {
cache.put(e.request.url, response.clone());
return response;
});
})
);
} else {
e.respondWith(
caches.match(e.request).then(function(response) {
return response || fetch(e.request);
})
);
}
});

이 코드는 요청을 가로채서 URL이 weather API의 주소로 시작하는지를 확인합니다. 확인되었다면 요청을 만들기 위해 fetch를 실행합니다. 응답이 반환되면 코드는 캐시를 열고 응답을 복제한다음 캐시에 저장하고 마지막으로 원래 요청자에게 응답을 보냅니다.

우리의 앱은 아직 오프라인에서 잘 작동하지 않습니다. app shell에 대한 캐싱 및 검색을 구현했지만 데이터를 캐싱했음에도 불구하고 앱은 어떤 날씨 정보가 있는지 확인하기 위해 캐시를 체크하고 있지 않습니다.

Making the requests

앞에서 언급했듯 앱은 캐시로 하나 네트워크로 하나씩 비동기식 요청을 필요로 합니다. 앱은 window에 있는 caches 오브젝트(객체)를 사용하여 캐시에 접근하고 최신 데이타를 받아옵니다. 이것은 caches 오브젝트(객체)가 모든 브라우저에서 사용 가능하지 않을수 있고 네트워크 요청이 여전히 작동되지 않는 경우의 점진적 향상(PW)을 보여주는 훌륭한 예입니다.

이것을 위해서 우리는 다음과 같이 합니다:

  1. 전역 window 오브젝트(객체)에 caches 오브젝트가 존재하는지 체크합니다.
  2. 캐시로부터 데이터를 요청합니다.

* 서버 요청이 아직 해결되지 않았다면 캐시된 데이터로 앱을 업데이트 합니다.
3. 서버로부터 데이터를 요청합니다.
* 추후 빠르게 접근할수 있도록 데이터를 저장합니다.
* 서버로부터 받은 최신 정보를 앱에 업데이트 합니다.

Get data from the cache

이제 caches 오브젝트가 있는지 확인하고 최신 데이터를 요청합니다. app.getForecast()에 있는 TODO add chche login here 주석을 찾아 그 아래에 다음 코드를 추가합니다.

if (‘caches’ in window) {
caches.match(url).then(function(response) {
if (response) {
response.json().then(function updateFromCache(json) {
var results = json.query.results;
results.key = key;
results.label = label;
results.created = json.query.created;
app.updateForecastCard(results);
});
}
});
}

우리 날씨 앱은 이제 하나는 cache에서 다른하나는 XHR을 통해 두개의 비동기 요청을 만듭니다. XHR이 미해결상태인경우에 캐시에 데이타가 있다면 그것을 반환받아 매우 빠르게(수십밀리초) 렌더링되어 카드가 업데이트 됩니다. 그런 다음 XHR이 응답하면 날씨 API에서 직접 전달된 최신 데이터로 카드가 업데이트 됩니다.

캐시 요청과 XHR요청 모두 기상정보 카드를 업데이트하는 호출을 하며 끝난다는것에 주목하세요. 앱이 최신 데이터를 표시하는지 어떻게 알수 있을까요? app.updateForecastCard의 다음 코드를 통해 제어됩니다.

var cardLastUpdatedElem = card.querySelector(‘.card-last-updated’);
var cardLastUpdated = cardLastUpdatedElem.textContent;
if (cardLastUpdated) {
cardLastUpdated = new Date(cardLastUpdated);
// Bail if the card has more recent data then the data
if (dataLastUpdated.getTime() < cardLastUpdated.getTime()) {
return;
}
}

카드가 업데이트 될때마다 앱은 카드의 숨겨진 속성에 데이터의 타입스탬프를 저장합니다. The app just bails if the timestamp that already exists on the card is newer than the data that was passed to the function.

Test it out

이제 앱은 완전히 오프라인-사용가능해졌습니다. 몇개의 도시를 저장하고 새 기상정보를 얻기 위해 앱의 리프레쉬버튼을 누르신 후 오프라인으로 만든 후 페이지를 다시 로드해보세요.

그러고 DevTools를 열어 Application 패널의 Cache Storage 창으로 갑니다. 섹션을 확장하면 왼쪽에 app shell 및 데이터 캐시 목록이 표시됩니다. 데이터 캐시를 열려면 각 도시에 대한 데이터를 저장해야 합니다.

8. Support native integration

아무도 모바일 키보드로 긴 URL을 입력하는것을 좋아하지 않고 필요로 하지 않습니다. 홈화면에 추가하기 기능을 사용하면 상점에서 네이티브 앱을 설치하는것처럼 기기에 바로가기 링크를 추가할 수 있지만 충돌이 훨씬 덜합니다.

Web App Install Banners and Add to Homescreen for Chrome on Android

웹 앱 설치배너는 사용자가 신속하고 원활하게 웹 앱을 홈화면에 추가할수 있고 그로인해 쉽게 실행하고 앱으로 다시 찾을 수 있습니다. 앱 설치 배너는 쉽게 추가할 수 있으며 Chrome이 대부분의 어려운 작업을 처리해줍니다. 앱의 세부정보가 담긴 web app manifest 파일만 있으면 됩니다.

Chrome then uses a set of criteria including the use of a service worker, SSL status and visit frequency heuristics to determine when to show the banner. 또한 사용자는 크롬의 “Add to Home Screen” 메뉴 버튼을 통해 직접 추가할 수 있습니다.

Declare an app manifest with a minifest.json file

The web app manifest is a simple JSON file that gives you, the developer, the ability to control how your app appears to the user in the areas that they would expect to see apps (for example the mobile home screen), direct what the user can launch and more importantly how they can launch it.

web app minifest를 사용하면 웹 앱은 다음과 같은것을 할 수 있습니다.

  • 사용자의 안드로이드 홈 화면에서 풍부한 존재감을 가집니다.
  • 안드로이드에서 URL바 없이 전체화면 모드로 실행됩니다.
  • 최적으로 보여주기 위해 화면의 방향을 제어할 수 있습니다.
  • 사이트의 테마색 및 스플래쉬 스크린을 보여주는 것을 정의할 수 있습니다.
  • 홈화면 또는 URL을 통해 실행되었는지를 추적할 수 있습니다.

work 폴더에 manifest.json 파일을 생성하고 다음 내용을 복사/붙여넣기 하세요.

{
“name”: “Weather”,
“short_name”: “Weather”,
“icons”: [{
“src”: “images/icons/icon-128×128.png”,
“sizes”: “128×128”,
“type”: “image/png”
}, {
“src”: “images/icons/icon-144×144.png”,
“sizes”: “144×144”,
“type”: “image/png”
}, {
“src”: “images/icons/icon-152×152.png”,
“sizes”: “152×152”,
“type”: “image/png”
}, {
“src”: “images/icons/icon-192×192.png”,
“sizes”: “192×192”,
“type”: “image/png”
}, {
“src”: “images/icons/icon-256×256.png”,
“sizes”: “256×256”,
“type”: “image/png”
}],
“start_url”: “/index.html”,
“display”: “standalone”,
“background_color”: “#3E4EB8”,
“theme_color”: “#2F3BA2”
}

manifest는 다양한 화면크기를 위한 아이콘들을 지원합니다. 이 글을 쓰는 시점에서 웹 앱 manifest를 지원하는 유일한 브라우저인 크롬과 오페라 모바일은 192px보다 작은것들은 사용하지 않습니다.

앱 시작방법을 추적하는 가장 쉬운 방법은 start_utl 매가변수에 쿼리 문자열을 추가하여 analytics suite를 사용하여 쿼리 문자열을 추적하는것입니다. 이 방법을 사용하는 경우 App Shell에 캐시된 파일을을 업데이트하여 쿼리 문자열이 포함된 파일이 캐시되는지 확인하세요.

Tell the browser about your manifest file

이제 index.html 파일의 요소 맨 아랫줄에 다음 라인을 추가하세요:

Best Practices

  • manifest 링크를 사이트의 모든 페이지에 배치하면 사용자가 처음 방문할때 어떤 페이지를 통해 들어와도 상관없이 Chrome에 의해 적절히 끌어내어집니다.
  • short_name은 Chrome에서 선호되며 네임 필드로 선언될 경우 사용됩니다.
  • 여러 다른 해상도에 맞는 아이콘 세트를 만드세요. Chrome은 48dp와 가장 가까운 아이콘을 사용하려고 할것입니다. 예로 2x 기기에서는 96px, 3x 기기에서는 144px.
  • 스플래시 화면에 적합한 크기의 아이콘을 포함하고 background_color를 설정하는것을 잊지 마세요.

Further Reading:

Using app install banners
링크가 죽어있고 다른곳으로 리다이렉션 됩니다.

Add to Homescreen elements for Safari on iOS

index.html 파일의 요소 끝쪽에 다음 내용을 추가하세요.

   <!-- Add to home screen for Safari on iOS -->
   <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="Weather PWA">
   <link rel="apple-touch-icon" href="images/icons/icon-152x152.png">

Tile Icon for Windows

index.html 파일의 요소 끝쪽에 다음 내용을 추가하세요.

   <meta name="msapplication-TileImage" content="images/icons/icon-144x144.png">
   <meta name="msapplication-TileColor" content="#2F3BA2">

Test it out

이 섹션에서는 web app manifest를 테스트 하는 여러 방법을 보여드립니다.

첫번째 방법은 DevTools를 이용하는것입니다. Application 패널의 Manifest 창을 열어보세요. manifest 정보를 제대로 추가했다면 이 창을 통해 읽기 편한 형태로 파싱되어 보여집니다.

또한 이 패널에서 홈화면에 추가하는 기능을 테스트 할수도 있습니다. Add to homescreen 버튼을 클릭해보세요. 아래 스크린샷에서와 같이 URL바 아래 “Add this site to your shelf”라는 메시지가 표시된걸 볼수 있습니다.

이것은 모바일에서의 홈화면에 추가하기 기능과 같은 데스크탑입니다. 데스크탑에서 이 프롬프트를 성공적으로 실행할 수 있다면 모바일 사용자가 앱을 자신의 기기에 추가할수 있다는것을 보증합니다.

두번째 방법은 Chrome용 웹서버를 이용하는것입니다. 이렇게 하면 로컬 개발 서버(데스크탑이나 랩탑)를 다른 컴퓨터에 노출하고 실제 모바일 장치에서 당신의 PWA에 접근할 수 있습니다.

Opening up a port for remote access is handy for testing this step but may be blocked by your computer’s firewall rules or network administrator. Opening ports for remote access is generally not a good thing to leave running on your computer. So, for security reasons, when you’ve completed testing this step, disable the Accessible on local network option and restart your web server.

크롬용 웹서버 설정창에서 Accessible on local network 옵션을 선택하세요.

웹서버를 STOPPED로 토글하고 다시 STARTED로 되돌리면 원격으로 앱에 접속할때 사용할수 있는 새 URL이 표시됩니다.

이제 모바일 기기에서 새 URL을 통해 사이트에 접속해보세요.

서비스워커가 HTTPS를 통해 제공되지 않기 때문에 이 방법을 테스트 할때 콘솔에 서비스워커 오류가 표시됩니다.

안드로이드기기에서 Chrome을 사용하여 홈화면에 앱을 추가해보고 실행화면이 올바른 아이콘을 사용하는지 확인해보세요.

사파리나 인터넷익스플로러에서는 수동으로 직접 홈화면에 추가할 수 있습니다.\

9. Deploy to a secure host and celebrate

마지막 단계는 우리의 기상앱을 HTTPS를 지원하는 서버에 배포하는것입니다. 아직 서버를 가지고 있지 않다면 가장 쉬운(게다가 무료) 접근은 Firebase의 정적 컨텐츠 호스팅을 이용하는것입니다. 이것은 굉장히 사용하기 쉽고 글로벌 컨텐츠 전송 네트워크(CDN)에 HTTPS를 통해 컨텐츠를 배포할 수 있습니다.

Extra credit: minify and inline CSS

한가지 더 고려해야 할 사항은 Page Speed Insights 컨텐츠를 요청의 처음 15k byte에 가두어 핵심 스타일을 축소하고 index.html에 직접 인라인처리하는것을 권장합니다.

처음으로 요청한 모든것을 인라인으로 얼마나 작게 처리할수 있을지 알고싶다면 PageSpeed Insight Rules 이곳을 참고하세요.

이 단계에서는 시스템에 Node 와 NPM이 설치되어 있어야 합니다. 그렇지 않은 경우 HTTPS를 지원하는 다른 호스팅 업체를 사용할수도 있습니다. 사용자를 HTTP에서 HTTPS로 자동으로 재전송 해주기 때문에 우리는 Firebase를 이용했습니다. 다른 공급자를 사용한다면 HTTPS로 재전송되는지 항상 확인하세요.

Deploy to Firebase

Firebase를 처음 접한다면 먼저 계정을 생성하고 몇가지 툴을 설치해야 합니다.

  1. Firebase 계정을 여기 https://firebase.google.com/console/를 통해 만드세요.
  2. Firebase 툴을 npm을 통해 설치하세요: npm install -g firebase-tools

계정이 만들어졌고 로그인했다면 배포할 준비가 된것입니다!

  1. https://firebase.google.com/console/을 통해 새로운 앱(app)을 생성하세요.
  2. 최근에 Firebase 툴에 로그인한적이 없다면 자격증명을 갱신하세요: firebase login
  3. 앱을 초기화 하고 완성된 앱이 있는 디렉토리(/work 와 같은곳)를 제공하세요: firebase init
  4. 마지막으로 Firebase에 앱을 배포하세요: firebase deploy
  5. 완료를 축하합니다. 앱이 https://YOUR-FIREBASE-APP.firebaseapp.com 도메인을 통해 배포될것입니다.

Firebase Hosting Guide를 참고하세요.

계속 수정해가며 공부하고 있습니다. 최종 수정일은 2018년 6월 9일입니다.

Progressive Web Apps on the Desktop

데스크탑에서의 Progressive Web Apps(PWA)
https://developers.google.com/web/updates/2018/05/dpwa

위 글을 공부삼아, 구글번역기에 의존해서….

데스크탑 PWA는 사용자 디바이스에 네이티브앱과 같이 설치될 수 있고 빠르게 동작합니다. 다른 앱들과 같은 방식으로 동작되기 때문에 내장된 앱과 같이 느껴지고 주소표시줄이나 탭없이 완전한 하나의 윈도우 앱처럼 실행됩니다. 서비스 워커가 실행에 필요한 모든 정보들을 저장할수 있기 때문에 신뢰할 수 있고 사용자에게 즐거운 경험을 만들어 줍니다.

참고 / 필자의 Google I/O 발표
PWAs:building bridges to mobile,desktop,and native
desktop PWAs

Desktop usage is important

모바일은 PWA의 수많은 변화를 이끌어왔습니다. 모바일의 성장세는 매우 굳건하지만 데스크탑에서의 사용량은 여전히 성장하고 있습니다. 휴대전화의 주 사용시간은 아침과 저녁이고 태블릿은 저녁시간대의 사용량이 현저히 높습니다. 데스크탑에서의 이용율은 모바일에 비해 하루에 걸쳐 균등하게 분산됩니다. 대부분의 사람들이 일중이거나 책상에 머무를때 상당한 사용량을 가지고 있습니다.
사용자에게는 Native라는 느낌이 중요하기 때문에 설치되었다는것은 앱이 빠르고 통합적이며 신뢰할수있고 매력적으로 다가갑니다. 데스크탑 PWA는 다른 데스크탑 앱과 같은 위치에서 시작할 수 있지만 앱윈도우(app window)에서 실행되기 때문에 데스크탑의 다른 앱들과 같이 보여집니다.

Getting started

사족: 데스크탑 PWA는 Chrome OS 67에서 사용할 수 있습니다.(2018년 6월 2일 현재 정식버전) 다른 운영체제에서 Chrome의 데스크탑 PWA를 사용하려면 #enable-desktop-pwas 플래그를 사용설정해야 합니다.

Getting started는 이미 당신이 하고 있는것과 다르지 않습니다. 이것은 전혀 새로운 종류의 앱이 아닙니다. 기존 PWA에서 하던 모든 작업이 적용됩니다. Service workers는 빠르고 안정적으로 동작합니다. Web Push and Notifications는 사용자로 하여금 업데이트를 유지하게 해주며 홈화면에 add to home screen prompt로 ‘설치’할 수 있습니다. 유일한 차이점은 브라우저탭에서 실행하는 대신 app window에서 실행된다는것입니다.

Add to home screen

홈화면에 추가하기 위한 기준이 충족되면 크롬은 beforeinstallprompt 이벤트를 발생시킵니다. 이벤트 핸들러는 이벤트를 저장하고 홈화면에 앱을 추가할 수 있음을 사용자에게 알리기 위한 인터페이스를 업데이트합니다. 예를들면 Spotify의 데스크탑 PWA는 사용자 프로필 바로 위에 ‘앱설치’버튼을 추가해논걸 볼수있습니다.
이벤트 처리, UI 업데이트 및 홈화면에 추가 프롬프트 표시하기 위한 방법에 대한 자세한 내용은 Add to Home Screen을 참조하세요.

The app window

탭이나 주소창이 없는 app window를 이용한 당신의 앱입니다. 브라우저 탭에 비해 유연한 창 구성 및 조작으로 앱의 요구를 지원하도록 최적화되어 있습니다. app window를 이용하면 전체화면이나 여러개의 창을 열어 놓는 작업을 쉽게 할 수 있습니다. app window를 이용하면 앱전환기나 alt-tab과 같은 키보드 단축키를 이용해 앱간 전환을 쉽게 할 수 있습니다.
당신이 생각하는대로 app window는 표준 제목표시줄, 최소, 최대화 및 단기 아이콘이 있습니다. Chrome OS에서 제목표시줄은 app manifest에 정의된 theme_color를 기반으로 보여집니다. 그리고 앱은 윈도우의 전체 너비를 차지하도록 디자인(설계)되어야 합니다.
app window에는 앱에 대한 정보를 액세스하거나 URL에 쉽게 접근하고 페이지를 인쇄, 페이지의 확대/축소 혹은 앱을 브라우저에서 열도록 도와주는 app menu(세개의 점으로 된 버튼, 최소화버튼부근)가 있습니다.

Design considerations

데스크탑 PWA를 만들 경우 몇몇 특유의 디자인 고려사항이 있으며 이것은 모바일 PWA에서는 반드시 적용되진 않습니다.
데스크탑 앱은 훨씬 더 큰 화면 영역을 사용합니다. 여분의 공간에 내용을 덧대어 채우지 말고 더 넓은 화면을 위해 새로운 구분점을 만들어 추가공간을 사용하세요. 일부 앱은 더 넓은 공간에서 실제로 이점을 가집니다.
구분점에 대해 생각할때는 사용자가 어떻게 앱을 사용할지, 어떻게 앱 사이즈를 조정하는지에 대해 고려하세요. 날씨 앱을 예로 들면 큰창에서는 7일간의 일기예보를 표시할수 있지만 창이 작아지면 모든것(글자크기나 그림의 크기등)이 줄어들지 않고 5일간의 일기예보가 표시되어야합니다. 계속 작아지면 내용이 섞어 작은 화면에 최적화하여 표현되어야 합니다.
일부 앱에서는 미니모드가 도움이 될 수 있습니다. 이 날씨앱(예제의 화면, 원본이미지 참조)에는 현재 조건만 표시됩니다. 음악플레이어라면 현재곡을 표시하고 다음노래로 넘어갈수 있는 버튼만 표시되면 될것입니다.
Pixel북이나 Surface와 같은 컨버터블 기기를 지원하기 위해 이 반응형 디자인에 대한 아이디어를 채택할수도 있습니다. 태블릿모드로 전환되면 이 장치는 활성창을 전체화면으로 만들고 어떻게 들고 있느냐에 따라 가로모드나 세로모드가 되어야 합니다.
반응형 디자인에 집중하는것이 중요합니다. 사용자가 창크기를 조정했건 태블릿모드로 전환했건 상관없이 반응형 디자인은 성공적인 데스크탑 PWA에 매우 중요합니다.
데스크탑에서의 app window는 수많은 새로운 가능성을 열어줍니다. 더 큰 화면을 위한 중단점 설정, 가로/세로모드 지원, 전체화면이나 아니거나 가상 키보드와 함께 잘 작동하는 방식을 취하기 위해 디자이너와 협업할수 있습니다.

What’s next?

우린 이미 맥과 윈도우를 지원하기 위해 노력하고 있습니다. 이 모든 플랫폼에서 다음을 준비하고 있습니다:

  • 키보드 단축키 지원을 추가하여 사용자 고유의 기능을 제공할 수 있도록 하겠습니다.
  • 아이콘에 배지를 지정하여 전체알림을 표시하지 않고 중요한 이벤트에 대해 사용자에게 알릴수 있도록 합니다.
  • 사용자가 설치된 PWA에 대해 앱에서 처리하는 링크를 클릭하면 열도록 하는 링크캡처를 지원하도록 하겠습니다.