Flutter용 Firebase 알아보기

1. 시작하기 전에

이 Codelab에서는 Android 및 iOS용 Flutter 모바일 앱을 만들기 위한 Firebase 의 몇 가지 기본 사항을 알아봅니다.

전제 조건

당신이 배울 것

  • Flutter를 사용하여 Android, iOS, 웹 및 macOS에서 이벤트 RSVP 및 방명록 채팅 앱을 빌드하는 방법입니다.
  • Firebase 인증으로 사용자를 인증하고 Firestore와 데이터를 동기화하는 방법.

Android 앱의 홈 화면

iOS 앱의 홈 화면

필요한 것

다음 장치 중 하나:

  • 컴퓨터에 연결되고 개발자 모드로 설정된 물리적 Android 또는 iOS 기기.
  • iOS 시뮬레이터( Xcode 도구 필요).
  • Android 에뮬레이터( Android Studio 에서 설정 필요).

또한 다음이 필요합니다.

  • Chrome과 같은 원하는 브라우저.
  • Android Studio 또는 Visual Studio Code 와 같은 Dart 및 Flutter 플러그인으로 구성된 선택한 IDE 또는 텍스트 편집기.
  • Flutter 의 최신 stable 버전 또는 가장자리에서 생활하는 것을 즐기는 경우 beta .
  • Firebase 프로젝트 생성 및 관리를 위한 Google 계정.
  • Firebase CLI가 Google 계정에 로그인했습니다.

2. 샘플 코드 받기

GitHub에서 프로젝트의 초기 버전을 다운로드합니다.

  1. 명령줄에서 flutter-codelabs 디렉터리의 GitHub 저장소를 복제합니다.
git clone https://github.com/flutter/codelabs.git flutter-codelabs

flutter-codelabs 디렉토리에는 Codelab 컬렉션의 코드가 포함되어 있습니다. 이 Codelab의 코드는 flutter-codelabs/firebase-get-to-know-flutter 디렉터리에 있습니다. 디렉토리에는 각 단계가 끝날 때 프로젝트가 어떻게 보여야 하는지를 보여주는 일련의 스냅샷이 포함되어 있습니다. 예를 들어, 두 번째 단계에 있습니다.

  1. 두 번째 단계에서 일치하는 파일을 찾습니다.
cd flutter-codelabs/firebase-get-to-know-flutter/step_02

앞으로 건너뛰거나 단계 후에 어떤 것이 어떻게 보여야 하는지 보려면 관심 있는 단계 다음에 이름이 지정된 디렉토리를 살펴보십시오.

시작 앱 가져오기

  • 선호하는 IDE에서 flutter-codelabs/firebase-get-to-know-flutter/step_02 디렉토리를 열거나 가져옵니다. 이 디렉터리에는 아직 작동하지 않는 Flutter 모임 앱으로 구성된 Codelab의 시작 코드가 포함되어 있습니다.

작업이 필요한 파일 찾기

이 앱의 코드는 여러 디렉토리에 분산되어 있습니다. 이러한 기능 분할은 코드를 기능별로 그룹화하기 때문에 작업이 더 쉬워집니다.

  • 다음 파일을 찾습니다.
    • lib/main.dart : 이 파일에는 기본 진입점과 앱 위젯이 포함되어 있습니다.
    • lib/home_page.dart : 이 파일에는 홈 페이지 위젯이 포함되어 있습니다.
    • lib/src/widgets.dart : 이 파일에는 앱의 스타일을 표준화하는 데 도움이 되는 몇 가지 위젯이 포함되어 있습니다. 스타터 앱의 화면을 구성합니다.
    • lib/src/authentication.dart : 이 파일에는 Firebase 이메일 기반 인증을 위한 로그인 사용자 환경을 만들기 위한 일련의 위젯과 함께 인증 의 부분 구현이 포함되어 있습니다. 인증 흐름에 대한 이러한 위젯은 아직 시작 앱에서 사용되지 않지만 곧 추가됩니다.

나머지 앱을 빌드하는 데 필요한 추가 파일을 추가합니다.

lib/main.dart 파일 검토

이 앱은 google_fonts 패키지를 활용하여 앱 전체에서 Roboto를 기본 글꼴로 만듭니다. fonts.google.com을 탐색하고 앱의 다른 부분에서 찾은 글꼴을 사용할 수 있습니다.

Header , ParagraphIconAndDetail 형식의 lib/src/widgets.dart 파일에서 헬퍼 위젯을 사용합니다. 이러한 위젯은 중복 코드를 제거하여 HomePage 에 설명된 페이지 레이아웃의 혼란을 줄입니다. 이것은 또한 일관된 모양과 느낌을 가능하게 합니다.

Android, iOS, 웹 및 macOS에서 앱이 다음과 같이 표시됩니다.

Android 앱의 홈 화면

iOS 앱의 홈 화면

웹에서 앱의 홈 화면

macOS 앱의 홈 화면

3. Firebase 프로젝트 생성 및 구성

이벤트 정보 표시는 손님에게는 좋지만 그 자체로는 아무에게도 유용하지 않습니다. 앱에 몇 가지 동적 기능을 추가해야 합니다. 이렇게 하려면 Firebase를 앱에 연결해야 합니다. Firebase를 시작하려면 Firebase 프로젝트를 만들고 구성해야 합니다.

Firebase 프로젝트 만들기

  1. Firebase 에 로그인합니다.
  2. 콘솔에서 프로젝트 추가 또는 프로젝트 생성 을 클릭합니다.
  3. 프로젝트 이름 필드에 Firebase-Flutter-Codelab을 입력한 다음 계속을 클릭합니다.

4395e4e67c08043a.png

  1. 프로젝트 생성 옵션을 클릭합니다. 메시지가 표시되면 Firebase 약관에 동의하되 이 앱에 Google 애널리틱스를 사용하지 않을 것이므로 Google 애널리틱스 설정을 건너뜁니다.

b7138cde5f2c7b61.png

Firebase 프로젝트에 대한 자세한 내용은 Firebase 프로젝트 이해를 참조하세요.

