Cloud Firestore 웹 Codelab

컬렉션을 사용해 정리하기 내 환경설정을 기준으로 콘텐츠를 저장하고 분류하세요.

1. 개요

목표

이 코드랩에서는 Cloud Firestore 에서 제공하는 레스토랑 추천 웹 앱을 빌드합니다.

img5.png

배울 내용

  • 웹 앱에서 Cloud Firestore로 데이터 읽기 및 쓰기
  • Cloud Firestore 데이터의 변화를 실시간으로 들어보세요.
  • Firebase 인증 및 보안 규칙을 사용하여 Cloud Firestore 데이터 보호
  • 복잡한 Cloud Firestore 쿼리 작성

필요한 것

이 코드랩을 시작하기 전에 다음을 설치했는지 확인하세요.

2. Firebase 프로젝트 생성 및 설정

Firebase 프로젝트 만들기

  1. Firebase 콘솔 에서 프로젝트 추가 를 클릭한 다음 Firebase 프로젝트의 이름을 FriendlyEats 로 지정합니다.

Firebase 프로젝트의 프로젝트 ID를 기억하세요.

  1. 프로젝트 만들기 를 클릭합니다.

빌드할 애플리케이션은 웹에서 사용할 수 있는 몇 가지 Firebase 서비스를 사용합니다.

  • 사용자를 쉽게 식별하는 Firebase 인증
  • Cloud Firestore 는 클라우드에 구조화된 데이터를 저장하고 데이터가 업데이트되면 즉시 알림을 받습니다.
  • 정적 자산을 호스팅하고 제공하는 Firebase 호스팅

이 특정 코드랩에서는 이미 Firebase 호스팅을 구성했습니다. 그러나 Firebase Auth 및 Cloud Firestore의 경우 Firebase 콘솔을 사용하여 서비스를 구성하고 활성화하는 방법을 안내해 드립니다.

익명 인증 사용

인증이 이 코드랩의 초점은 아니지만 앱에 어떤 형태의 인증을 포함하는 것이 중요합니다. 우리는 익명 로그인 을 사용할 것입니다. 즉, 사용자는 프롬프트 없이 자동으로 로그인됩니다.

익명 로그인을 활성화해야 합니다.

  1. Firebase 콘솔의 왼쪽 탐색 메뉴에서 Build 섹션을 찾습니다.
  2. 인증 을 클릭한 다음 로그인 방법 탭을 클릭합니다(또는 여기를 클릭 하여 바로 이동).
  3. 익명 로그인 공급자를 활성화한 다음 저장 을 클릭합니다.

img7.png

이렇게 하면 사용자가 웹 앱에 액세스할 때 애플리케이션에서 자동으로 로그인할 수 있습니다. 자세한 내용은 익명 인증 문서 를 참조하십시오.

Cloud Firestore 사용

앱은 Cloud Firestore를 사용하여 레스토랑 정보 및 평가를 저장하고 수신합니다.

Cloud Firestore를 사용 설정해야 합니다. Firebase 콘솔의 빌드 섹션에서 Firestore 데이터베이스 를 클릭합니다. Cloud Firestore 창에서 데이터베이스 만들기 를 클릭합니다.

Cloud Firestore의 데이터에 대한 액세스는 보안 규칙에 의해 제어됩니다. 이 코드랩의 뒷부분에서 규칙에 대해 더 자세히 설명하겠지만 시작하려면 먼저 데이터에 몇 가지 기본 규칙을 설정해야 합니다. Firebase 콘솔의 규칙 탭 에서 다음 규칙을 추가한 다음 게시 를 클릭합니다.

service cloud.firestore {
  match /databases/{database}/documents {
    match /{document=**} {
      //
      // WARNING: These rules are insecure! We will replace them with
      // more secure rules later in the codelab
      //
      allow read, write: if request.auth != null;
    }
  }
}

위의 규칙은 로그인한 사용자에 대한 데이터 액세스를 제한하여 인증되지 않은 사용자가 읽거나 쓰는 것을 방지합니다. 이것은 공개 액세스를 허용하는 것보다 낫지만 여전히 안전하지 않습니다. 이 규칙은 나중에 코드랩에서 개선할 것입니다.

3. 샘플 코드 받기

명령줄에서 GitHub 리포지토리 를 복제합니다.

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

샘플 코드는 📁 friendlyeats-web 디렉터리에 복제되어야 합니다. 지금부터 이 디렉토리에서 모든 명령을 실행해야 합니다.

cd friendlyeats-web

시작 앱 가져오기

