1. 개요
목표
이 Codelab에서는 Cloud Firestore 에서 제공하는 레스토랑 추천 웹 앱을 빌드합니다.
당신이 배울 것
- 웹 앱에서 Cloud Firestore로 데이터 읽기 및 쓰기
- 실시간으로 Cloud Firestore 데이터의 변경 사항 듣기
- Firebase 인증 및 보안 규칙을 사용하여 Cloud Firestore 데이터 보호
- 복잡한 Cloud Firestore 쿼리 작성
필요한 것
이 Codelab을 시작하기 전에 다음을 설치했는지 확인하세요.
2. Firebase 프로젝트 생성 및 설정
Firebase 프로젝트 만들기
- Firebase 콘솔 에서 프로젝트 추가 를 클릭한 다음 Firebase 프로젝트의 이름을 FriendlyEats 로 지정합니다.
Firebase 프로젝트의 프로젝트 ID를 기억하세요.
- 프로젝트 만들기 를 클릭합니다.
우리가 만들 애플리케이션은 웹에서 사용할 수 있는 몇 가지 Firebase 서비스를 사용합니다.
- 사용자를 쉽게 식별하는 Firebase 인증
- 구조화된 데이터를 클라우드에 저장하고 데이터가 업데이트되면 즉시 알림을 받는 Cloud Firestore
- 정적 자산을 호스팅하고 제공하는 Firebase 호스팅
이 특정 Codelab에서는 이미 Firebase 호스팅을 구성했습니다. 그러나 Firebase 인증 및 Cloud Firestore의 경우 Firebase 콘솔을 사용하여 서비스를 구성하고 활성화하는 과정을 안내합니다.
익명 인증 활성화
인증이 이 Codelab의 초점은 아니지만 앱에 어떤 형태의 인증이 있는지는 중요합니다. 우리는 익명 로그인 을 사용할 것입니다. 즉, 메시지가 표시되지 않고 사용자가 자동으로 로그인됩니다.
익명 로그인을 활성화해야 합니다.
- Firebase 콘솔에서 왼쪽 탐색 메뉴의 빌드 섹션을 찾습니다.
- 인증 을 클릭한 다음 로그인 방법 탭을 클릭합니다(또는 여기를 클릭 하여 바로 이동).
- 익명 로그인 공급자를 활성화한 다음 저장 을 클릭합니다.
이렇게 하면 사용자가 웹 앱에 액세스할 때 애플리케이션이 자동으로 로그인할 수 있습니다. 자세한 내용은 익명 인증 설명서 를 참조하십시오.
Cloud Firestore 활성화
앱은 Cloud Firestore를 사용하여 레스토랑 정보 및 평점을 저장하고 수신합니다.
Cloud Firestore를 활성화해야 합니다. Firebase 콘솔의 빌드 섹션에서 Firestore 데이터베이스 를 클릭합니다. Cloud Firestore 창에서 데이터베이스 생성 을 클릭합니다.
Cloud Firestore의 데이터에 대한 액세스는 보안 규칙에 의해 제어됩니다. 이 Codelab의 뒷부분에서 규칙에 대해 자세히 이야기하겠지만 먼저 시작하려면 데이터에 대한 몇 가지 기본 규칙을 설정해야 합니다. 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; } } }
위의 규칙은 인증되지 않은 사용자가 읽거나 쓸 수 없도록 로그인한 사용자의 데이터 액세스를 제한합니다. 이것은 공개 액세스를 허용하는 것보다 낫지만 여전히 안전하지 않습니다. 나중에 Codelab에서 이러한 규칙을 개선할 것입니다.
3. 샘플 코드 받기
명령줄에서 GitHub 리포지토리 를 복제합니다.
git clone https://github.com/firebase/friendlyeats-web
샘플 코드는 📁 friendlyeats-web
디렉터리에 복제되어 있어야 합니다. 이제부터 이 디렉터리에서 모든 명령을 실행해야 합니다.
cd friendlyeats-web
시작 앱 가져오기
IDE(WebStorm, Atom, Sublime, Visual Studio Code...)를 사용하여 📁 friendlyeats-web
디렉터리를 열거나 가져옵니다. 이 디렉터리에는 아직 작동하지 않는 레스토랑 추천 앱으로 구성된 Codelab의 시작 코드가 포함되어 있습니다. 이 Codelab 전체에서 작동하도록 만들 예정이므로 곧 해당 디렉터리에서 코드를 수정해야 합니다.
4. Firebase 명령줄 인터페이스 설치
Firebase 명령줄 인터페이스(CLI)를 사용하면 웹 앱을 로컬에서 제공하고 웹 앱을 Firebase 호스팅에 배포할 수 있습니다.
- 다음 npm 명령을 실행하여 CLI를 설치합니다.
npm -g install firebase-tools
- 다음 명령을 실행하여 CLI가 올바르게 설치되었는지 확인합니다.
firebase --version
Firebase CLI 버전이 v7.4.0 이상인지 확인하세요.
- 다음 명령어를 실행하여 Firebase CLI를 승인합니다.
firebase login
앱의 로컬 디렉터리 및 파일에서 Firebase 호스팅에 대한 앱 구성을 가져오도록 웹 앱 템플릿을 설정했습니다. 하지만 이렇게 하려면 앱을 Firebase 프로젝트와 연결해야 합니다.
- 명령줄이 앱의 로컬 디렉터리에 액세스하고 있는지 확인하세요.
- 다음 명령을 실행하여 앱을 Firebase 프로젝트와 연결합니다.
firebase use --add
- 메시지가 표시되면 프로젝트 ID 를 선택한 다음 Firebase 프로젝트에 별칭을 지정합니다.
여러 환경(프로덕션, 스테이징 등)이 있는 경우 별칭이 유용합니다. 하지만 이 Codelab에서는 default
라는 별칭을 사용하겠습니다.
- 명령줄의 나머지 지침을 따릅니다.
5. 로컬 서버 실행
실제로 앱 작업을 시작할 준비가 되었습니다! 앱을 로컬에서 실행해 봅시다!
- 다음 Firebase CLI 명령어를 실행합니다.
firebase emulators:start --only hosting
- 명령줄에 다음 응답이 표시되어야 합니다.
hosting: Local server: http://localhost:5000
Firebase 호스팅 에뮬레이터를 사용하여 앱을 로컬에서 제공하고 있습니다. 이제 웹앱을 http://localhost:5000 에서 사용할 수 있습니다.
- http://localhost:5000 에서 앱을 엽니다.
Firebase 프로젝트에 연결된 FriendlyEats 사본이 표시되어야 합니다.
앱이 Firebase 프로젝트에 자동으로 연결되었으며 익명 사용자로 자동 로그인되었습니다.
6. Cloud Firestore에 데이터 쓰기
이 섹션에서는 앱의 UI를 채울 수 있도록 일부 데이터를 Cloud Firestore에 씁니다. 이 작업은 Firebase 콘솔 을 통해 수동으로 수행할 수 있지만 기본 Cloud Firestore 쓰기를 시연하기 위해 앱 자체에서 수행합니다.
데이터 모델
Firestore 데이터는 컬렉션, 문서, 필드, 하위 컬렉션으로 분할됩니다. 우리는 각 레스토랑을 restaurants
이라는 최상위 컬렉션에 문서로 저장할 것입니다.
나중에 각 리뷰를 각 레스토랑 아래의 ratings
이라는 하위 컬렉션에 저장합니다.
Firestore에 레스토랑 추가
우리 앱의 주요 모델 개체는 식당입니다. restaurants
컬렉션에 레스토랑 문서를 추가하는 코드를 작성해 봅시다.
- 다운로드한 파일에서
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
컬렉션에 새 문서를 추가합니다. 문서 데이터는 일반 JavaScript 개체에서 가져옵니다. 먼저 Cloud Firestore 컬렉션 restaurants
에 대한 참조를 가져온 다음 데이터를 add
하여 이를 수행합니다.
레스토랑을 추가하자!
- 브라우저에서 FriendlyEats 앱으로 돌아가서 새로 고칩니다.
- 모의 데이터 추가 를 클릭합니다.
앱은 레스토랑 개체의 임의 집합을 자동으로 생성한 다음 addRestaurant
함수를 호출합니다. 그러나 아직 데이터 검색 을 구현해야 하므로(Codelab의 다음 섹션) 실제 웹 앱에는 아직 데이터가 표시되지 않습니다 .
하지만 Firebase 콘솔에서 Cloud Firestore 탭 으로 이동하면 이제 restaurants
컬렉션에 새 문서가 표시됩니다!
축하합니다. 웹 앱에서 Cloud Firestore에 데이터를 작성했습니다!
다음 섹션에서는 Cloud Firestore에서 데이터를 검색하여 앱에 표시하는 방법을 알아봅니다.
7. Cloud Firestore의 데이터 표시
이 섹션에서는 Cloud Firestore에서 데이터를 검색하여 앱에 표시하는 방법을 알아봅니다. 두 가지 주요 단계는 쿼리를 만들고 스냅샷 수신기를 추가하는 것입니다. 이 리스너는 쿼리와 일치하는 모든 기존 데이터에 대한 알림을 받고 실시간으로 업데이트를 수신합니다.
먼저 필터링되지 않은 기본 레스토랑 목록을 제공할 쿼리를 구성해 보겠습니다.
-
scripts/FriendlyEats.Data.js
파일로 돌아갑니다. -
FriendlyEats.prototype.getAllRestaurants
함수를 찾으십시오. - 전체 함수를 다음 코드로 바꿉니다.
FriendlyEats.Data.js
FriendlyEats.prototype.getAllRestaurants = function(renderer) { var query = firebase.firestore() .collection('restaurants') .orderBy('avgRating', 'desc') .limit(50); this.getDocumentsInQuery(query, renderer); };
위의 코드에서 평균 등급(현재는 모두 0)으로 정렬된 최대 50개 restaurants
을 최상위 컬렉션에서 검색하는 쿼리를 구성합니다. 이 쿼리를 선언한 후 데이터 로드 및 렌더링을 담당하는 getDocumentsInQuery()
메서드에 쿼리를 전달합니다.
스냅샷 리스너를 추가하여 이를 수행합니다.
-
scripts/FriendlyEats.Data.js
파일로 돌아갑니다. -
FriendlyEats.prototype.getDocumentsInQuery
함수를 찾습니다. - 전체 함수를 다음 코드로 바꿉니다.
FriendlyEats.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
함수에 전달합니다. - 문서가 삭제되면
change.type
은removed
와 같습니다. 따라서 이 경우 UI에서 레스토랑을 제거하는 함수를 호출합니다.
이제 두 방법을 모두 구현했으므로 앱을 새로고침하고 이전에 Firebase 콘솔에서 보았던 레스토랑이 이제 앱에 표시되는지 확인합니다. 이 섹션을 성공적으로 완료했다면 이제 앱이 Cloud Firestore로 데이터를 읽고 쓰고 있는 것입니다!
레스토랑 목록이 변경되면 이 리스너가 자동으로 계속 업데이트됩니다. Firebase 콘솔로 이동하여 레스토랑을 수동으로 삭제하거나 이름을 변경해 보세요. 변경 사항이 사이트에 즉시 표시됩니다!
8. Get() 데이터
지금까지 onSnapshot
을 사용하여 업데이트를 실시간으로 검색하는 방법을 살펴보았습니다. 그러나 그것이 항상 우리가 원하는 것은 아닙니다. 때때로 데이터를 한 번만 가져오는 것이 더 합리적입니다.
사용자가 앱에서 특정 레스토랑을 클릭할 때 트리거되는 메서드를 구현하려고 합니다.
- 파일
scripts/FriendlyEats.Data.js
로 돌아갑니다. -
FriendlyEats.prototype.getRestaurant
함수를 찾습니다. - 전체 함수를 다음 코드로 바꿉니다.
FriendlyEats.Data.js
FriendlyEats.prototype.getRestaurant = function(id) { return firebase.firestore().collection('restaurants').doc(id).get(); };
이 방법을 구현한 후에는 각 레스토랑에 대한 페이지를 볼 수 있습니다. 목록에서 식당을 클릭하면 식당 세부 정보 페이지가 표시됩니다.
나중에 Codelab에서 평점 추가를 구현해야 하므로 지금은 평점을 추가할 수 없습니다.
9. 데이터 정렬 및 필터링
현재 우리 앱은 레스토랑 목록을 표시하지만 사용자가 필요에 따라 필터링할 수 있는 방법이 없습니다. 이 섹션에서는 Cloud Firestore의 고급 쿼리를 사용하여 필터링을 활성화합니다.
다음은 모든 Dim Sum
레스토랑을 가져오는 간단한 쿼리의 예입니다.
var filteredQuery = query.where('category', '==', 'Dim Sum')
이름에서 알 수 있듯이 where()
메서드는 필드가 우리가 설정한 제한을 충족하는 컬렉션의 구성원만 쿼리를 다운로드하도록 합니다. 이 경우 category
가 Dim Sum
인 레스토랑만 다운로드합니다.
우리 앱에서 사용자는 "샌프란시스코의 피자" 또는 "인기순으로 주문한 로스앤젤레스의 해산물"과 같은 특정 쿼리를 생성하기 위해 여러 필터를 연결할 수 있습니다.
사용자가 선택한 여러 기준에 따라 레스토랑을 필터링하는 쿼리를 작성하는 메서드를 만듭니다.
- 파일
scripts/FriendlyEats.Data.js
로 돌아갑니다. -
FriendlyEats.prototype.getFilteredRestaurants
함수를 찾습니다. - 전체 함수를 다음 코드로 바꿉니다.
FriendlyEats.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/project-id/database/firestore/indexes?create_composite=...
이러한 오류는 Cloud Firestore에 대부분의 복합 쿼리에 대한 색인이 필요하기 때문입니다. 쿼리에 색인을 요구하면 Cloud Firestore를 대규모로 빠르게 유지할 수 있습니다.
오류 메시지에서 링크를 열면 올바른 매개변수가 채워진 Firebase 콘솔의 색인 생성 UI가 자동으로 열립니다. 다음 섹션에서는 이 애플리케이션에 필요한 색인을 작성하고 배포합니다.
10. 인덱스 배포
앱의 모든 경로를 탐색하고 각 인덱스 생성 링크를 따르지 않으려면 Firebase CLI를 사용하여 한 번에 많은 인덱스를 쉽게 배포할 수 있습니다.
- 앱의 다운로드된 로컬 디렉터리에서
firestore.indexes.json
파일을 찾을 수 있습니다.
이 파일은 가능한 모든 필터 조합에 필요한 모든 인덱스를 설명합니다.
firestore.indexes.json
{ "indexes": [ { "collectionGroup": "restaurants", "queryScope": "COLLECTION", "fields": [ { "fieldPath": "city", "order": "ASCENDING" }, { "fieldPath": "avgRating", "order": "DESCENDING" } ] }, ... ] }
- 다음 명령을 사용하여 이러한 인덱스를 배포합니다.
firebase deploy --only firestore:indexes
몇 분 후 색인이 활성화되고 오류 메시지가 사라집니다.
11. 트랜잭션에 데이터 쓰기
이 섹션에서는 사용자가 레스토랑에 리뷰를 제출할 수 있는 기능을 추가합니다. 지금까지 우리의 모든 쓰기는 원자적이고 상대적으로 단순했습니다. 그들 중 하나라도 오류가 발생하면 사용자에게 다시 시도하라는 메시지를 표시하거나 앱이 자동으로 쓰기를 다시 시도합니다.
우리 앱에는 식당에 대한 평가를 추가하려는 사용자가 많을 것이므로 여러 읽기 및 쓰기를 조정해야 합니다. 먼저 리뷰 자체를 제출한 다음 레스토랑의 평점 count
와 average rating
을 업데이트해야 합니다. 이 중 하나는 실패하고 다른 하나는 실패하면 데이터베이스의 한 부분에 있는 데이터가 다른 부분의 데이터와 일치하지 않는 일관성 없는 상태가 됩니다.
다행스럽게도 Cloud Firestore는 단일 원자 작업에서 여러 읽기 및 쓰기를 수행할 수 있는 트랜잭션 기능을 제공하여 데이터의 일관성을 유지합니다.
- 파일
scripts/FriendlyEats.Data.js
로 돌아갑니다. -
FriendlyEats.prototype.addRating
함수를 찾습니다. - 전체 함수를 다음 코드로 바꿉니다.
FriendlyEats.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); }); }); };
위의 블록에서 레스토랑 문서의 avgRating
및 numRatings
의 숫자 값을 업데이트하는 트랜잭션을 트리거합니다. 동시에 ratings
하위 컬렉션에 새 rating
을 추가합니다.
12. 데이터 보안
이 Codelab의 시작 부분에서 우리는 모든 읽기 또는 쓰기에 대해 데이터베이스를 완전히 개방하도록 앱의 보안 규칙을 설정했습니다. 실제 애플리케이션에서는 바람직하지 않은 데이터 액세스 또는 수정을 방지하기 위해 훨씬 더 세분화된 규칙을 설정하려고 합니다.
- Firebase 콘솔의 빌드 섹션에서 Firestore 데이터베이스 를 클릭합니다.
- Cloud Firestore 섹션에서 규칙 탭을 클릭합니다(또는 여기를 클릭 하여 바로 이동).
- 기본값을 다음 규칙으로 바꾼 다음 게시 를 클릭합니다.
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. 결론
이 Codelab에서는 Cloud Firestore로 기본 및 고급 읽기 및 쓰기를 수행하는 방법과 보안 규칙으로 데이터 액세스를 보호하는 방법을 배웠습니다. quickstarts-js 리포지토리 에서 전체 솔루션을 찾을 수 있습니다.
Cloud Firestore에 대해 자세히 알아보려면 다음 리소스를 방문하세요.