멀티 플랫폼 Firestore Flutter

목표

이 Codelab에서는 FlutterCloud Firestore에서 제공하는 멀티 플랫폼 레스토랑 추천 앱을 빌드합니다.

완성된 앱은 Android, iOS, 웹의 단일 Dart 코드베이스에서 실행됩니다.

5e7215e72fa571b.png

학습할 내용

  • Flutter 앱에서 Cloud Firestore로 데이터 읽기 및 쓰기
  • 실시간으로 Cloud Firestore 데이터 변경사항 수신 대기
  • Firebase 인증 및 보안 규칙을 사용하여 Cloud Firestore 데이터 보호
  • 복잡한 Cloud Firestore 쿼리 및 트랜잭션 작성

이 Codelab에서 학습하고 싶은 내용은 무엇인가요?

이 주제를 처음 접하므로 개요를 파악하고 싶습니다. 이 주제에 관해 약간 알고 있지만 한 번 더 확인하고 싶습니다. 프로젝트에 사용할 코드 예를 찾고 있습니다. 특정 항목에 관한 설명을 찾고 있습니다.

요구사항

Flutter 또는 Firestore를 잘 알지 못하는 경우 먼저, Flutter의 Firebase Codelab을 완료합니다.

이 Codelab을 완료하려면 다음이 필요합니다.

  • 원하는 IDE 또는 텍스트 편집기(예: Dart 및 Flutter 플러그인으로 구성된 VS Code 또는 Android 스튜디오)
  • Chrome 브라우저 그리고 Chrome 개발자 도구에 관한 약간의 지식
  • 웹 지원이 사용 설정된 최신 버전의 Flutter(채널 beta 이상): 이 Codelab을 진행하는 동안 웹 지원을 구성하지만 자세한 내용은 Flutter의 웹 지원 페이지를 참고하세요.
  • npm 도구: 이 Codelab의 마지막 부분(색인 배포, 데이터 보안, Firebase 호스팅에 배포)을 위한 공식 firebase 명령줄 도구를 설치하는 데 필요함
  • (선택사항) Android용 앱을 컴파일하려는 경우 연결된 Android 기기 또는 에뮬레이터
  • (선택사항) iOS용 앱을 컴파일하려는 경우 최신 버전의 XCode가 설치된 Mac

Firebase 프로젝트 만들기

  1. Firebase Console에서 프로젝트 추가를 클릭한 후 Firebase 프로젝트의 이름을 FriendlyEats로 지정합니다. Firebase 프로젝트의 프로젝트 ID를 기억합니다(또는 수정 아이콘을 클릭하여 선호하는 프로젝트 ID 설정).
  2. 프로젝트 만들기를 클릭합니다.

빌드하는 애플리케이션은 다음과 같이 웹에서 사용 가능한 여러 Firebase 서비스를 사용합니다.

  • Firebase 인증 - 사용자를 쉽게 식별할 수 있음
  • Cloud Firestore - 클라우드에 구조화된 데이터를 저장하고 데이터가 업데이트되면 인스턴트 알림을 받을 수 있음
  • Firebase 호스팅 - 정적 애셋을 호스팅하고 제공할 수 있음

다음으로, Firebase Console을 사용하여 서비스를 구성하고 사용 설정하는 방법을 안내합니다.

익명 인증 사용 설정

이 Codelab에서 인증을 중점적으로 다루지는 않지만 앱에 어떤 형태로든 인증이 있는 것이 중요합니다. 따라서 익명 로그인을 사용합니다. 즉, 사용자에게 로그인하라는 메시지가 표시되지 않고 사용자가 자동으로 로그인됩니다.

익명 로그인을 사용 설정하는 방법은 다음과 같습니다.

  1. Firebase Console에서 왼쪽 탐색 메뉴의 개발 섹션을 찾습니다.
  2. Authentication을 클릭한 후 로그인 방법 탭을 클릭합니다(또는 Firebase Console로 직접 이동).
  3. 익명 로그인 제공업체를 사용 설정하고 저장을 클릭합니다.

fee6c3ebdf904459.png

익명 로그인을 사용 설정하면 사용자가 웹 앱에 액세스할 때 애플리케이션에 자동으로 로그인할 수 있습니다. 자세한 내용은 익명 인증 문서를 참고하세요.

Cloud Firestore 사용 설정

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