IDE(WebStorm, Atom, Sublime, Visual Studio Code...)를 사용하여 📁 friendlyeats-web 디렉터리를 열거나 가져옵니다. 이 디렉토리에는 아직 기능하지 않는 레스토랑 추천 앱으로 구성된 코드랩의 시작 코드가 포함되어 있습니다. 이 코드랩 전체에서 작동하도록 만들 것이므로 곧 해당 디렉토리에서 코드를 편집해야 합니다.

4. Firebase 명령줄 인터페이스 설치

Firebase CLI(명령줄 인터페이스)를 사용하면 웹 앱을 로컬에서 제공하고 웹 앱을 Firebase 호스팅에 배포할 수 있습니다.

  1. 다음 npm 명령을 실행하여 CLI를 설치합니다.
npm -g install firebase-tools
  1. 다음 명령을 실행하여 CLI가 올바르게 설치되었는지 확인하십시오.
firebase --version

Firebase CLI의 버전이 v7.4.0 이상인지 확인합니다.

  1. 다음 명령어를 실행하여 Firebase CLI를 승인합니다.
firebase login

앱의 로컬 디렉토리 및 파일에서 Firebase 호스팅을 위한 앱 구성을 가져오도록 웹 앱 템플릿을 설정했습니다. 하지만 이렇게 하려면 앱을 Firebase 프로젝트와 연결해야 합니다.

  1. 명령줄이 앱의 로컬 디렉터리에 액세스하고 있는지 확인합니다.
  2. 다음 명령어를 실행하여 앱을 Firebase 프로젝트와 연결합니다.
firebase use --add
  1. 메시지가 표시되면 프로젝트 ID 를 선택한 다음 Firebase 프로젝트에 별칭을 지정합니다.

별칭은 여러 환경(프로덕션, 스테이징 등)이 있는 경우에 유용합니다. 그러나 이 코드랩에서는 default 의 별칭만 사용하겠습니다.

  1. 명령줄의 나머지 지침을 따릅니다.

5. 로컬 서버 실행

우리는 실제로 앱에서 작업을 시작할 준비가 되었습니다! 로컬에서 앱을 실행해 봅시다!

  1. 다음 Firebase CLI 명령어를 실행합니다.
firebase emulators:start --only hosting
  1. 명령줄에 다음 응답이 표시되어야 합니다.
hosting: Local server: http://localhost:5000

Firebase 호스팅 에뮬레이터를 사용하여 앱을 로컬로 제공하고 있습니다. 이제 웹 앱을 http://localhost:5000 에서 사용할 수 있습니다.

  1. http://localhost:5000 에서 앱을 엽니다.

Firebase 프로젝트에 연결된 FriendlyEats 사본이 표시되어야 합니다.

앱이 Firebase 프로젝트에 자동으로 연결되어 익명의 사용자로 자동 로그인되었습니다.

img2.png

6. Cloud Firestore에 데이터 쓰기

이 섹션에서는 앱의 UI를 채울 수 있도록 일부 데이터를 Cloud Firestore에 씁니다. Firebase 콘솔 을 통해 수동으로 수행할 수 있지만 기본 Cloud Firestore 쓰기를 시연하기 위해 앱 자체에서 수행합니다.

데이터 모델

Firestore 데이터는 컬렉션, 문서, 필드 및 하위 컬렉션으로 분할됩니다. 우리는 각 레스토랑을 restaurants 이라는 최상위 컬렉션에 문서로 저장할 것입니다.

img3.png

나중에 각 레스토랑 아래의 ratings 이라는 하위 컬렉션에 각 리뷰를 저장합니다.

img4.png

Firestore에 레스토랑 추가

우리 앱의 주요 모델 객체는 레스토랑입니다. 레스토랑 문서를 restaurants 컬렉션에 추가하는 코드를 작성해 보겠습니다.

  1. 다운로드한 파일에서 scripts/FriendlyEats.Data.js 를 엽니다.
  2. FriendlyEats.prototype.addRestaurant 함수를 찾습니다.
  3. 전체 함수를 다음 코드로 바꿉니다.

프렌들리이츠.Data.js

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

위의 코드는 restaurants 컬렉션에 새 문서를 추가합니다. 문서 데이터는 일반 JavaScript 개체에서 가져옵니다. 먼저 Cloud Firestore 컬렉션 restaurants 에 대한 참조를 얻은 다음 데이터를 add 하여 이를 수행합니다.

레스토랑을 추가하자!

  1. 브라우저에서 FriendlyEats 앱으로 돌아가서 새로 고칩니다.
  2. 모의 데이터 추가 를 클릭합니다.