앱은 웹 앱에 사용할 수 있는 다음 Firebase 제품을 사용합니다.

  • 인증: 사용자가 앱에 로그인할 수 있습니다.
  • Firestore: 구조화된 데이터를 클라우드에 저장하고 데이터가 변경되면 즉시 알림을 받습니다.
  • Firebase 보안 규칙: 데이터베이스를 보호합니다.

이러한 제품 중 일부는 특별한 구성이 필요하거나 Firebase 콘솔에서 사용 설정해야 합니다.

이메일 로그인 인증 활성화

  1. Firebase 콘솔의 프로젝트 개요 창에서 빌드 메뉴를 확장합니다.
  2. 인증 > 시작하기 > 로그인 방법 > 이메일/비밀번호 > 활성화 > 저장을 클릭합니다.

58e3e3e23c2f16a4.png

Firestore 활성화

웹 앱은 Firestore를 사용하여 채팅 메시지를 저장하고 새 채팅 메시지를 받습니다.

Firestore 활성화:

  • 빌드 메뉴에서 Cloud Firestore > 데이터베이스 생성을 클릭합니다.

99e8429832d23fa3.png

  1. 테스트 모드에서 시작을 선택한 다음 보안 규칙에 대한 고지 사항을 읽으십시오. 테스트 모드에서는 개발 중에 데이터베이스에 자유롭게 쓸 수 있습니다.

6be00e26c72ea032.png

  1. 다음을 클릭한 후 데이터베이스 위치를 선택하십시오. 기본값을 사용할 수 있습니다. 나중에 위치를 변경할 수 없습니다.

278656eefcfb0216.png

  1. 사용 을 클릭합니다.

4. Firebase 구성

Flutter와 함께 Firebase를 사용하려면 다음 작업을 완료하여 FlutterFire 라이브러리를 올바르게 사용하도록 Flutter 프로젝트를 구성해야 합니다.

  1. 프로젝트에 FlutterFire 종속성을 추가합니다.
  2. Firebase 프로젝트에 원하는 플랫폼을 등록합니다.
  3. 플랫폼별 구성 파일을 다운로드한 다음 코드에 추가합니다.

Flutter 앱의 최상위 디렉터리에는 각각 iOS 및 Android용 플랫폼별 구성 파일을 포함하는 android , ios , macosweb 하위 디렉터리가 있습니다.

종속성 구성

이 앱에서 사용하는 두 가지 Firebase 제품(인증 및 Firestore)에 FlutterFire 라이브러리를 추가해야 합니다.

  • 명령줄에서 다음 종속 항목을 추가합니다.
$ flutter pub add firebase_core

firebase_core 패키지는 모든 Firebase Flutter 플러그인에 필요한 공통 코드입니다.

$ flutter pub add firebase_auth

firebase_auth 패키지는 인증과의 통합을 가능하게 합니다.

$ flutter pub add cloud_firestore

cloud_firestore 패키지는 Firestore 데이터 스토리지에 대한 액세스를 활성화합니다.

$ flutter pub add provider

firebase_ui_auth 패키지는 인증 흐름을 통해 개발자 속도를 높일 수 있는 일련의 위젯 및 유틸리티를 제공합니다.

$ flutter pub add firebase_ui_auth

필요한 패키지를 추가했지만 Firebase를 적절하게 사용하려면 iOS, Android, macOS 및 웹 러너 프로젝트도 구성해야 합니다. 또한 디스플레이 논리에서 비즈니스 논리를 분리할 수 있는 provider 패키지를 사용합니다.

FlutterFire CLI 설치

FlutterFire CLI는 기본 Firebase CLI에 의존합니다.

  1. 아직 수행하지 않은 경우 컴퓨터에 Firebase CLI를 설치합니다.
  2. FlutterFire CLI를 설치합니다.
$ dart pub global activate flutterfire_cli

설치가 완료되면 flutterfire 명령은 전역에서 사용할 수 있습니다.

앱 구성

CLI는 Firebase 프로젝트 및 선택한 프로젝트 앱에서 정보를 추출하여 특정 플랫폼에 대한 모든 구성을 생성합니다.

앱의 루트에서 configure 명령을 실행합니다.

$ flutterfire configure

구성 명령은 다음 프로세스를 안내합니다.

  1. .firebaserc 파일을 기반으로 하거나 Firebase 콘솔에서 Firebase 프로젝트를 선택합니다.
  2. Android, iOS, macOS 및 웹과 같은 구성을 위한 플랫폼을 결정합니다.
  3. 구성을 추출할 Firebase 앱을 식별합니다. 기본적으로 CLI는 현재 프로젝트 구성을 기반으로 Firebase 앱을 자동으로 일치시키려고 시도합니다.
  4. 프로젝트에서 firebase_options.dart 파일을 생성합니다.

macOS 구성

macOS의 Flutter는 완전한 샌드박스 앱을 빌드합니다. 이 앱은 Firebase 서버와 통신하기 위해 네트워크와 통합되므로 네트워크 클라이언트 권한으로 앱을 구성해야 합니다.

macos/Runner/DebugProfile.entitlements

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
	<key>com.apple.security.app-sandbox</key>
	<true/>
	<key>com.apple.security.cs.allow-jit</key>
	<true/>
	<key>com.apple.security.network.server</key>
	<true/>
  <!-- Add the following two lines -->
	<key>com.apple.security.network.client</key>
	<true/>
</dict>
</plist>

macos/Runner/Release.entitlements

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
	<key>com.apple.security.app-sandbox</key>
	<true/>
  <!-- Add the following two lines -->
	<key>com.apple.security.network.client</key>
	<true/>
</dict>
</plist>

자세한 내용은 Flutter에 대한 데스크톱 지원을 참조하세요.

5. RSVP 기능 추가

앱에 Firebase를 추가했으므로 이제 사용자를 인증 으로 등록하는 RSVP 버튼을 만들 수 있습니다. Android 기본, iOS 기본 및 웹의 경우 사전 빌드된 FirebaseUI Auth 패키지가 있지만 Flutter용으로 이 기능을 빌드해야 합니다.

이전에 검색한 프로젝트에는 대부분의 인증 흐름에 대한 사용자 인터페이스를 구현하는 위젯 세트가 포함되어 있습니다. 비즈니스 로직을 구현하여 인증을 앱과 통합합니다.

