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로 해야 진행이 수월했습니다.

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(저장)을 클릭하세요.

4. Run the Local Server

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

firebase serve

이제 브라우저를 열어 localhost:5000을 봅시다. Firebase 프로젝트에 연결된 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에 데이터를 기록한걸 축하합니다. 다음 섹션에서 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로부터 문서를 한번 가져오는것도 가능합니다.

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일 입니다.