1. 시작하기 전에
이 Codelab에서는 Android 및 iOS용 Flutter 모바일 앱을 만들기 위한 Firebase 의 몇 가지 기본 사항을 알아봅니다.
전제 조건
- Flutter에 대한 친숙함
- 플러터 SDK
- 원하는 텍스트 편집기
당신이 배울 것
- Flutter를 사용하여 Android, iOS, 웹 및 macOS에서 이벤트 RSVP 및 방명록 채팅 앱을 빌드하는 방법입니다.
- Firebase 인증으로 사용자를 인증하고 Firestore와 데이터를 동기화하는 방법.
필요한 것
다음 장치 중 하나:
- 컴퓨터에 연결되고 개발자 모드로 설정된 물리적 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에서 프로젝트의 초기 버전을 다운로드합니다.
- 명령줄에서
flutter-codelabs
디렉터리의 GitHub 저장소 를 복제합니다.
git clone https://github.com/flutter/codelabs.git flutter-codelabs
flutter-codelabs
디렉토리에는 Codelab 컬렉션의 코드가 포함되어 있습니다. 이 Codelab의 코드는 flutter-codelabs/firebase-get-to-know-flutter
디렉터리에 있습니다. 디렉토리에는 각 단계가 끝날 때 프로젝트가 어떻게 보여야 하는지를 보여주는 일련의 스냅샷이 포함되어 있습니다. 예를 들어, 두 번째 단계에 있습니다.
- 두 번째 단계에서 일치하는 파일을 찾습니다.
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/src/widgets.dart
: 이 파일에는 앱의 스타일을 표준화하는 데 도움이 되는 몇 가지 위젯이 포함되어 있습니다. 스타터 앱의 화면을 구성합니다. -
lib/src/authentication.dart
: 이 파일에는 Firebase 이메일 기반 인증을 위한 로그인 사용자 환경을 만들기 위한 일련의 위젯과 함께 인증 의 부분 구현이 포함되어 있습니다. 인증 흐름에 대한 이러한 위젯은 아직 시작 앱에서 사용되지 않지만 곧 추가됩니다.
-
나머지 앱을 빌드하는 데 필요한 추가 파일을 추가합니다.
lib/main.dart
파일 검토
이 앱은 google_fonts
패키지를 활용하여 앱 전체에서 Roboto를 기본 글꼴로 만듭니다. fonts.google.com 을 탐색하고 앱의 다른 부분에서 찾은 글꼴을 사용할 수 있습니다.
Header
, Paragraph
및 IconAndDetail
형식의 lib/src/widgets.dart
파일에서 헬퍼 위젯을 사용합니다. 이러한 위젯은 중복 코드를 제거하여 HomePage
에 설명된 페이지 레이아웃의 혼란을 줄입니다. 이것은 또한 일관된 모양과 느낌을 가능하게 합니다.
Android, iOS, 웹 및 macOS에서 앱이 다음과 같이 표시됩니다.
3. Firebase 프로젝트 생성 및 구성
이벤트 정보 표시는 손님에게는 좋지만 그 자체로는 아무에게도 유용하지 않습니다. 앱에 몇 가지 동적 기능을 추가해야 합니다. 이렇게 하려면 Firebase를 앱에 연결해야 합니다. Firebase를 시작하려면 Firebase 프로젝트를 만들고 구성해야 합니다.
Firebase 프로젝트 만들기
- Firebase 에 로그인합니다.
- 콘솔에서 프로젝트 추가 또는 프로젝트 생성 을 클릭합니다.
- 프로젝트 이름 필드에 Firebase-Flutter-Codelab 을 입력한 다음 계속 을 클릭합니다.
- 프로젝트 생성 옵션을 클릭합니다. 메시지가 표시되면 Firebase 약관에 동의하되 이 앱에 Google 애널리틱스를 사용하지 않을 것이므로 Google 애널리틱스 설정을 건너뜁니다.
Firebase 프로젝트에 대한 자세한 내용은 Firebase 프로젝트 이해 를 참조하세요.
앱은 웹 앱에 사용할 수 있는 다음 Firebase 제품을 사용합니다.
- 인증: 사용자가 앱에 로그인할 수 있습니다.
- Firestore: 구조화된 데이터를 클라우드에 저장하고 데이터가 변경되면 즉시 알림을 받습니다.
- Firebase 보안 규칙: 데이터베이스를 보호합니다.
이러한 제품 중 일부는 특별한 구성이 필요하거나 Firebase 콘솔에서 사용 설정해야 합니다.
이메일 로그인 인증 활성화
- Firebase 콘솔의 프로젝트 개요 창에서 빌드 메뉴를 확장합니다.
- 인증 > 시작하기 > 로그인 방법 > 이메일/비밀번호 > 활성화 > 저장 을 클릭합니다.
Firestore 활성화
웹 앱은 Firestore 를 사용하여 채팅 메시지를 저장하고 새 채팅 메시지를 받습니다.
Firestore 활성화:
- 빌드 메뉴에서 Cloud Firestore > 데이터베이스 생성 을 클릭합니다.
- 테스트 모드에서 시작을 선택한 다음 보안 규칙에 대한 고지 사항을 읽으십시오. 테스트 모드에서는 개발 중에 데이터베이스에 자유롭게 쓸 수 있습니다.
- 다음 을 클릭한 후 데이터베이스 위치를 선택하십시오. 기본값을 사용할 수 있습니다. 나중에 위치를 변경할 수 없습니다.
- 사용 을 클릭합니다.
4. Firebase 구성
Flutter와 함께 Firebase를 사용하려면 다음 작업을 완료하여 FlutterFire
라이브러리를 올바르게 사용하도록 Flutter 프로젝트를 구성해야 합니다.
- 프로젝트에
FlutterFire
종속성을 추가합니다. - Firebase 프로젝트에 원하는 플랫폼을 등록합니다.
- 플랫폼별 구성 파일을 다운로드한 다음 코드에 추가합니다.
Flutter 앱의 최상위 디렉터리에는 각각 iOS 및 Android용 플랫폼별 구성 파일을 포함하는 android
, ios
, macos
및 web
하위 디렉터리가 있습니다.
종속성 구성
이 앱에서 사용하는 두 가지 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에 의존합니다.
- 아직 수행하지 않은 경우 컴퓨터에 Firebase CLI 를 설치합니다.
- FlutterFire CLI를 설치합니다.
$ dart pub global activate flutterfire_cli
설치가 완료되면 flutterfire
명령은 전역에서 사용할 수 있습니다.
앱 구성
CLI는 Firebase 프로젝트 및 선택한 프로젝트 앱에서 정보를 추출하여 특정 플랫폼에 대한 모든 구성을 생성합니다.
앱의 루트에서 configure
명령을 실행합니다.
$ flutterfire configure
구성 명령은 다음 프로세스를 안내합니다.
-
.firebaserc
파일을 기반으로 하거나 Firebase 콘솔에서 Firebase 프로젝트를 선택합니다. - Android, iOS, macOS 및 웹과 같은 구성을 위한 플랫폼을 결정합니다.
- 구성을 추출할 Firebase 앱을 식별합니다. 기본적으로 CLI는 현재 프로젝트 구성을 기반으로 Firebase 앱을 자동으로 일치시키려고 시도합니다.
- 프로젝트에서
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 위젯 트리 전체에서 중앙 집중식 앱 상태 개체를 사용할 수 있도록 합니다.
-
lib/main.dart
파일 상단에서 가져오기를 수정합니다.
lib/main.dart
import 'dart:async'; // new
import 'package:firebase_auth/firebase_auth.dart' // new
hide EmailAuthProvider, PhoneAuthProvider; // new
import 'package:firebase_core/firebase_core.dart'; // new
import 'package:firebase_ui_auth/firebase_ui_auth.dart'; // new
import 'package:flutter/material.dart';
import 'package:google_fonts/google_fonts.dart';
import 'package:provider/provider.dart'; // new
import 'firebase_options.dart'; // new
import 'src/authentication.dart'; // new
import 'src/widgets.dart';
import
문은 Firebase Core 및 Auth를 도입하고, 위젯 트리 전체에서 앱 상태 개체를 사용할 수 있도록 하는 provider
패키지를 가져오고, firebase_ui_auth
패키지의 인증 위젯을 포함합니다.
이 ApplicationState
애플리케이션 상태 객체는 이 단계에서 인증된 상태에 대한 업데이트가 있음을 위젯 트리에 경고하는 한 가지 주요 책임이 있습니다.
-
lib/main.dart
파일 끝에 다음 클래스를 추가합니다.
lib/main.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();
});
}
}
공급자를 사용하여 사용자의 로그인 상태를 앱에 전달합니다. 사용자가 로그인할 수 있도록 하려면 firebase_ui_auth
패키지에서 제공하는 UI를 사용합니다. 이는 앱에서 로그인 화면을 빠르게 부트스트랩할 수 있는 좋은 방법입니다.
인증 흐름 통합
- 앱 상태를 앱 초기화와 연결한 다음 인증 흐름을
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
패키지가 종속 위젯을 다시 표시할 시기를 알 수 있도록 하기 때문입니다.
- FirebaseUI가 제공하는 다양한 화면으로의 탐색을 처리하도록 앱을 업데이트하세요.
lib/main.dart
class App extends StatelessWidget {
const App({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
//Start adding here
initialRoute: '/home',
routes: {
'/home': (context) {
return const HomePage();
},
'/sign-in': ((context) {
return SignInScreen(
actions: [
ForgotPasswordAction(((context, email) {
Navigator.of(context)
.pushNamed('/forgot-password', arguments: {'email': email});
})),
AuthStateChangeAction(((context, state) {
if (state is SignedIn || state is UserCreated) {
var user = (state is SignedIn)
? state.user
: (state as UserCreated).credential.user;
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);
}
Navigator.of(context).pushReplacementNamed('/home');
}
})),
],
);
}),
'/forgot-password': ((context) {
final arguments = ModalRoute.of(context)?.settings.arguments
as Map<String, dynamic>?;
return ForgotPasswordScreen(
email: arguments?['email'] as String,
headerMaxExtent: 200,
);
}),
'/profile': ((context) {
return ProfileScreen(
providers: [],
actions: [
SignedOutAction(
((context) {
Navigator.of(context).pushReplacementNamed('/home');
}),
),
],
);
})
},
// end adding here
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,
),
);
}
}
각 화면에는 인증 흐름의 새로운 상태에 따라 관련된 다른 유형의 작업이 있습니다. 인증에서 대부분의 상태가 변경된 후에는 홈 화면이든 프로필과 같은 다른 화면이든 원하는 화면으로 다시 라우팅할 수 있습니다.
-
HomePage
클래스의 빌드 메서드에서 앱 상태를AuthFunc
위젯과 통합합니다.
lib/main.dart
class HomePage extends StatelessWidget {
const HomePage({super.key});
@override
Widget build(BuildContext context) {
return Consumer<ApplicationState>(
builder: (context, appState, child) => 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
위젯은 테스트하는 보조 위젯입니다.
인증 흐름 테스트
- 앱에서 RSVP 버튼을 탭하여
SignInScreen
을 시작합니다.
- 이메일 주소를 입력하세요. 이미 등록된 경우 시스템에서 암호를 입력하라는 메시지를 표시합니다. 그렇지 않으면 시스템에서 등록 양식을 작성하라는 메시지를 표시합니다.
- 오류 처리 흐름을 확인하려면 6자 미만의 비밀번호를 입력하세요. 등록된 경우 대신에 대한 암호가 표시됩니다.
- 오류 처리 흐름을 확인하려면 잘못된 암호를 입력하십시오.
- 정확한 비밀번호를 입력하세요. 사용자에게 로그아웃 기능을 제공하는 로그인 환경이 표시됩니다.
6. Firestore에 메시지 쓰기
사용자가 오고 있다는 사실을 아는 것은 좋지만 게스트가 앱에서 할 수 있는 다른 작업을 제공해야 합니다. 방명록에 메시지를 남길 수 있다면 어떨까요? 그들은 왜 오고 싶은지 또는 누구를 만나고 싶은지 공유할 수 있습니다.
사용자가 앱에 작성하는 채팅 메시지를 저장하려면 Firestore 를 사용합니다.
데이터 모델
Firestore는 NoSQL 데이터베이스이며 데이터베이스에 저장된 데이터는 컬렉션, 문서, 필드 및 하위 컬렉션으로 분할됩니다. 채팅의 각 메시지를 최상위 컬렉션인 gustbook
컬렉션에 문서로 저장합니다.
Firestore에 메시지 추가
이 섹션에서는 사용자가 데이터베이스에 메시지를 쓰는 기능을 추가합니다. 먼저 양식 필드와 보내기 버튼을 추가한 다음 이러한 요소를 데이터베이스와 연결하는 코드를 추가합니다.
-
lib/main.dart
파일에서cloud_firestore
및dart:async
패키지에 대한 가져오기를 추가합니다.
lib/main.dart
import 'dart:async'; // new
import 'package:cloud_firestore/cloud_firestore.dart'; // new
import 'package:firebase_auth/firebase_auth.dart';
import 'package:firebase_core/firebase_core.dart';
import 'package:flutter/material.dart';
import 'package:google_fonts/google_fonts.dart';
import 'package:provider/provider.dart';
import 'firebase_options.dart';
import 'src/authentication.dart';
import 'src/widgets.dart';
-
lib/main.dart
파일 끝에GuestBook
상태 저장 위젯을 추가하여 메시지 필드 및 보내기 버튼의 UI 요소를 구성합니다.
lib/main.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
가 포함된 Row
와 Row
가 포함된 StyledButton
이 있습니다. 또한 TextFormField
는 Expanded
위젯에 래핑되어 TextFormField
가 행의 추가 공간을 채우도록 합니다. 이것이 필요한 이유를 더 잘 이해하려면 제약 조건 이해 를 참조하십시오.
이제 사용자가 방명록에 추가할 텍스트를 입력할 수 있는 위젯이 있으므로 화면에 표시해야 합니다.
-
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)),
이것은 위젯을 표시하기에 충분하지만 유용한 작업을 수행하기에는 충분하지 않습니다. 이 코드를 곧 업데이트하여 작동하도록 합니다.
앱 미리보기
사용자가 SEND 를 클릭하면 다음 코드 스니펫이 트리거됩니다. 메시지 입력 필드의 내용을 데이터베이스의 guestbook
모음에 추가합니다. 특히 addMessageToGuestBook
메서드는 guestbook
컬렉션에서 자동으로 생성된 ID를 사용하여 새 문서에 메시지 콘텐츠를 추가합니다.
FirebaseAuth.instance.currentUser.uid
는 인증이 모든 로그인 사용자에게 제공하는 자동 생성 고유 ID에 대한 참조입니다.
-
lib/main.dart
파일에서addMessageToGuestBook
메서드를 추가합니다. 다음 단계에서 이 기능을 사용자 인터페이스와 연결합니다.
lib/main.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/main.dart
파일에서HomePage
위젯을 다음과 같이 변경합니다.
lib/main.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에 메시지를 입력하는 사람에게 반응하고 데이터베이스에 게시할 수 있습니다. 다음 섹션에서는 추가된 메시지가 데이터베이스에 게시되는지 여부를 테스트합니다.
테스트 전송 메시지
- 필요한 경우 앱에 로그인합니다.
-
Hey there!
와 같은 메시지를 입력합니다. 을 클릭한 다음 보내기 를 클릭합니다.
이 작업은 Firestore 데이터베이스에 메시지를 씁니다. 그러나 다음 단계에서 수행하는 데이터 검색을 구현해야 하기 때문에 실제 Flutter 앱에는 메시지가 표시되지 않습니다. 그러나 Firebase 콘솔의 데이터베이스 대시보드 에서 guestbook
모음에 추가된 메시지를 볼 수 있습니다. 더 많은 메시지를 보내면 guestbook
컬렉션에 더 많은 문서가 추가됩니다. 예를 들어 다음 코드 스니펫을 참조하십시오.
7. 메시지 읽기
게스트가 데이터베이스에 메시지를 작성할 수 있지만 아직 앱에서 메시지를 볼 수 없다는 것은 멋진 일입니다. 그것을 고칠 시간입니다!
메시지 동기화
메시지를 표시하려면 데이터가 변경될 때 트리거하는 리스너를 추가한 다음 새 메시지를 표시하는 UI 요소를 만들어야 합니다. 앱에서 새로 추가된 메시지를 수신 대기하는 앱 상태에 코드를 추가합니다.
-
GuestBook
위젯 앞의lib/main.dart
파일에서 다음 클래스를 추가하여 Firestore에 저장하는 데이터의 구조화된 보기를 노출합니다.
lib/main.dart
class GuestBookMessage {
GuestBookMessage({required this.name, required this.message});
final String name;
final String message;
}
- 상태 및 게터를 정의하는
ApplicationState
섹션에서 다음 줄을 추가합니다.
lib/main.dart
bool _loggedIn = false;
bool get loggedIn => _loggedIn;
// Add from here...
StreamSubscription<QuerySnapshot>? _guestBookSubscription;
List<GuestBookMessage> _guestBookMessages = [];
List<GuestBookMessage> get guestBookMessages => _guestBookMessages;
// ...to here.
-
ApplicationState
의 초기화 섹션에서 다음 줄을 추가하여 사용자가 로그인할 때 문서 컬렉션에 대한 쿼리를 구독하고 로그아웃할 때 구독을 취소합니다.
lib/main.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로 실시간 업데이트 받기 를 참조하세요.
-
GuestBook
위젯에서 구성의 일부로 메시지 목록을 추가하여 이 변화하는 상태를 사용자 인터페이스에 연결합니다.
lib/main.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();
}
-
_GuestBookState
에서 다음과 같이build
메서드를 수정하여 이 구성을 노출합니다.
lib/main.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
위젯으로 래핑한 다음 Column
의 자식 끝에 컬렉션을 추가하여 메시지 목록의 각 메시지에 대한 새 Paragraph
를 생성합니다.
- 새
messages
매개변수로GuestBook
을 올바르게 구성하도록HomePage
의 본문을 업데이트합니다.
lib/main.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는 데이터베이스에 가입한 클라이언트와 데이터를 자동으로 즉시 동기화합니다.
테스트 메시지 동기화:
- 앱에서 이전에 데이터베이스에서 생성한 메시지를 찾습니다.
- 새 메시지를 작성합니다. 그들은 즉시 나타납니다.
- 여러 창 또는 탭에서 작업 공간을 엽니다. 메시지는 창과 탭에서 실시간으로 동기화됩니다.
- 선택 사항: Firebase 콘솔의 데이터베이스 메뉴에서 새 메시지를 수동으로 삭제, 수정 또는 추가합니다. 모든 변경 사항은 UI에 나타납니다.
축하합니다! 앱에서 Firestore 문서를 읽습니다!
앱 미리보기
8. 기본 보안 규칙 설정
처음에 테스트 모드를 사용하도록 Firestore를 설정했습니다. 즉, 데이터베이스가 읽기 및 쓰기를 위해 열려 있음을 의미합니다. 그러나 개발 초기 단계에서만 테스트 모드를 사용해야 합니다. 가장 좋은 방법은 앱을 개발할 때 데이터베이스에 대한 보안 규칙을 설정하는 것입니다. 보안은 앱의 구조와 동작에 필수적입니다.
Firebase 보안 규칙을 사용하면 데이터베이스의 문서 및 컬렉션에 대한 액세스를 제어할 수 있습니다. 유연한 규칙 구문을 사용하면 전체 데이터베이스에 대한 모든 쓰기에서 특정 문서에 대한 작업에 이르기까지 모든 항목과 일치하는 규칙을 만들 수 있습니다.
기본 보안 규칙을 설정합니다.
- Firebase 콘솔의 개발 메뉴에서 데이터베이스 > 규칙 을 클릭합니다. 다음 기본 보안 규칙과 공개 규칙에 대한 경고가 표시되어야 합니다.
- 앱이 데이터를 쓰는 컬렉션을 식별합니다.
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를 가지고 있는지 확인할 수 있습니다.
- 규칙 세트에 읽기 및 쓰기 규칙을 추가합니다.
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;
}
}
}
이제 로그인한 사용자만 방명록의 메시지를 읽을 수 있지만 메시지 작성자만 메시지를 편집할 수 있습니다.
- 모든 예상 필드가 문서에 있는지 확인하기 위해 데이터 유효성 검사를 추가합니다.
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 상태 기록
현재 귀하의 앱은 사람들이 이벤트에 관심이 있을 때만 채팅할 수 있도록 허용합니다. 또한 누군가가 오는지 여부를 알 수 있는 유일한 방법은 그들이 채팅에서 그렇게 말하는 경우입니다.
이 단계에서는 정리하고 얼마나 많은 사람들이 오는지 사람들에게 알립니다. 앱 상태에 몇 가지 기능을 추가합니다. 첫 번째는 로그인한 사용자가 참석 여부를 지정할 수 있는 기능입니다. 두 번째는 얼마나 많은 사람들이 참석하는지 카운터입니다.
- UI 코드가 이 상태와 상호 작용할 수 있도록
lib/main.dart
파일에서 접근자 섹션에 다음 줄을 추가합니다.
lib/main.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});
}
}
- 다음과 같이
ApplicationState
의init()
메서드를 업데이트합니다.
lib/main.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();
});
}
이 코드는 참석자 수를 확인하기 위해 항상 구독하는 쿼리를 추가하고 사용자가 참석하는지 확인하기 위해 사용자가 로그인한 동안에만 활성화되는 두 번째 쿼리를 추가합니다.
-
GuestBookMessage
선언 뒤에 다음 열거형을 추가합니다.
lib/main.dart
enum Attending { yes, no, unknown }
- 이전 라디오 버튼처럼 작동하는 새 위젯을 정의합니다.
lib/main.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: [
ElevatedButton(
style: ElevatedButton.styleFrom(elevation: 0),
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),
ElevatedButton(
style: ElevatedButton.styleFrom(elevation: 0),
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'),
),
],
),
);
}
}
}
예도 아니요 도 선택 되지 않은 불확실한 상태에서 시작됩니다. 사용자가 참석 여부를 선택하면 해당 옵션이 채워진 버튼으로 강조 표시되고 다른 옵션은 평면 렌더링으로 사라집니다.
-
HomePage
의build()
메서드를 업데이트하여YesNoSelection
을 활용하고, 로그인한 사용자가 참석 여부를 지정할 수 있도록 하고, 이벤트 참석자 수를 표시합니다.
lib/main.dart
Consumer<ApplicationState>(
builder: (context, appState, _) => Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Add from here...
if (appState.attendees >= 2)
Paragraph('${appState.attendees} people going')
else if (appState.attendees == 1)
const Paragraph('1 person going')
else
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
컬렉션에 추가할 수 있도록 규칙을 업데이트해야 합니다.
-
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;
}
}
}
참석자 목록에는 비공개 데이터가 없기 때문에 모든 사람이 참석자 목록을 읽을 수 있지만 작성자만 업데이트할 수 있습니다.
- 모든 예상 필드가 문서에 있는지 확인하기 위해 데이터 유효성 검사를 추가합니다.
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;
}
}
}
- 선택사항: 앱에서 버튼을 클릭하여 Firebase 콘솔의 Firestore 대시보드에서 결과를 확인합니다.
앱 미리보기
10. 축하합니다!
Firebase를 사용하여 대화형 실시간 웹 앱을 만들었습니다.