Provider 패키지로 비즈니스 로직 추가

provider 패키지를 사용하여 앱의 Flutter 위젯 트리 전체에서 중앙 집중식 앱 상태 개체를 사용할 수 있도록 합니다.

  1. 다음 내용으로 app_state.dart 라는 새 파일을 만듭니다.

lib/app_state.dart

import 'package:firebase_auth/firebase_auth.dart'
    hide EmailAuthProvider, PhoneAuthProvider;
import 'package:firebase_core/firebase_core.dart';
import 'package:firebase_ui_auth/firebase_ui_auth.dart';
import 'package:flutter/material.dart';

import 'firebase_options.dart';

class ApplicationState extends ChangeNotifier {
  ApplicationState() {
    init();
  }

  bool _loggedIn = false;
  bool get loggedIn => _loggedIn;

  Future<void> init() async {
    await Firebase.initializeApp(
        options: DefaultFirebaseOptions.currentPlatform);

    FirebaseUIAuth.configureProviders([
      EmailAuthProvider(),
    ]);

    FirebaseAuth.instance.userChanges().listen((user) {
      if (user != null) {
        _loggedIn = true;
      } else {
        _loggedIn = false;
      }
      notifyListeners();
    });
  }
}

import 문은 Firebase Core 및 Auth를 도입하고, 위젯 트리 전체에서 앱 상태 개체를 사용할 수 있도록 하는 provider 패키지를 가져오고, firebase_ui_auth 패키지의 인증 위젯을 포함합니다.

ApplicationState 애플리케이션 상태 객체는 이 단계에서 인증된 상태에 대한 업데이트가 있음을 위젯 트리에 경고하는 한 가지 주요 책임이 있습니다.

공급자를 사용하여 사용자의 로그인 상태를 앱에 전달합니다. 사용자가 로그인할 수 있도록 하려면 firebase_ui_auth 패키지에서 제공하는 UI를 사용합니다. 이는 앱에서 로그인 화면을 빠르게 부트스트랩할 수 있는 좋은 방법입니다.

인증 흐름 통합

  1. lib/main.dart 파일 상단에서 가져오기를 수정합니다.

lib/main.dart

import 'package:firebase_ui_auth/firebase_ui_auth.dart'; // new
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';               // new
import 'package:google_fonts/google_fonts.dart';
import 'package:provider/provider.dart';                 // new

import 'app_state.dart';                                 // new
import 'home_page.dart';
  1. 앱 상태를 앱 초기화와 연결한 다음 인증 흐름을 HomePage 에 추가합니다.

lib/main.dart

void main() {
  // Modify from here...
  WidgetsFlutterBinding.ensureInitialized();

  runApp(ChangeNotifierProvider(
    create: (context) => ApplicationState(),
    builder: ((context, child) => const App()),
  ));
  // ...to here.
}

main() 함수를 수정하면 공급자 패키지가 ChangeNotifierProvider 위젯을 사용하여 앱 상태 개체의 인스턴스화를 담당하게 됩니다. 이 특정 provider 클래스를 사용하는 이유는 앱 상태 개체가 ChangeNotifier 클래스를 확장하여 provider 패키지가 종속 위젯을 다시 표시할 시기를 알 수 있도록 하기 때문입니다.

  1. GoRouter 구성을 생성하여 FirebaseUI가 제공하는 다양한 화면으로의 탐색을 처리하도록 앱을 업데이트합니다.

lib/main.dart

// Add GoRouter configuration outside the App class
final _router = GoRouter(
  routes: [
    GoRoute(
      path: '/',
      builder: (context, state) => const HomePage(),
      routes: [
        GoRoute(
          path: 'sign-in',
          builder: (context, state) {
            return SignInScreen(
              actions: [
                ForgotPasswordAction(((context, email) {
                  final uri = Uri(
                    path: '/sign-in/forgot-password',
                    queryParameters: <String, String?>{
                      'email': email,
                    },
                  );
                  context.push(uri.toString());
                })),
                AuthStateChangeAction(((context, state) {
                  final user = switch (state) {
                    SignedIn state => state.user,
                    UserCreated state => state.credential.user,
                    _ => null
                  };
                  if (user == null) {
                    return;
                  }
                  if (state is UserCreated) {
                    user.updateDisplayName(user.email!.split('@')[0]);
                  }
                  if (!user.emailVerified) {
                    user.sendEmailVerification();
                    const snackBar = SnackBar(
                        content: Text(
                            'Please check your email to verify your email address'));
                    ScaffoldMessenger.of(context).showSnackBar(snackBar);
                  }
                  context.pushReplacement('/');
                })),
              ],
            );
          },
          routes: [
            GoRoute(
              path: 'forgot-password',
              builder: (context, state) {
                final arguments = state.queryParameters;
                return ForgotPasswordScreen(
                  email: arguments['email'],
                  headerMaxExtent: 200,
                );
              },
            ),
          ],
        ),
        GoRoute(
          path: 'profile',
          builder: (context, state) {
            return ProfileScreen(
              providers: const [],
              actions: [
                SignedOutAction((context) {
                  context.pushReplacement('/');
                }),
              ],
            );
          },
        ),
      ],
    ),
  ],
);
// end of GoRouter configuration

// Change MaterialApp to MaterialApp.router and add the routerConfig
class App extends StatelessWidget {
  const App({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp.router(
      title: 'Firebase Meetup',
      theme: ThemeData(
        buttonTheme: Theme.of(context).buttonTheme.copyWith(
              highlightColor: Colors.deepPurple,
            ),
        primarySwatch: Colors.deepPurple,
        textTheme: GoogleFonts.robotoTextTheme(
          Theme.of(context).textTheme,
        ),
        visualDensity: VisualDensity.adaptivePlatformDensity,
        useMaterial3: true,
      ),
      routerConfig: _router, // new
    );
  }
}

각 화면에는 인증 흐름의 새로운 상태에 따라 관련된 다른 유형의 작업이 있습니다. 인증에서 대부분의 상태가 변경된 후에는 홈 화면이든 프로필과 같은 다른 화면이든 원하는 화면으로 다시 라우팅할 수 있습니다.