앱은 자동으로 임의의 레스토랑 객체 세트를 생성한 다음 addRestaurant 함수를 호출합니다. 그러나 아직 데이터 검색 을 구현해야 하기 때문에 실제 웹 앱에는 데이터가 표시되지 않습니다 (코드랩의 다음 섹션).

하지만 Firebase 콘솔에서 Cloud Firestore 탭 으로 이동하면 이제 restaurants 컬렉션에 새 문서가 표시됩니다!

img6.png

축하합니다. 웹 앱에서 Cloud Firestore에 데이터를 작성했습니다.

다음 섹션에서는 Cloud Firestore에서 데이터를 검색하고 앱에 표시하는 방법을 배웁니다.

7. Cloud Firestore의 데이터 표시

이 섹션에서는 Cloud Firestore에서 데이터를 검색하고 앱에 표시하는 방법을 배웁니다. 두 가지 주요 단계는 쿼리 생성과 스냅샷 리스너 추가입니다. 이 수신기는 쿼리와 일치하는 모든 기존 데이터에 대한 알림을 받고 실시간으로 업데이트를 받습니다.

먼저 필터링되지 않은 기본 레스토랑 목록을 제공할 쿼리를 구성해 보겠습니다.

  1. scripts/FriendlyEats.Data.js 파일로 돌아갑니다.
  2. FriendlyEats.prototype.getAllRestaurants 함수를 찾습니다.
  3. 전체 함수를 다음 코드로 바꿉니다.

프렌들리이츠.Data.js

FriendlyEats.prototype.getAllRestaurants = function(renderer) {
  var query = firebase.firestore()
      .collection('restaurants')
      .orderBy('avgRating', 'desc')
      .limit(50);

  this.getDocumentsInQuery(query, renderer);
};

위의 코드에서 우리는 restaurants 이라는 최상위 컬렉션에서 평균 평점(현재 모두 0)으로 정렬된 최대 50개의 레스토랑을 검색하는 쿼리를 구성합니다. 이 쿼리를 선언한 후 데이터 로드 및 렌더링을 담당하는 getDocumentsInQuery() 메서드에 전달합니다.

스냅샷 리스너를 추가하여 이 작업을 수행합니다.

  1. scripts/FriendlyEats.Data.js 파일로 돌아갑니다.
  2. FriendlyEats.prototype.getDocumentsInQuery 함수를 찾습니다.
  3. 전체 함수를 다음 코드로 바꿉니다.

프렌들리이츠.Data.js

FriendlyEats.prototype.getDocumentsInQuery = function(query, renderer) {
  query.onSnapshot(function(snapshot) {
    if (!snapshot.size) return renderer.empty(); // Display "There are no restaurants".

    snapshot.docChanges().forEach(function(change) {
      if (change.type === 'removed') {
        renderer.remove(change.doc);
      } else {
        renderer.display(change.doc);
      }
    });
  });
};

위의 코드에서 query.onSnapshot 은 쿼리 결과가 변경될 때마다 콜백을 트리거합니다.

  • 처음으로 콜백은 쿼리의 전체 결과 세트로 트리거됩니다. 즉, Cloud Firestore의 전체 restaurants 컬렉션을 의미합니다. 그런 다음 모든 개별 문서를 renderer.display 함수에 전달합니다.
  • 문서가 removed 되면 change.type 은 remove 와 같습니다. 따라서 이 경우 UI에서 레스토랑을 제거하는 함수를 호출합니다.

이제 두 가지 방법을 모두 구현했으므로 앱을 새로고침하고 이전에 Firebase 콘솔에서 본 레스토랑이 이제 앱에 표시되는지 확인합니다. 이 섹션을 성공적으로 완료했다면 이제 앱이 Cloud Firestore로 데이터를 읽고 쓰는 것입니다!

레스토랑 목록이 변경되면 이 리스너는 자동으로 계속 업데이트됩니다. Firebase 콘솔로 이동하여 수동으로 레스토랑을 삭제하거나 이름을 변경해 보세요. 변경 사항이 사이트에 즉시 표시되는 것을 볼 수 있습니다.

img5.png

8. Get() 데이터

지금까지 onSnapshot 을 사용하여 실시간으로 업데이트를 검색하는 방법을 살펴보았습니다. 그러나 그것이 항상 우리가 원하는 것은 아닙니다. 때로는 데이터를 한 번만 가져오는 것이 더 합리적입니다.

사용자가 앱의 특정 레스토랑을 클릭할 때 트리거되는 메서드를 구현하려고 합니다.

  1. 파일 scripts/FriendlyEats.Data.js 로 돌아갑니다.
  2. FriendlyEats.prototype.getRestaurant 함수를 찾으십시오.
  3. 전체 함수를 다음 코드로 바꿉니다.