Cloud Firestore를 설정하는 방법은 다음과 같습니다.

  1. Firebase Console의 개발 섹션에서 Database를 클릭합니다.
  2. Cloud Firestore 창에서 데이터베이스 만들기를 클릭합니다.

57e83568e05c7710.png

  1. 테스트 모드로 시작 옵션을 선택하고 보안 규칙에 관한 면책조항을 읽은 후 사용 설정을 클릭합니다.

테스트 모드를 사용하면 개발 중에 데이터베이스에 자유롭게 쓸 수 있습니다. 이 Codelab의 뒷부분에서 데이터베이스 보안을 강화합니다.

daef1061fc25acc7.png

다음과 같이 명령줄에서 GitHub 저장소를 클론합니다.

git clone https://github.com/FirebaseExtended/codelab-friendlyeats-flutter.git friendlyeats-flutter

샘플 코드를 📁friendlyeats-flutter 디렉터리에 클론해야 합니다. 이제부터 이 디렉터리에서 명령어를 실행해야 합니다.

cd friendlyeats-flutter

시작 앱 가져오기

📁friendlyeats-flutter 디렉터리를 열거나 원하는 IDE로 가져옵니다. 이 디렉터리에는 아직 작동하지 않는 레스토랑 추천 앱으로 구성된 Codelab의 시작 코드가 포함되어 있습니다.

이 Codelab 전체에 걸쳐 앱이 작동하도록 설정합니다. 따라서 잠시 후 이 디렉터리의 코드를 수정합니다.

작업할 파일 찾기

Flutter 앱의 일반적인 진입점은 lib/main.dart 파일이지만 이 Codelab에서는 데이터 측면에 초점을 맞춥니다.

프로젝트에서 다음 파일을 찾습니다.

  • lib/src/model/data.dart: 이 Codelab을 진행하는 동안 수정하는 기본 파일입니다. 여기에는 Firestore에서 데이터를 읽고 쓰는 모든 로직이 포함되어 있습니다.
  • web/index.html: 브라우저가 애플리케이션을 시작하기 위해 로드하는 파일입니다. 이 파일을 수정하여 웹용 Firebase 라이브러리를 설치하고 초기화합니다.

Firebase CLI

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.json 파일이 이미 있습니다. 이제 다음과 같이 앱의 작업 복사본을 Firebase 프로젝트와 연결해야 합니다.

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

별칭은 여러 환경(프로덕션, 스테이징 등)이 있는 경우에 유용합니다. 그러나 이 Codelab에서는 default 별칭을 사용하면 됩니다.

  1. 명령줄에 제공된 안내를 따릅니다.

Flutter의 웹 지원 사용 설정

웹에서 실행할 Flutter 앱을 컴파일하려면 이 기능을 사용 설정해야 합니다(현재 베타 상태임). 웹 지원을 사용 설정하려면 다음을 입력합니다.

$ flutter channel beta
$ flutter upgrade
$ flutter config --enable-web

IDE의 기기 풀다운에 또는 명령줄에서 flutter devices 명령어 사용 시 이제 Chrome웹 서버 목록이 표시됩니다.

Chrome 기기는 Chrome을 자동으로 시작합니다. 웹 서버는 앱을 호스팅하는 서버를 시작합니다. 그러면 어떤 브라우저에서든 앱을 로드할 수 있습니다.

개발 중에 DevTools를 사용할 수 있도록 Chrome 기기를 사용하고, 다른 브라우저에서 테스트하려는 경우 웹 서버를 사용합니다.

Firebase 프로젝트를 만든 후 이 Firebase 프로젝트를 사용할 앱을 하나 이상 구성할 수 있습니다. 다음과 같이 합니다.

  • Firebase에 앱의 플랫폼별 ID를 등록합니다.
  • 앱의 구성 파일을 생성합니다.
  • 프로젝트 폴더의 적절한 위치에 구성을 추가합니다.

ac27fbbadff7a3b9.png

여러 플랫폼용 Flutter 앱을 개발하는 경우 동일한 Firebase 프로젝트 내에서 앱이 실행되는 각 플랫폼을 등록해야 합니다.

이 Codelab에서는 웹 플랫폼에 중점을 둡니다. Flutter의 Firebase Codelab에서 iOS 및 Android를 집중적으로 다루기 때문입니다. FriendlyEats 앱에 Android 또는 iOS 지원을 추가하려는 경우 그 Codelab으로 이동하세요.