  1. HomePage 클래스의 빌드 메서드에서 앱 상태를 AuthFunc 위젯과 통합합니다.

lib/home_page.dart

import 'package:firebase_auth/firebase_auth.dart' // new
    hide EmailAuthProvider, PhoneAuthProvider;    // new
import 'package:flutter/material.dart';           // new
import 'package:provider/provider.dart';          // new

import 'app_state.dart';                          // new
import 'src/authentication.dart';                 // new
import 'src/widgets.dart';

class HomePage extends StatelessWidget {
  const HomePage({super.key});

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Firebase Meetup'),
      ),
      body: ListView(
        children: <Widget>[
          Image.asset('assets/codelab.png'),
          const SizedBox(height: 8),
          const IconAndDetail(Icons.calendar_today, 'October 30'),
          const IconAndDetail(Icons.location_city, 'San Francisco'),
          // Add from here
          Consumer<ApplicationState>(
            builder: (context, appState, _) => AuthFunc(
                loggedIn: appState.loggedIn,
                signOut: () {
                  FirebaseAuth.instance.signOut();
                }),
          ),
          // to here
          const Divider(
            height: 8,
            thickness: 1,
            indent: 8,
            endIndent: 8,
            color: Colors.grey,
          ),
          const Header("What we'll be doing"),
          const Paragraph(
            'Join us for a day full of Firebase Workshops and Pizza!',
          ),
        ],
      ),
    );
  }
}

AuthFunc 위젯을 인스턴스화하고 이를 Consumer 위젯에 래핑합니다. 소비자 위젯은 앱 상태가 변경될 때 provider 패키지를 사용하여 트리의 일부를 다시 빌드할 수 있는 일반적인 방법입니다. AuthFunc 위젯은 테스트하는 보조 위젯입니다.

인증 흐름 테스트

cdf2d25e436bd48d.png

  1. 앱에서 RSVP 버튼을 탭하여 SignInScreen 을 시작합니다.

2a2cd6d69d172369.png

  1. 이메일 주소를 입력하세요. 이미 등록된 경우 시스템에서 암호를 입력하라는 메시지를 표시합니다. 그렇지 않으면 시스템에서 등록 양식을 작성하라는 메시지를 표시합니다.

e5e65065dba36b54.png

  1. 오류 처리 흐름을 확인하려면 6자 미만의 비밀번호를 입력하세요. 등록된 경우 대신에 대한 암호가 표시됩니다.
  2. 오류 처리 흐름을 확인하려면 잘못된 암호를 입력하십시오.
  3. 정확한 비밀번호를 입력하세요. 사용자에게 로그아웃 기능을 제공하는 로그인 환경이 표시됩니다.

4ed811a25b0cf816.png

6. Firestore에 메시지 쓰기

사용자가 오고 있다는 사실을 아는 것은 좋지만 게스트가 앱에서 할 수 있는 다른 작업을 제공해야 합니다. 방명록에 메시지를 남길 수 있다면 어떨까요? 그들은 왜 오고 싶은지 또는 누구를 만나고 싶은지 공유할 수 있습니다.

사용자가 앱에 작성하는 채팅 메시지를 저장하려면 Firestore 를 사용합니다.

데이터 모델

Firestore는 NoSQL 데이터베이스이며 데이터베이스에 저장된 데이터는 컬렉션, 문서, 필드 및 하위 컬렉션으로 분할됩니다. 채팅의 각 메시지를 최상위 컬렉션인 guestbook 컬렉션에 문서로 저장합니다.

7c20dc8424bb1d84.png

Firestore에 메시지 추가

이 섹션에서는 사용자가 데이터베이스에 메시지를 쓰는 기능을 추가합니다. 먼저 양식 필드와 보내기 버튼을 추가한 다음 이러한 요소를 데이터베이스와 연결하는 코드를 추가합니다.

  1. guest_book.dart 라는 새 파일을 만들고 GuestBook 상태 저장 위젯을 추가하여 메시지 필드 및 보내기 버튼의 UI 요소를 구성합니다.

lib/guest_book.dart

import 'dart:async';

import 'package:flutter/material.dart';

import 'src/widgets.dart';

class GuestBook extends StatefulWidget {
  const GuestBook({required this.addMessage, super.key});

  final FutureOr<void> Function(String message) addMessage;

  @override
  State<GuestBook> createState() => _GuestBookState();
}

class _GuestBookState extends State<GuestBook> {
  final _formKey = GlobalKey<FormState>(debugLabel: '_GuestBookState');
  final _controller = TextEditingController();

  @override
  Widget build(BuildContext context) {
    return Padding(
      padding: const EdgeInsets.all(8.0),
      child: Form(
        key: _formKey,
        child: Row(
          children: [
            Expanded(
              child: TextFormField(
                controller: _controller,
                decoration: const InputDecoration(
                  hintText: 'Leave a message',
                ),
                validator: (value) {
                  if (value == null || value.isEmpty) {
                    return 'Enter your message to continue';
                  }
                  return null;
                },
              ),
            ),
            const SizedBox(width: 8),
            StyledButton(
              onPressed: () async {
                if (_formKey.currentState!.validate()) {
                  await widget.addMessage(_controller.text);
                  _controller.clear();
                }
              },
              child: Row(
                children: const [
                  Icon(Icons.send),
                  SizedBox(width: 4),
                  Text('SEND'),
                ],
              ),
            ),
          ],
        ),
      ),
    );
  }
}

여기에 몇 가지 흥미로운 점이 있습니다. 먼저 메시지에 실제로 콘텐츠가 포함되어 있는지 확인하고 콘텐츠가 없는 경우 사용자에게 오류 메시지를 표시할 수 있도록 양식을 인스턴스화합니다. 양식의 유효성을 검사하려면 GlobalKey 를 사용하여 양식 뒤에 있는 양식 상태에 액세스합니다. 키 및 키 사용 방법에 대한 자세한 내용은 키 사용 시기 를 참조하십시오.

또한 위젯이 배치되는 방식에 유의하십시오. TextFormField 포함된 RowRow 가 포함된 StyledButton 있습니다. 또한 TextFormFieldExpanded 위젯에 래핑되어 TextFormField 행의 추가 공간을 채우도록 합니다. 이것이 필요한 이유를 더 잘 이해하려면 제약 조건 이해를 참조하십시오.