프렌들리이츠.Data.js

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

이 방법을 구현한 후에는 각 레스토랑의 페이지를 볼 수 있습니다. 목록에서 레스토랑을 클릭하기만 하면 레스토랑의 세부 정보 페이지가 표시됩니다.

img1.png

현재로서는 나중에 코드랩에서 평가 추가를 구현해야 하므로 평가를 추가할 수 없습니다.

9. 데이터 정렬 및 필터링

현재 우리 앱은 레스토랑 목록을 표시하지만 사용자가 필요에 따라 필터링할 수 있는 방법은 없습니다. 이 섹션에서는 Cloud Firestore의 고급 쿼리를 사용하여 필터링을 활성화합니다.

다음은 모든 Dim Sum 레스토랑을 가져오는 간단한 쿼리의 예입니다.

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

이름에서 알 수 있듯이 where() 메서드는 쿼리가 우리가 설정한 제한 사항을 충족하는 필드의 컬렉션 구성원만 다운로드하도록 합니다. 이 경우 categoryDim Sum 인 레스토랑만 다운로드합니다.

우리 앱에서 사용자는 여러 필터를 연결하여 "샌프란시스코의 피자" 또는 "인기에 의해 주문된 로스앤젤레스의 해산물"과 같은 특정 쿼리를 생성할 수 있습니다.

사용자가 선택한 여러 기준에 따라 레스토랑을 필터링하는 쿼리를 작성하는 메서드를 만들 것입니다.

  1. 파일 scripts/FriendlyEats.Data.js 로 돌아갑니다.
  2. FriendlyEats.prototype.getFilteredRestaurants 함수를 찾습니다.
  3. 전체 함수를 다음 코드로 바꿉니다.

프렌들리이츠.Data.js

FriendlyEats.prototype.getFilteredRestaurants = function(filters, renderer) {
  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, renderer);
};

위의 코드는 여러 where 필터와 단일 orderBy 절을 추가하여 사용자 입력을 기반으로 복합 쿼리를 작성합니다. 이제 쿼리는 사용자의 요구 사항과 일치하는 레스토랑만 반환합니다.

브라우저에서 FriendlyEats 앱을 새로 고친 다음 가격, 도시 및 카테고리별로 필터링할 수 있는지 확인하십시오. 테스트하는 동안 브라우저의 JavaScript 콘솔에 다음과 같은 오류가 표시됩니다.

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

이러한 오류는 Cloud Firestore에 대부분의 복합 쿼리에 대한 색인이 필요하기 때문입니다. 쿼리에 색인이 필요하면 Cloud Firestore를 대규모로 빠르게 유지할 수 있습니다.

오류 메시지의 링크를 열면 올바른 매개변수가 채워진 Firebase 콘솔의 인덱스 생성 UI가 자동으로 열립니다. 다음 섹션에서는 이 애플리케이션에 필요한 인덱스를 작성하고 배포합니다.

10. 인덱스 배포

앱의 모든 경로를 탐색하고 각 인덱스 생성 링크를 따르고 싶지 않다면 Firebase CLI를 사용하여 한 번에 많은 인덱스를 쉽게 배포할 수 있습니다.

  1. 앱의 다운로드한 로컬 디렉토리에서 firestore.indexes.json 파일을 찾을 수 있습니다.

이 파일은 가능한 모든 필터 조합에 필요한 모든 인덱스를 설명합니다.

firestore.indexes.json

{
 "indexes": [
   {
     "collectionGroup": "restaurants",
     "queryScope": "COLLECTION",
     "fields": [
       { "fieldPath": "city", "order": "ASCENDING" },
       { "fieldPath": "avgRating", "order": "DESCENDING" }
     ]
   },

   ...

 ]
}
  1. 다음 명령을 사용하여 이러한 인덱스를 배포합니다.
firebase deploy --only firestore:indexes

몇 분 후에 색인이 활성화되고 오류 메시지가 사라집니다.

11. 트랜잭션에 데이터 쓰기

이 섹션에서는 사용자가 레스토랑에 리뷰를 제출할 수 있는 기능을 추가합니다. 지금까지 우리의 모든 쓰기는 원자적이고 비교적 단순했습니다. 이들 중 하나라도 오류가 발생하면 사용자에게 다시 시도하라는 메시지를 표시하거나 앱에서 자동으로 쓰기를 다시 시도합니다.