Flutter 앱에는 웹에서 실행할 때 앱의 진입점으로 사용되는 특별한 web/index.html 파일이 있습니다. 프로젝트의 특정 구성으로 이 진입점을 수정합니다. 그러면 웹 애플리케이션이 Firebase 백엔드에 연결할 수 있습니다.

웹용 구성

  1. Firebase Console의 왼쪽 탐색 메뉴에서 프로젝트 개요를 선택하고 앱에 Firebase를 추가하여 시작하기에서 버튼을 클릭합니다. 그러면 다음 대화상자가 표시됩니다.

f76ed55f71f15953.png

  1. 앱에 닉네임을 지정합니다. 이 닉네임은 Firestore 콘솔에서 앱의 웹 버전을 식별하는 데 사용하는 값입니다.
  2. 앱 등록을 클릭합니다.
  3. 앱을 등록하면 Firebase SDK 추가 단계에서 Flutter 앱의 web/index.html 파일에 붙여넣어야 하는 코드를 몇 가지 제공합니다. 완료 시 파일의 코드는 다음과 유사합니다.

web/index.html

<!DOCTYPE html>
<html>
<head>
  <meta charset="UTF-8">
  <title>friendlyeats</title>

  <!-- The core Firebase JS SDK is always required and must be listed first -->
  <script src="https://www.gstatic.com/firebasejs/7.5.1/firebase-app.js"></script>

  <!-- TODO: Add SDKs for Firebase products that you want to use
      https://firebase.google.com/docs/web/setup#available-libraries -->

  <script>
    // Your web app's Firebase configuration
    var firebaseConfig = {
      apiKey: "YoUr_RaNdOm_API_kEy",
      authDomain: "your-project-name.firebaseapp.com",
      databaseURL: "https://your-project-name.firebaseio.com",
      projectId: "your-project-name",
      storageBucket: "your-project-name.appspot.com",
      messagingSenderId: "012345678901",
      appId: "1:109876543210:web:r4nd0mH3xH45h"
    };
    // Initialize Firebase
    firebase.initializeApp(firebaseConfig);
  </script>

</head>
<body>
  <script src="main.dart.js" type="application/javascript"></script>
</body>
</html>
  1. 방금 붙여넣은 코드에 TODO가 있습니다. 지금 바로 수정합니다. 이 Codelab에서는 Firebase 인증 및 Firestore를 사용하므로 이제 다음과 같이 두 제품의 스크립트 태그를 추가합니다.