이제 사용자가 방명록에 추가할 텍스트를 입력할 수 있는 위젯이 있으므로 화면에 표시해야 합니다.

  1. HomePage 본문을 편집하여 ListView 의 자식 끝에 다음 두 줄을 추가합니다.
const Header("What we'll be doing"),
const Paragraph(
  'Join us for a day full of Firebase Workshops and Pizza!',
),
// Add the following two lines.
const Header('Discussion'),
GuestBook(addMessage: (message) => print(message)),

이것은 위젯을 표시하기에 충분하지만 유용한 작업을 수행하기에는 충분하지 않습니다. 이 코드를 곧 업데이트하여 작동하도록 합니다.

앱 미리보기

채팅이 통합된 Android의 앱 홈 화면

채팅이 통합된 iOS의 앱 홈 화면

채팅이 통합된 웹 앱의 홈 화면

채팅이 통합된 macOS의 앱 홈 화면

사용자가 SEND 를 클릭하면 다음 코드 스니펫이 트리거됩니다. 메시지 입력 필드의 내용을 데이터베이스의 guestbook 모음에 추가합니다. 특히 addMessageToGuestBook 메서드는 guestbook 컬렉션에서 자동으로 생성된 ID를 사용하여 새 문서에 메시지 콘텐츠를 추가합니다.

FirebaseAuth.instance.currentUser.uid 는 인증이 모든 로그인 사용자에게 제공하는 자동 생성 고유 ID에 대한 참조입니다.

  • lib/app_state.dart 파일에서 addMessageToGuestBook 메서드를 추가합니다. 다음 단계에서 이 기능을 사용자 인터페이스와 연결합니다.

lib/app_state.dart

import 'package:cloud_firestore/cloud_firestore.dart'; // new
import 'package:firebase_auth/firebase_auth.dart'
    hide EmailAuthProvider, PhoneAuthProvider;
import 'package:firebase_core/firebase_core.dart';
import 'package:firebase_ui_auth/firebase_ui_auth.dart';
import 'package:flutter/material.dart';

import 'firebase_options.dart';

class ApplicationState extends ChangeNotifier {

  // Current content of ApplicationState elided ...

  // Add from here...
  Future<DocumentReference> addMessageToGuestBook(String message) {
    if (!_loggedIn) {
      throw Exception('Must be logged in');
    }

    return FirebaseFirestore.instance
        .collection('guestbook')
        .add(<String, dynamic>{
      'text': message,
      'timestamp': DateTime.now().millisecondsSinceEpoch,
      'name': FirebaseAuth.instance.currentUser!.displayName,
      'userId': FirebaseAuth.instance.currentUser!.uid,
    });
  }
  // ...to here.
}

UI와 데이터베이스 연결

사용자가 방명록에 추가하려는 텍스트를 입력할 수 있는 UI가 있고 항목을 Firestore에 추가하는 코드가 있습니다. 이제 두 개를 연결하기만 하면 됩니다.

  • lib/home_page.dart 파일에서 HomePage 위젯을 다음과 같이 변경합니다.

lib/home_page.dart

import 'package:firebase_auth/firebase_auth.dart'
    hide EmailAuthProvider, PhoneAuthProvider;
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';

import 'app_state.dart';
import 'guest_book.dart';                         // new
import 'src/authentication.dart';
import 'src/widgets.dart';

class HomePage extends StatelessWidget {
  const HomePage({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Firebase Meetup'),
      ),
      body: ListView(
        children: <Widget>[
          Image.asset('assets/codelab.png'),
          const SizedBox(height: 8),
          const IconAndDetail(Icons.calendar_today, 'October 30'),
          const IconAndDetail(Icons.location_city, 'San Francisco'),
          Consumer<ApplicationState>(
            builder: (context, appState, _) => AuthFunc(
                loggedIn: appState.loggedIn,
                signOut: () {
                  FirebaseAuth.instance.signOut();
                }),
          ),
          const Divider(
            height: 8,
            thickness: 1,
            indent: 8,
            endIndent: 8,
            color: Colors.grey,
          ),
          const Header("What we'll be doing"),
          const Paragraph(
            'Join us for a day full of Firebase Workshops and Pizza!',
          ),
          // Modify from here...
          Consumer<ApplicationState>(
            builder: (context, appState, _) => Column(
              crossAxisAlignment: CrossAxisAlignment.start,
              children: [
                if (appState.loggedIn) ...[
                  const Header('Discussion'),
                  GuestBook(
                    addMessage: (message) =>
                        appState.addMessageToGuestBook(message),
                  ),
                ],
              ],
            ),
          ),
          // ...to here.
        ],
      ),
    );
  }
}

이 단계 시작 시 추가한 두 줄을 전체 구현으로 교체했습니다. 다시 Consumer<ApplicationState> 사용하여 렌더링하는 트리의 일부에서 앱 상태를 사용할 수 있도록 합니다. 이를 통해 UI에 메시지를 입력하는 사람에게 반응하고 데이터베이스에 게시할 수 있습니다. 다음 섹션에서는 추가된 메시지가 데이터베이스에 게시되는지 여부를 테스트합니다.

테스트 전송 메시지

  1. 필요한 경우 앱에 로그인합니다.
  2. Hey there! 와 같은 메시지를 입력합니다. 을 클릭한 다음 보내기 를 클릭합니다.

이 작업은 Firestore 데이터베이스에 메시지를 씁니다. 그러나 다음 단계에서 수행하는 데이터 검색을 구현해야 하기 때문에 실제 Flutter 앱에는 메시지가 표시되지 않습니다. 그러나 Firebase 콘솔의 데이터베이스 대시보드 에서 guestbook 모음에 추가된 메시지를 볼 수 있습니다. 더 많은 메시지를 보내면 guestbook 컬렉션에 더 많은 문서가 추가됩니다. 예를 들어 다음 코드 스니펫을 참조하십시오.

713870af0b3b63c.png

7. 메시지 읽기

게스트가 데이터베이스에 메시지를 작성할 수 있지만 아직 앱에서 메시지를 볼 수 없다는 것은 멋진 일입니다. 그것을 고칠 시간입니다!

메시지 동기화