우리 앱에는 식당에 대한 평가를 추가하려는 많은 사용자가 있으므로 여러 읽기 및 쓰기를 조정해야 합니다. 먼저 리뷰 자체를 제출한 다음 레스토랑의 평점 countaverage rating 을 업데이트해야 합니다. 이 중 하나가 실패하고 다른 하나는 실패하면 데이터베이스의 한 부분에 있는 데이터가 다른 부분에 있는 데이터와 일치하지 않는 일관성 없는 상태가 됩니다.

다행히 Cloud Firestore는 단일 원자성 작업으로 여러 읽기 및 쓰기를 수행할 수 있는 트랜잭션 기능을 제공하여 데이터의 일관성을 유지합니다.

  1. 파일 scripts/FriendlyEats.Data.js 로 돌아갑니다.
  2. FriendlyEats.prototype.addRating 함수를 찾습니다.
  3. 전체 함수를 다음 코드로 바꿉니다.

프렌들리이츠.Data.js

FriendlyEats.prototype.addRating = function(restaurantID, rating) {
  var collection = firebase.firestore().collection('restaurants');
  var document = collection.doc(restaurantID);
  var newRatingDocument = document.collection('ratings').doc();

  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);

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

위의 블록에서 트랜잭션을 트리거하여 레스토랑 문서에서 avgRatingnumRatings 의 숫자 값을 업데이트합니다. 동시에 ratings 하위 컬렉션에 새 rating 을 추가합니다.

12. 데이터 보호

이 코드랩의 시작 부분에서 앱의 보안 규칙을 설정하여 데이터베이스를 모든 읽기 또는 쓰기에 완전히 개방합니다. 실제 응용 프로그램에서는 바람직하지 않은 데이터 액세스 또는 수정을 방지하기 위해 훨씬 더 세분화된 규칙을 설정하려고 합니다.

  1. Firebase 콘솔의 빌드 섹션에서 Firestore 데이터베이스 를 클릭합니다.
  2. Cloud Firestore 섹션에서 규칙 탭을 클릭합니다(또는 여기를 클릭 하여 바로 이동).
  3. 기본값을 다음 규칙으로 바꾼 다음 게시 를 클릭합니다.

firestore.rules

rules_version = '2';
service cloud.firestore {

  // Determine if the value of the field "key" is the same
  // before and after the request.
  function unchanged(key) {
    return (key in resource.data) 
      && (key in request.resource.data) 
      && (resource.data[key] == request.resource.data[key]);
  }

  match /databases/{database}/documents {
    // Restaurants:
    //   - Authenticated user can read
    //   - Authenticated user can create/update (for demo purposes only)
    //   - Updates are allowed if no fields are added and name is unchanged
    //   - Deletes are not allowed (default)
    match /restaurants/{restaurantId} {
      allow read: if request.auth != null;
      allow create: if request.auth != null;
      allow update: if request.auth != null
                    && (request.resource.data.keys() == resource.data.keys()) 
                    && unchanged("name");
      
      // Ratings:
      //   - Authenticated user can read
      //   - Authenticated user can create if userId matches
      //   - Deletes and updates are not allowed (default)
      match /ratings/{ratingId} {
        allow read: if request.auth != null;
        allow create: if request.auth != null
                      && request.resource.data.userId == request.auth.uid;
      }
    }
  }
}

이러한 규칙은 클라이언트가 안전한 변경만 수행할 수 있도록 액세스를 제한합니다. 예를 들어:

  • 레스토랑 문서에 대한 업데이트는 등급만 변경할 수 있으며 이름이나 기타 변경할 수 없는 데이터는 변경할 수 없습니다.
  • 사용자 ID가 로그인한 사용자와 일치하는 경우에만 등급을 생성할 수 있으므로 스푸핑을 방지할 수 있습니다.

Firebase 콘솔을 사용하는 대신 Firebase CLI를 사용하여 Firebase 프로젝트에 규칙을 배포할 수 있습니다. 작업 디렉토리의 firestore.rules 파일에는 이미 위의 규칙이 포함되어 있습니다. Firebase 콘솔을 사용하지 않고 로컬 파일 시스템에서 이러한 규칙을 배포하려면 다음 명령어를 실행합니다.

firebase deploy --only firestore:rules

13. 결론

이 코드랩에서는 Cloud Firestore로 기본 및 고급 읽기 및 쓰기를 수행하는 방법과 보안 규칙으로 데이터 액세스를 보호하는 방법을 배웠습니다. quickstarts-js 리포지토리 에서 전체 솔루션을 찾을 수 있습니다.

Cloud Firestore에 대해 자세히 알아보려면 다음 리소스를 방문하세요.