web/index.html

  ...
  <!-- TODO: Add SDKs for Firebase products that you want to use
      https://firebase.google.com/docs/web/setup#available-libraries -->

  <script src="https://www.gstatic.com/firebasejs/7.5.1/firebase-auth.js"></script>
  <script src="https://www.gstatic.com/firebasejs/7.5.1/firebase-firestore.js"></script>

  <script>
    // Your web app's Firebase configuration
    var firebaseConfig = {
      ...
  1. web/index.html 파일을 저장하고 웹 앱에 Firebase 추가 대화상자에서 콘솔로 이동을 클릭합니다.
  2. Flutter 앱이 Firebase에 연결할 준비가 되었습니다.

d43a19dbf1248134.png특별한 것을 찾을 수 있어요.

Firebase 지원을 사용 설정하는 데 필요한 대부분의 코드 변경사항은 작업 중인 프로젝트에 이미 체크인되어 있습니다. 그러나 모바일 플랫폼을 위한 지원을 추가하려면 다음과 같이 웹과 관련하여 진행한 것과 비슷한 프로세스를 따라야 합니다.

  • Firebase 프로젝트에 원하는 플랫폼을 등록합니다.
  • 플랫폼별 구성 파일을 다운로드하여 코드에 추가합니다.

Flutter 앱의 최상위 디렉터리에는 iosandroid라는 하위 디렉터리가 있습니다. 이러한 디렉터리에는 각각 iOS 및 Android용 플랫폼별 구성 파일이 들어 있습니다.

iOS 구성

  1. Firebase Console의 왼쪽 탐색 메뉴에서 프로젝트 개요를 선택하고 앱에 Firebase를 추가하여 시작하기에서 iOS 버튼을 클릭합니다.

다음 대화상자가 표시됩니다.

c42139f18fb9a2ee.png

  1. 제공해야 할 중요한 값은 iOS 번들 ID입니다. 다음 세 단계를 진행하여 번들 ID를 얻습니다.
  1. 명령줄 도구에서 Flutter 앱의 최상위 디렉터리로 이동합니다.
  2. open ios/Runner.xcworkspace 명령어를 실행하여 Xcode를 엽니다.
  1. 아래와 같이 Xcode의 왼쪽 창에서 최상위 Runner(실행기)를 클릭하면 오른쪽 창에 General(일반) 탭이 표시됩니다. Bundle Identifier(번들 식별자) 값을 복사합니다.

9733e26be329f329.png

  1. Firebase 대화상자로 돌아가서, 복사한 번들 식별자iOS 번들 ID 필드에 붙여넣고 앱 등록을 클릭합니다.
  1. Firebase에서 계속 진행하여 안내에 따라 구성 파일 GoogleService-Info.plist를 다운로드합니다.
  2. Xcode로 돌아갑니다. Runner(실행기)에는 Runner라는 하위 폴더도 있습니다(위 이미지 참조).
  3. 방금 다운로드한 GoogleService-Info.plist 파일을 이 Runner 하위 폴더로 드래그합니다.
  4. Xcode에 표시되는 대화상자에서 Finish(완료)를 클릭합니다.
  5. Firebase Console로 돌아갑니다. 설정 단계에서 다음을 클릭하고 나머지 단계를 건너뛰며 Firebase Console의 기본 페이지로 돌아갑니다.

iOS용 Flutter 앱 구성을 완료했습니다.

Android 구성

  1. Firebase Console의 왼쪽 탐색 메뉴에서 프로젝트 개요를 선택하고 앱에 Firebase를 추가하여 시작하기에서 Android 버튼을 클릭합니다.

다음 대화상자가 표시됩니다. 8254fc299e82f528.png

  1. 제공해야 할 중요한 값은 Android 패키지 이름입니다. 다음 두 단계를 진행하면 패키지 이름을 얻을 수 있습니다.
  1. Flutter 앱 디렉터리에서 android/app/src/main/AndroidManifest.xml 파일을 엽니다.
  2. manifest 요소에서 package 속성의 문자열 값을 찾습니다. 이 값이 Android 패키지 이름입니다(예: com.yourcompany.yourproject). 이 값을 복사합니다.
  3. Firebase 대화상자에서 복사한 패키지 이름을 Android 패키지 이름 필드에 붙여넣습니다.
  4. 이 Codelab에서는 디버그 서명 인증서 SHA-1이 필요하지 않습니다. 이 필드는 비워둡니다.
  5. 앱 등록을 클릭합니다.
  6. Firebase에서 계속 진행하여 안내에 따라 구성 파일 google-services.json을 다운로드합니다.
  7. Flutter 앱 디렉터리로 이동하여 방금 다운로드한 google-services.json 파일을 android/app 디렉터리로 이동합니다.
  8. 다시 Firebase Console에서 나머지 단계를 건너뛰고 Firebase Console의 기본 페이지로 돌아갑니다.
  9. 모든 Gradle 구성이 이미 체크인되어 있습니다. 앱이 여전히 실행 중인 경우 앱을 닫았다가 다시 빌드하여 Gradle이 종속 항목을 설치할 수 있도록 합니다.

Android용 Flutter 앱 구성을 완료했습니다.

실제로 앱에 관한 작업을 할 준비가 되었습니다. 먼저, 로컬에서 앱을 실행합니다. 이제 구성한(그리고 기기 및 에뮬레이터를 사용할 수 있는) 모든 플랫폼에서 앱을 실행할 수 있습니다.

다음 명령어로 사용할 수 있는 기기를 검색해 보세요.

flutter devices

사용 가능한 기기에 따라 위 명령어의 출력은 다음과 비슷합니다.

3 connected devices:

Android SDK built for x86 • emulator-5554 • android-x86    • Android 7.1.1 (API 25) (emulator)
Chrome                    • chrome        • web-javascript • Google Chrome 79.0.3945.130
Web Server                • web-server    • web-javascript • Flutter Tools

chrome 기기를 사용하여 이 Codelab을 계속 진행합니다.

  1. 다음 Flutter CLI 명령어를 실행합니다.
flutter run -d chrome
  1. Flutter는 Building your application for the web으로 시작하고 실행 중인 앱이 있는 Chrome 창을 자동으로 엽니다.

이제 Firebase 프로젝트에 연결된 FriendlyEats 복사본이 표시됩니다.

앱이 Firebase 프로젝트에 자동으로 연결되고 익명 사용자로 자동 로그인됩니다.

c45806a2ac9300d9.png

이 섹션에서는 Cloud Firestore에 일부 데이터를 작성하여 앱의 UI를 채웁니다. 이 작업은 Firebase Console을 사용하여 수동으로 할 수 있지만 기본적인 Cloud Firestore 쓰기 데모를 보기 위해 앱에서 진행합니다.

데이터 모델

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

92f8dc2c769d2d6c.png

나중에 각 restaurant 내의 ratings라는 하위 컬렉션에 각 리뷰를 저장합니다.

a00d9eb006ddd6c0.png

Firestore에 레스토랑 추가

앱의 기본 모델 객체는 레스토랑입니다. 다음으로, restaurants 컬렉션에 레스토랑 문서를 추가하는 코드를 작성합니다.

  1. lib/src/model/data.dart를 엽니다.
  2. addRestaurant 함수를 찾습니다.
  3. 함수 전체를 다음 코드로 대체합니다.

lib/src/model/data.dart

Future<void> addRestaurant(Restaurant restaurant) {
  final restaurants = FirebaseFirestore.instance.collection('restaurants');
  return restaurants.add({
    'avgRating': restaurant.avgRating,
    'category': restaurant.category,
    'city': restaurant.city,
    'name': restaurant.name,
    'numRatings': restaurant.numRatings,
    'photo': restaurant.photo,
    'price': restaurant.price,
  });
}

위의 코드는 restaurants 컬렉션에 새 문서를 추가합니다.

이렇게 하려면 먼저, Cloud Firestore 컬렉션 restaurants에 대한 참조를 가져온 후 데이터를 adding하면 됩니다.

문서 데이터는 Restaurant 객체에서 가져오며 Firestore 플러그인의 Map으로 변환해야 합니다.

일부 레스토랑 추가

  1. Flutter 앱을 다시 빌드하고 새로고침합니다(앱을 실행하는 터미널 창에서 Shift + R을 누름).
  2. ADD SOME을 클릭합니다.

앱은 임의의 restaurants 객체 세트를 자동으로 생성하고 addRestaurant 함수를 호출합니다. 그러나 웹 앱에 데이터가 표시되지 않습니다. 이는 계속해서 데이터 검색을 구현해야 하기 때문입니다(이 Codelab의 다음 섹션에서).

Firebase Console에서 개발 > Database > Cloud Firestore 탭으로 이동하면 restaurants 컬렉션의 새 문서가 표시됩니다.

f06898b9d6dd4881.png

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

다음 섹션에서는 Cloud Firestore에서 데이터를 검색하여 앱에 표시하는 방법을 알아봅니다.

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

먼저, 필터링되지 않은 기본 레스토랑 목록을 제공하는 쿼리를 생성합니다.

  1. lib/src/model/data.dart 파일로 돌아갑니다.
  2. loadAllRestaurants 함수를 찾습니다.
  3. 함수 전체를 다음 코드로 대체합니다.

lib/src/model/data.dart

Stream<QuerySnapshot> loadAllRestaurants() {
  return FirebaseFirestore.instance
      .collection('restaurants')
      .orderBy('avgRating', descending: true)
      .limit(50)
      .snapshots();
}

위의 코드는 restaurants라는 최상위 컬렉션에서 최대 50개의 레스토랑을 검색하여 평균 평점(현재는 모두 0)을 기준으로 정렬하는 쿼리를 생성합니다.

이제 Stream에서 반환된 각 QuerySnapshot을 렌더링 가능한 Restaurant 데이터로 변환해야 합니다.

restaurants 컬렉션의 QuerySnapshot에서 Restaurant 정보를 추출하는 방법은 다음과 같습니다.

  1. lib/src/model/data.dart 파일로 돌아갑니다.
  2. getRestaurantsFromQuery 함수를 찾습니다.
  3. 함수 전체를 다음 코드로 대체합니다.

lib/src/model/data.dart

List<Restaurant> getRestaurantsFromQuery(QuerySnapshot snapshot) {
  return snapshot.docs.map((DocumentSnapshot doc) {
    return Restaurant.fromSnapshot(doc);
  }).toList();
}

getRestaurantsFromQuery 메서드는 앞서 만든 Query의 새 QuerySnapshot이 있을 때마다 호출됩니다. QuerySnapshots는 Firestore에서 Query의 변경사항을 앱에 실시간으로 알리는 데 사용하는 메커니즘입니다.

이 메서드는 snapshot에 포함된 모든 documents를 Flutter 앱의 다른 위치에 사용할 수 있는 Restaurant 객체로 변환합니다.

두 메서드를 모두 구현했으므로 이제 앱을 다시 빌드하고 새로고침한 후 앞서 Firebase Console에서 보았던 레스토랑이 앱에 표시되는지 확인합니다. 이 섹션을 성공적으로 완료했다면 앱이 Cloud Firestore로 데이터를 읽고 씁니다.

레스토랑 목록이 변경되면 이 리스너가 자동으로 업데이트됩니다. Firebase Console로 이동하여 레스토랑을 직접 삭제하거나 이름을 변경해 보세요. 그러면 변경사항이 사이트에 즉시 표시되는 것을 확인할 수 있습니다.

edd9adbafa5bd539.png

지금까지 onSnapshot을 사용하여 실시간으로 업데이트를 검색하는 방법을 알아보았습니다. 그러나 실시간 업데이트 검색이 항상 필요한 것은 아닙니다. 때로 데이터를 한 번만 가져오는 것이 적합한 사례도 있습니다.

사용자가 앱에서 특정 레스토랑을 클릭할 때 ID에서 특정 레스토랑을 로드하는 메서드가 필요합니다.

  1. lib/src/model/data.dart 파일로 돌아갑니다.
  2. getRestaurant 함수를 찾습니다.
  3. 함수 전체를 다음 코드로 대체합니다.

lib/src/model/data.dart

Future<Restaurant> getRestaurant(String restaurantId) {
  return FirebaseFirestore.instance
      .collection('restaurants')
      .doc(restaurantId)
      .get()
      .then((DocumentSnapshot doc) => Restaurant.fromSnapshot(doc));
}

이 코드는 get()을 사용하여 요청된 레스토랑의 정보가 포함된 Future<DocumentSnapshot>을 검색합니다. 준비가 될 때마다 then()을 통해 DocumentSnapshotRestaurant 객체로 변환하는 함수로 파이핑하기만 하면 됩니다.

이 메서드를 구현하면 각 레스토랑의 세부정보 페이지를 확인할 수 있습니다.

  1. flutter가 실행되고 있는 터미널에서 Shift + R을 눌러 앱을 새로고침합니다.
  2. 목록에서 레스토랑을 클릭하면 다음과 같이 레스토랑 세부정보 페이지가 표시됩니다.

f8ca540dda5540a9.png

다음으로, 트랜잭션을 사용하여 레스토랑에 평점을 추가하는 데 필요한 코드를 추가합니다.

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

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

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

  1. lib/src/model/data.dart 파일로 돌아갑니다.
  2. addReview 함수를 찾습니다.
  3. 함수 전체를 다음 코드로 대체합니다.

lib/src/model/data.dart

Future<void> addReview({String restaurantId, Review review}) {
  final restaurant =
      FirebaseFirestore.instance.collection('restaurants').doc(restaurantId);
  final newReview = restaurant.collection('ratings').doc();

  return FirebaseFirestore.instance.runTransaction((Transaction transaction) {
    return transaction
        .get(restaurant)
        .then((DocumentSnapshot doc) => Restaurant.fromSnapshot(doc))
        .then((Restaurant fresh) {
      final newRatings = fresh.numRatings + 1;
      final newAverage =
          ((fresh.numRatings * fresh.avgRating) + review.rating) / newRatings;

      transaction.update(restaurant, {
        'numRatings': newRatings,
        'avgRating': newAverage,
      });

      transaction.set(newReview, {
        'rating': review.rating,
        'text': review.text,
        'userName': review.userName,
        'timestamp': review.timestamp ?? FieldValue.serverTimestamp(),
        'userId': review.userId,
      });
    });
  });
}

위의 함수는 restaurantId로 표시되는 Restaurantfresh 버전을 가져옴으로써 시작되는 트랜잭션을 트리거합니다.

그런 다음, restaurant 문서 참조에서 avgRatingnumRatings의 숫자 값을 업데이트합니다.

동시에 newReview 문서 참조를 통해 새로운 review를 레스토랑의 ratings 하위 컬렉션에 추가합니다.

방금 추가한 코드를 테스트하는 방법은 다음과 같습니다.

  1. flutter가 실행되고 있는 터미널에서 Shift + R을 눌러 앱을 새로고침합니다.
  2. 레스토랑 세부정보 페이지로 이동합니다.
  3. 리뷰를 추가합니다. 그 방법은 다음과 같습니다.
  • 리뷰가 없는 경우 빈 목록에서 ADD SOME 버튼을 클릭합니다.
  • + 플로팅 작업 버튼을 클릭하고 직접 리뷰를 입력합니다.

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

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

Query filteredCollection = FirebaseFirestore.instance
        .collection('restaurants')
        .where('category', isEqualTo: 'Dim Sum');

이름에서 알 수 있듯이 where() 메서드에 의해 쿼리는 필드가 설정된 제한사항을 충족하는 컬렉션의 멤버만 다운로드합니다. 이 경우에는 categoryDim Sum과 동일한 레스토랑만 다운로드합니다.

마찬가지로 다음과 같이 반환된 데이터를 정렬할 수 있습니다.

Query filteredAndSortedCollection = FirebaseFirestore.instance
        .collection('restaurants')
        .where('category', isEqualTo: 'Dim Sum')
        .orderBy('price', descending: true);

orderBy() 메서드에 의해 쿼리는 price 속성을 기준으로 가장 비싼 곳부터 가장 저렴한 곳까지 Dim Sum 레스토랑을 정렬합니다.

앱에서 사용자는 여러 필터를 연결하여 인기순으로 정렬되는 Pizza in San Francisco 또는 Seafood in Los Angeles와 같은 특정 쿼리를 만들 수 있습니다.

이제 사용자가 선택한 여러 기준에 따라 레스토랑을 필터링하는 쿼리를 빌드하는 메서드를 만듭니다.

  1. lib/src/model/data.dart 파일로 돌아갑니다.
  2. loadFilteredRestaurants 함수를 찾습니다.
  3. 함수 전체를 다음 코드로 대체합니다.

lib/src/model/data.dart

Stream<QuerySnapshot> loadFilteredRestaurants(Filter filter) {
  Query collection = FirebaseFirestore.instance.collection('restaurants');
  if (filter.category != null) {
    collection = collection.where('category', isEqualTo: filter.category);
  }
  if (filter.city != null) {
    collection = collection.where('city', isEqualTo: filter.city);
  }
  if (filter.price != null) {
    collection = collection.where('price', isEqualTo: filter.price);
  }
  return collection
      .orderBy(filter.sort ?? 'avgRating', descending: true)
      .limit(50)
      .snapshots();
}

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

flutter가 실행되고 있는 터미널에서 Shift + R을 눌러 브라우저에서 앱을 새로고침합니다.

이제 가격, 도시, 카테고리를 기준으로 필터링해 보세요. 테스트하는 동안 브라우저의 자바스크립트 콘솔에 다음과 비슷한 오류가 표시됩니다.

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 Console에서 올바른 매개변수가 채워진 색인 생성 UI가 자동으로 열립니다.

다음 섹션에서는 Firebase CLI에서 한 번에 이 애플리케이션에 필요한 색인을 작성하고 배포합니다.

앱의 모든 경로를 탐색하고 각 색인 생성 링크를 따르지 않으려는 경우에는 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

몇 분 후 색인이 활성화되고 오류 메시지가 사라집니다. 색인이 완전히 준비되기 전에 색인을 사용하려고 하면 다음과 비슷한 오류가 표시될 수 있습니다.

The query requires an index. That index is currently building and cannot be used yet. See its status here:
https://console.firebase.google.com/project/.../database/firestore/indexes?create_index=...

이 Codelab의 시작 부분에서 읽기 또는 쓰기를 위해 데이터베이스를 완전히 열도록 앱의 보안 규칙을 설정했습니다. 실제 애플리케이션에서는 원치 않는 데이터 액세스 또는 수정을 방지하기 위해 훨씬 더 세분화된 규칙을 설정합니다.

  1. Firebase Console의 개발 섹션에서 Database를 클릭합니다.
  2. Cloud Firestore 섹션에서 규칙 탭을 클릭합니다(또는 Firebase Console로 직접 이동).
  3. 기본값을 다음 규칙으로 바꾸고 게시를 클릭합니다.

firestore.rules

service cloud.firestore {
  match /databases/{database}/documents {
    // Restaurants:
    //   - Authenticated user can read
    //   - Authenticated user can create/update (for demo)
    //   - Validate updates
    //   - Deletes are not allowed
    match /restaurants/{restaurantId} {
      allow read, create: if request.auth != null;
      allow update: if request.auth != null
                    && request.resource.data.name == resource.data.name
      allow delete: if false;

      // Ratings:
      //   - Authenticated user can read
      //   - Authenticated user can create if userId matches
      //   - Deletes and updates are not allowed
      match /ratings/{ratingId} {
        allow read: if request.auth != null;
        allow create: if request.auth != null
                      && request.resource.data.userId == request.auth.uid;
        allow update, delete: if false;
      }
    }
  }
}

이러한 규칙은 클라이언트가 다음과 같이 안전한 변경만 실행할 수 있도록 액세스를 제한합니다.

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

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

firebase deploy --only firestore:rules

flutter build web

지금까지 Flutter 앱의 '디버그' 버전만을 사용했습니다. 이러한 빌드는 디버깅을 더 쉽게 할 수 있는 추가 정보가 포함되어 있으므로 조금 더 느립니다.

앱을 배포하기 전에 프로덕션(prod) 버전을 빌드해야 합니다. Flutter를 사용하면 다음과 같이 build 도구를 이용해 프로덕션용으로 빌드할 수 있습니다.

flutter build web

이렇게 하면 프로덕션용으로 빌드한 모든 애셋이 프로젝트의 build/web 디렉터리에 배치됩니다.

이제 앱을 Firebase에 배포할 준비가 되었습니다.

firebase deploy

프로젝트는 Flutter가 생성하는 애셋을 빌드하고 배포하도록 사전 구성되어 있습니다(프로젝트의 루트에 있는 firebase.json 파일 확인).

다음 명령어를 사용하여 Firebase에 앱의 새 버전을 배포합니다.

firebase init hosting
firebase deploy --only hosting

위의 프로세스는 몇 초밖에 걸리지 않습니다. 이전의 빌드를 정리하고(flutter clean), 앱을 다시 빌드하며(flutter build web), 새로 빌드한 애셋(build/web의 콘텐츠)을 Firebase 호스팅에 배포합니다.

성공 메시지에는 게시된 앱을 인터넷에서 사용할 수 있는 호스팅 URL이 포함됩니다.

축하합니다.

이 Codelab에서는 Firebase 인증 및 Firestore 플러그인을 사용하여 Flutter 웹 앱을 Firebase에 연결하는 방법을 알아보고, Cloud Firestore로 기본 및 고급 읽기 및 쓰기를 실행하고, 보안 규칙으로 데이터 액세스를 보호했습니다.

저장소의 done 분기에서 전체 솔루션을 확인할 수 있습니다.

Dart 및 Flutter에 관해 자세히 알아보려면 다음과 같은 공식 사이트를 참고하세요.

Cloud Firestore에 관해 자세히 알아보려면 다음과 같은 리소스를 참고하세요.

이 Codelab은 다른 Firestore(및 Firebase) 기능을 살펴보기 위한 좋은 시작점이 될 수 있습니다. 추가 과제를 원한다면 다음을 연습해 볼 수 있습니다.

  • 한 번의 요청으로 모든 레스토랑 및 리뷰를 추가하려면 addRestaurantsBatch 메서드에 일괄 쓰기를 사용합니다. 그러면 애플리케이션의 UI가 한 번만 업데이트됩니다.
  • 사용자가 레스토랑에 관한 리뷰를 추가할 때 레스토랑의 별표 평점이 실시간으로 업데이트되도록 RestaurantAppBar 위젯을 수정합니다.
  • 모든 사용자가 현재 익명 사용자이므로 리뷰를 게시하는 사용자의 실제 이름을 검색할 수 있게 하려면 google_sign_in과 함께 firebase_auth를 사용 설정합니다.