메시지를 표시하려면 데이터가 변경될 때 트리거하는 리스너를 추가한 다음 새 메시지를 표시하는 UI 요소를 만들어야 합니다. 앱에서 새로 추가된 메시지를 수신 대기하는 앱 상태에 코드를 추가합니다.

  1. 새 파일 guest_book_message.dart 만들고 다음 클래스를 추가하여 Firestore에 저장하는 데이터의 구조화된 보기를 노출합니다.

lib/guest_book_message.dart

class GuestBookMessage {
  GuestBookMessage({required this.name, required this.message});

  final String name;
  final String message;
}
  1. lib/app_state.dart 파일에서 다음 가져오기를 추가합니다.

lib/app_state.dart

import 'dart:async';                                     // new

import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:firebase_auth/firebase_auth.dart'
    hide EmailAuthProvider, PhoneAuthProvider;
import 'package:firebase_core/firebase_core.dart';
import 'package:firebase_ui_auth/firebase_ui_auth.dart';
import 'package:flutter/material.dart';

import 'firebase_options.dart';
import 'guest_book_message.dart';                        // new
  1. 상태 및 게터를 정의하는 ApplicationState 섹션에서 다음 줄을 추가합니다.

lib/app_state.dart

  bool _loggedIn = false;
  bool get loggedIn => _loggedIn;

  // Add from here...
  StreamSubscription<QuerySnapshot>? _guestBookSubscription;
  List<GuestBookMessage> _guestBookMessages = [];
  List<GuestBookMessage> get guestBookMessages => _guestBookMessages;
  // ...to here.
  1. ApplicationState 의 초기화 섹션에서 다음 줄을 추가하여 사용자가 로그인할 때 문서 컬렉션에 대한 쿼리를 구독하고 로그아웃할 때 구독을 취소합니다.

lib/app_state.dart

  Future<void> init() async {
    await Firebase.initializeApp(
        options: DefaultFirebaseOptions.currentPlatform);

    FirebaseUIAuth.configureProviders([
      EmailAuthProvider(),
    ]);
    
    FirebaseAuth.instance.userChanges().listen((user) {
      if (user != null) {
        _loggedIn = true;
        _guestBookSubscription = FirebaseFirestore.instance
            .collection('guestbook')
            .orderBy('timestamp', descending: true)
            .snapshots()
            .listen((snapshot) {
          _guestBookMessages = [];
          for (final document in snapshot.docs) {
            _guestBookMessages.add(
              GuestBookMessage(
                name: document.data()['name'] as String,
                message: document.data()['text'] as String,
              ),
            );
          }
          notifyListeners();
        });
      } else {
        _loggedIn = false;
        _guestBookMessages = [];
        _guestBookSubscription?.cancel();
      }
      notifyListeners();
    });
  }

이 섹션은 guestbook 컬렉션에 대한 쿼리를 구성하고 이 컬렉션에 대한 구독 및 구독 취소를 처리하는 곳이기 때문에 중요합니다. guestbook 컬렉션에서 메시지의 로컬 캐시를 재구성하고 나중에 구독을 취소할 수 있도록 이 구독에 대한 참조를 저장하는 스트림을 듣습니다. 여기에서 많은 일이 진행되고 있으므로 디버거에서 탐색하여 더 명확한 정신 모델을 얻기 위해 어떤 일이 발생하는지 검사해야 합니다. 자세한 내용은 Firestore로 실시간 업데이트 받기를 참조하세요.

  1. lib/guest_book.dart 파일에서 다음 가져오기를 추가합니다.
import 'guest_book_message.dart';
  1. GuestBook 위젯에서 구성의 일부로 메시지 목록을 추가하여 이 변화하는 상태를 사용자 인터페이스에 연결합니다.

lib/guest_book.dart

class GuestBook extends StatefulWidget {
  // Modify the following line:
  const GuestBook({
    super.key, 
    required this.addMessage, 
    required this.messages,
  });

  final FutureOr<void> Function(String message) addMessage;
  final List<GuestBookMessage> messages; // new

  @override
  _GuestBookState createState() => _GuestBookState();
}
  1. _GuestBookState 에서 다음과 같이 build 메서드를 수정하여 이 구성을 노출합니다.

lib/guest_book.dart

class _GuestBookState extends State<GuestBook> {
  final _formKey = GlobalKey<FormState>(debugLabel: '_GuestBookState');
  final _controller = TextEditingController();

  @override
  // Modify from here...
  Widget build(BuildContext context) {
    return Column(
      crossAxisAlignment: CrossAxisAlignment.start,
      children: [
        // ...to here.
        Padding(
          padding: const EdgeInsets.all(8.0),
          child: Form(
            key: _formKey,
            child: Row(
              children: [
                Expanded(
                  child: TextFormField(
                    controller: _controller,
                    decoration: const InputDecoration(
                      hintText: 'Leave a message',
                    ),
                    validator: (value) {
                      if (value == null || value.isEmpty) {
                        return 'Enter your message to continue';
                      }
                      return null;
                    },
                  ),
                ),
                const SizedBox(width: 8),
                StyledButton(
                  onPressed: () async {
                    if (_formKey.currentState!.validate()) {
                      await widget.addMessage(_controller.text);
                      _controller.clear();
                    }
                  },
                  child: Row(
                    children: const [
                      Icon(Icons.send),
                      SizedBox(width: 4),
                      Text('SEND'),
                    ],
                  ),
                ),
              ],
            ),
          ),
        ),
        // Modify from here...
        const SizedBox(height: 8),
        for (var message in widget.messages)
          Paragraph('${message.name}: ${message.message}'),
        const SizedBox(height: 8),
      ],
      // ...to here.
    );
  }
}

build() 메서드의 이전 내용을 Column 위젯으로 래핑한 다음 메시지 목록의 각 메시지에 대한 새 Paragraph 생성하기 위해 Column 의 자식 끝에 대한 컬렉션 을 추가합니다.

  1. messages 매개변수로 GuestBook 올바르게 구성하도록 HomePage 의 본문을 업데이트합니다.

lib/home_page.dart

Consumer<ApplicationState>(
  builder: (context, appState, _) => Column(
    crossAxisAlignment: CrossAxisAlignment.start,
    children: [
      if (appState.loggedIn) ...[
        const Header('Discussion'),
        GuestBook(
          addMessage: (message) =>
              appState.addMessageToGuestBook(message),
          messages: appState.guestBookMessages, // new
        ),
      ],
    ],
  ),
),

테스트 메시지 동기화

Firestore는 데이터베이스에 가입한 클라이언트와 데이터를 자동으로 즉시 동기화합니다.

테스트 메시지 동기화:

  1. 앱에서 이전에 데이터베이스에서 생성한 메시지를 찾습니다.
  2. 새 메시지를 작성합니다. 그들은 즉시 나타납니다.
  3. 여러 창 또는 탭에서 작업 공간을 엽니다. 메시지는 창과 탭에서 실시간으로 동기화됩니다.
  4. 선택 사항: Firebase 콘솔의 데이터베이스 메뉴에서 새 메시지를 수동으로 삭제, 수정 또는 추가합니다. 모든 변경 사항은 UI에 나타납니다.

축하해요! 앱에서 Firestore 문서를 읽습니다!

앱 미리보기

채팅이 통합된 Android의 앱 홈 화면

채팅이 통합된 iOS의 앱 홈 화면

채팅이 통합된 웹 앱의 홈 화면

채팅이 통합된 macOS의 앱 홈 화면

8. 기본 보안 규칙 설정

처음에 테스트 모드를 사용하도록 Firestore를 설정했습니다. 즉, 데이터베이스가 읽기 및 쓰기를 위해 열려 있음을 의미합니다. 그러나 개발 초기 단계에서만 테스트 모드를 사용해야 합니다. 가장 좋은 방법은 앱을 개발할 때 데이터베이스에 대한 보안 규칙을 설정하는 것입니다. 보안은 앱의 구조와 동작에 필수적입니다.

Firebase 보안 규칙을 사용하면 데이터베이스의 문서 및 컬렉션에 대한 액세스를 제어할 수 있습니다. 유연한 규칙 구문을 사용하면 전체 데이터베이스에 대한 모든 쓰기에서 특정 문서에 대한 작업에 이르기까지 모든 항목과 일치하는 규칙을 만들 수 있습니다.

기본 보안 규칙을 설정합니다.

  1. Firebase 콘솔의 개발 메뉴에서 데이터베이스 > 규칙을 클릭합니다. 다음 기본 보안 규칙과 공개 규칙에 대한 경고가 표시되어야 합니다.

7767a2d2e64e7275.png

  1. 앱이 데이터를 쓰는 컬렉션을 식별합니다.

match /databases/{database}/documents 에서 보호하려는 컬렉션을 식별합니다.

rules_version = '2';
service cloud.firestore {
  match /databases/{database}/documents {
    match /guestbook/{entry} {
     // You'll add rules here in the next step.
  }
}

인증 UID를 각 방명록 문서의 필드로 사용했기 때문에 인증 UID를 가져오고 문서에 쓰려고 시도하는 사람이 일치하는 인증 UID를 가지고 있는지 확인할 수 있습니다.

  1. 규칙 세트에 읽기 및 쓰기 규칙을 추가합니다.
rules_version = '2';
service cloud.firestore {
  match /databases/{database}/documents {
    match /guestbook/{entry} {
      allow read: if request.auth.uid != null;
      allow write:
        if request.auth.uid == request.resource.data.userId;
    }
  }
}

이제 로그인한 사용자만 방명록의 메시지를 읽을 수 있지만 메시지 작성자만 메시지를 편집할 수 있습니다.

  1. 모든 예상 필드가 문서에 있는지 확인하기 위해 데이터 유효성 검사를 추가합니다.
rules_version = '2';
service cloud.firestore {
  match /databases/{database}/documents {
    match /guestbook/{entry} {
      allow read: if request.auth.uid != null;
      allow write:
      if request.auth.uid == request.resource.data.userId
          && "name" in request.resource.data
          && "text" in request.resource.data
          && "timestamp" in request.resource.data;
    }
  }
}

9. 보너스 단계: 배운 내용을 연습하세요.

참석자의 RSVP 상태 기록

현재 귀하의 앱은 사람들이 이벤트에 관심이 있을 때만 채팅할 수 있도록 허용합니다. 또한 누군가가 오는지 여부를 알 수 있는 유일한 방법은 그들이 채팅에서 그렇게 말하는 경우입니다.

이 단계에서는 정리하고 얼마나 많은 사람들이 오는지 사람들에게 알립니다. 앱 상태에 몇 가지 기능을 추가합니다. 첫 번째는 로그인한 사용자가 참석 여부를 지정할 수 있는 기능입니다. 두 번째는 얼마나 많은 사람들이 참석하는지 카운터입니다.

  1. UI 코드가 이 상태와 상호 작용할 수 있도록 lib/app_state.dart 파일에서 ApplicationState 의 접근자 섹션에 다음 줄을 추가합니다.

lib/app_state.dart

int _attendees = 0;
int get attendees => _attendees;

Attending _attending = Attending.unknown;
StreamSubscription<DocumentSnapshot>? _attendingSubscription;
Attending get attending => _attending;
set attending(Attending attending) {
  final userDoc = FirebaseFirestore.instance
      .collection('attendees')
      .doc(FirebaseAuth.instance.currentUser!.uid);
  if (attending == Attending.yes) {
    userDoc.set(<String, dynamic>{'attending': true});
  } else {
    userDoc.set(<String, dynamic>{'attending': false});
  }
}
  1. 다음과 같이 ApplicationStateinit() 메서드를 업데이트합니다.

lib/app_state.dart

  Future<void> init() async {
    await Firebase.initializeApp(
        options: DefaultFirebaseOptions.currentPlatform);

    FirebaseUIAuth.configureProviders([
      EmailAuthProvider(),
    ]);

    // Add from here...
    FirebaseFirestore.instance
        .collection('attendees')
        .where('attending', isEqualTo: true)
        .snapshots()
        .listen((snapshot) {
      _attendees = snapshot.docs.length;
      notifyListeners();
    });
    // ...to here.

    FirebaseAuth.instance.userChanges().listen((user) {
      if (user != null) {
        _loginState = ApplicationLoginState.loggedIn;
        _guestBookSubscription = FirebaseFirestore.instance
            .collection('guestbook')
            .orderBy('timestamp', descending: true)
            .snapshots()
            .listen((snapshot) {
          _guestBookMessages = [];
          for (final document in snapshot.docs) {
            _guestBookMessages.add(
              GuestBookMessage(
                name: document.data()['name'] as String,
                message: document.data()['text'] as String,
              ),
            );
          }
          notifyListeners();
        });
        // Add from here...
        _attendingSubscription = FirebaseFirestore.instance
            .collection('attendees')
            .doc(user.uid)
            .snapshots()
            .listen((snapshot) {
          if (snapshot.data() != null) {
            if (snapshot.data()!['attending'] as bool) {
              _attending = Attending.yes;
            } else {
              _attending = Attending.no;
            }
          } else {
            _attending = Attending.unknown;
          }
          notifyListeners();
        });
        // ...to here.
      } else {
        _loginState = ApplicationLoginState.loggedOut;
        _guestBookMessages = [];
        _guestBookSubscription?.cancel();
        _attendingSubscription?.cancel(); // new
      }
      notifyListeners();
    });
  }

이 코드는 참석자 수를 확인하기 위해 항상 구독하는 쿼리를 추가하고 사용자가 참석하는지 여부를 확인하기 위해 사용자가 로그인한 동안에만 활성화되는 두 번째 쿼리를 추가합니다.

  1. lib/app_state.dart 파일의 맨 위에 다음 열거를 추가합니다.

lib/app_state.dart

enum Attending { yes, no, unknown }
  1. yes_no_selection.dart 새 파일을 만들고 라디오 버튼처럼 작동하는 새 위젯을 정의합니다.

lib/yes_no_selection.dart

import 'package:flutter/material.dart';

import 'app_state.dart';
import 'src/widgets.dart';

class YesNoSelection extends StatelessWidget {
  const YesNoSelection(
      {super.key, required this.state, required this.onSelection});
  final Attending state;
  final void Function(Attending selection) onSelection;

  @override
  Widget build(BuildContext context) {
    switch (state) {
      case Attending.yes:
        return Padding(
          padding: const EdgeInsets.all(8.0),
          child: Row(
            children: [
              FilledButton(
                onPressed: () => onSelection(Attending.yes),
                child: const Text('YES'),
              ),
              const SizedBox(width: 8),
              TextButton(
                onPressed: () => onSelection(Attending.no),
                child: const Text('NO'),
              ),
            ],
          ),
        );
      case Attending.no:
        return Padding(
          padding: const EdgeInsets.all(8.0),
          child: Row(
            children: [
              TextButton(
                onPressed: () => onSelection(Attending.yes),
                child: const Text('YES'),
              ),
              const SizedBox(width: 8),
              FilledButton(
                onPressed: () => onSelection(Attending.no),
                child: const Text('NO'),
              ),
            ],
          ),
        );
      default:
        return Padding(
          padding: const EdgeInsets.all(8.0),
          child: Row(
            children: [
              StyledButton(
                onPressed: () => onSelection(Attending.yes),
                child: const Text('YES'),
              ),
              const SizedBox(width: 8),
              StyledButton(
                onPressed: () => onSelection(Attending.no),
                child: const Text('NO'),
              ),
            ],
          ),
        );
    }
  }
}

예도 아니요도 선택되지 않은 불확실한 상태에서 시작됩니다. 사용자가 참석 여부를 선택하면 해당 옵션이 채워진 버튼으로 강조 표시되고 다른 옵션은 평면 렌더링으로 사라집니다.

  1. HomePagebuild() 메서드를 업데이트하여 YesNoSelection 활용하고, 로그인한 사용자가 참석 여부를 지정할 수 있도록 하고, 이벤트 참석자 수를 표시합니다.

lib/home_page.dart

Consumer<ApplicationState>(
  builder: (context, appState, _) => Column(
    crossAxisAlignment: CrossAxisAlignment.start,
    children: [
      // Add from here...
      switch (appState.attendees) {
        1 => const Paragraph('1 person going'),
        >= 2 => Paragraph('${appState.attendees} people going'),
        _ => const Paragraph('No one going'),
      },
      // ...to here.
      if (appState.loggedIn) ...[
        // Add from here...
        YesNoSelection(
          state: appState.attending,
          onSelection: (attending) => appState.attending = attending,
        ),
        // ...to here.
        const Header('Discussion'),
        GuestBook(
          addMessage: (message) =>
              appState.addMessageToGuestBook(message),
          messages: appState.guestBookMessages,
        ),
      ],
    ],
  ),
),

규칙 추가

이미 몇 가지 규칙을 설정했으므로 버튼으로 추가하는 데이터는 거부됩니다. attendees 컬렉션에 추가할 수 있도록 규칙을 업데이트해야 합니다.

  1. attendees 컬렉션에서 문서 이름으로 사용한 인증 UID를 가져오고 제출자의 uid 작성 중인 문서와 동일한지 확인합니다.
rules_version = '2';
service cloud.firestore {
  match /databases/{database}/documents {
    // ... //
    match /attendees/{userId} {
      allow read: if true;
      allow write: if request.auth.uid == userId;
    }
  }
}

참석자 목록에는 비공개 데이터가 없기 때문에 모든 사람이 참석자 목록을 읽을 수 있지만 생성자만 업데이트할 수 있습니다.

  1. 모든 예상 필드가 문서에 있는지 확인하기 위해 데이터 유효성 검사를 추가합니다.
rules_version = '2';
service cloud.firestore {
  match /databases/{database}/documents {
    // ... //
    match /attendees/{userId} {
      allow read: if true;
      allow write: if request.auth.uid == userId
          && "attending" in request.resource.data;

    }
  }
}
  1. 선택 사항: 앱에서 버튼을 클릭하여 Firebase 콘솔의 Firestore 대시보드에서 결과를 확인합니다.

앱 미리보기

Android 앱의 홈 화면

iOS 앱의 홈 화면

웹에서 앱의 홈 화면

macOS 앱의 홈 화면

10. 축하합니다!

Firebase를 사용하여 대화형 실시간 웹 앱을 만들었습니다.

더 알아보기