[개발일지] flutter 앱개발 5주차
<스파르타코딩 flutter 강의 수강일지>
5주차: Firebase 로그인 구현 및 데이터베이스 연동
1. Firebase 사용하기 위한 패키지
// 패키지 설치법
flutter pub add <패키지명>
// 패키지 목록
firebase_core : firebase 사용을 위한 필수 패키지
firebase_auth : firebase 로그인 패키지
cloud_firestore : firebase 데이터 베이스 패키지
provider : 상태 관리 패키지
강의에서는 패키지를 한꺼번에 pubspec.yaml 파일에 추가했지만, 버전 문제로 하나씩 직접 설치하는것이 좋을 것 같다.
2. Firebase 연결
https://console.firebase.google.com/u/1/?hl=ko
로그인 - Google 계정
이메일 또는 휴대전화
accounts.google.com
Firebase 콘솔에 들어가 새로운 프로젝트를 만든다.
진행 중에 안내되는바와 같이 Android, iOS 앱 추가를 완료한 후, 패키지를 설치하면 된다.
몇몇의 과정은 앱 추가 중에 생략해도 되는데, 이는 패키지를 설치하기 때문이다.
*생략 해도 되는 과정(iOS)
3) Firebase SDK 추가
4) 초기화 코드 추가
설치가 완료되면, firebase_core패키지의 함수를 맨 처음에 실행해준다.
// Firebase를 사용하기 위한 실행 함수
void main() async {
WidgetsFlutterBinding.ensureInitialized(); // main 함수에서 async 사용하기 위함
await Firebase.initializeApp(); // firebase 앱 시작
runApp(const MyApp());
}
*Firebase에 연결 후에, 나의 iOS 에뮬레이터가 동작하지 않고, 아래와 같은 오류가 발생했다.
이 문제는, Xcode 시뮬레이터의 기기를 변경하여 해결했다. (iPhone 13 Pro로 변경)
찾아보니, API에 관련된 버전과 기기의 버전이 맞지 않아서 일 수 있다고 한다.
3. Firebase Auth: 로그인 기능 구현
다소 번거로운 로그인 기능 구현을, firebase_auth 라는 패키지를 통해 쉽게 구현할 수 있다.
Firebase Console 웹 페이지에 접속하여
우측 메뉴 > 빌드 > Authentication 메뉴에서 시작하기를 누른 후 과정을 거치면 된다.
auth_service.dart 라는 파일을 만들어, provider 패키지를 이용해 main에 적용해주었다.
import 'package:firebase_auth/firebase_auth.dart';
import 'package:flutter/material.dart';
class AuthService extends ChangeNotifier {
User? currentUser() {
// 현재 유저(로그인 되지 않은 경우 null 반환)
return FirebaseAuth.instance.currentUser;
}
// 회원가입
void signUp({
required String email, // 이메일
required String password, // 비밀번호
required Function onSuccess, // 가입 성공시 호출되는 함수
required Function(String err) onError, // 에러 발생시 호출되는 함수
}) async {
// 회원가입
// 이메일 및 비밀번호 입력 여부 확인
if (email.isEmpty) {
onError("이메일을 입력해 주세요.");
return;
} else if (password.isEmpty) {
onError("비밀번호를 입력해 주세요.");
return;
}
// firebase auth 회원 가입
try {
await FirebaseAuth.instance.createUserWithEmailAndPassword(
email: email,
password: password,
);
// 성공 함수 호출
onSuccess();
} on FirebaseAuthException catch (e) {
// Firebase auth 에러 발생
onError(e.message!);
} catch (e) {
// Firebase auth 이외의 에러 발생
onError(e.toString());
}
notifyListeners();
}
// 로그인
void signIn({
required String email, // 이메일
required String password, // 비밀번호
required Function onSuccess, // 로그인 성공시 호출되는 함수
required Function(String err) onError, // 에러 발생시 호출되는 함수
}) async {
// 로그인
if (email.isEmpty) {
onError('이메일을 입력해주세요.');
return;
} else if (password.isEmpty) {
onError('비밀번호를 입력해주세요.');
return;
}
// 로그인 시도
try {
await FirebaseAuth.instance.signInWithEmailAndPassword(
email: email,
password: password,
);
onSuccess(); // 성공 함수 호출
notifyListeners(); // 로그인 상태 변경 알림
} on FirebaseAuthException catch (e) {
// firebase auth 에러 발생
onError(e.message!);
} catch (e) {
// Firebase auth 이외의 에러 발생
onError(e.toString());
}
}
// 로그아웃
void signOut() async {
// 로그아웃
await FirebaseAuth.instance.signOut();
notifyListeners(); // 로그인 상태 변경 알림
}
}
*Provider 설정하기
MultiProvider를 MyApp 상위에 만들어 준 뒤,
ChangeNotifierProvider를 설정하여, 앱이 빌드될 때 새로고침 및 데이터를 전달한다.
runApp(
MultiProvider(
providers: [
ChangeNotifierProvider(create: (context) => AuthService()),
],
child: const MyApp(),
),
);
4. Firestore 데이터베이스 연동하기
Firestore의 데이터베이스를 연동하여, CRUD 기능을 구현하였다.
import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:flutter/material.dart';
// firebase의 bucket이라는 이름의 Collection을 가리키는 변수 생성
// 빌드 시 실행
class BucketService extends ChangeNotifier {
final bucketCollection = FirebaseFirestore.instance.collection('bucket');
// Read: 내 bucketList 가져오기
Future<QuerySnapshot> read(String uid) async {
return bucketCollection.where('uid', isEqualTo: uid).get();
}
// Create: bucket 만들기
void create(String job, String uid) async {
await bucketCollection.add({
'uid': uid, // 유저 식별자
'job': job, // 할 일
'isDone': false, // 완료 여부
});
notifyListeners(); // 갱신
}
// Update: bucket isDone 업데이트
void update(String docId, bool isDone) async {
await bucketCollection.doc(docId).update({'isDone': isDone});
notifyListeners(); // 화면 갱신
}
// Delete: bucket 삭제
void delete(String docId) async {
await bucketCollection.doc(docId).delete();
notifyListeners();
}
}
*보안 규칙 설정
누구나 CRUD를 할 수 없도록 Firestore 보안 규칙을 설정해야 함
Firebase Console > Firestore > 규칙 탭
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
match /bucket/{document} {
// document의 uid와 일치하는 유저만 RUD 가능
allow read, update, delete: if request.auth != null && request.auth.uid == resource.data.uid;
// Create의 경우, 로그인 되어있으면 가능
allow create: if request.auth != null;
}
}
}
// bucket 밑에 있는 docment에 있는 uid 값과 요청자의 uid가 일치하는 경우만 CRUD가 가능
* request.auth == null : Firebase Auth 로그인 하지 않음
* request.auth != null : Firebase Auth 로그인 됨
* request.auth.uid : 로그인한 유저의 uid
* resource.data.<문서 field 이름> : 직접 정의한 document 상의 field 이름(여기에선 버킷을 작성할 때 추가한 uid)
알아둬야 할 것들
1. FutureBuilder
플러터와 다트는 본질적으로 동기화가 되지 않는다. (asynchronous)
다트의 Future를 사용하면, 스레즈나 교착상태를 걱정할 필요 없이 IO를 관리할 수 있다. (Async makes IO easy)
아래와 같이 FutureBuilder를 이용할 수 있다.
FutureBuilder는 미래의 현황을 쉽게 결정하게 하고, 정보를 불러오는 동안/ 가능할 때 어떤걸 보여줄 지 선택하도록 한다.
/// FutureBuilder 사용법.
FutureBuilder(
future: http.get('http://awsome.data'),
builder: (context, snapshot) {
// connectionState로 Future의 상태를 확인
if (snapshot.connectionState == ConnectionState.done) {
// Future가 해결되는 동안오류가 발생했는지 확인.
if (snapshot.hasError) {
return SomethingWentWrong();
}
// 연결이 완료되었을때 보여줄 것.
return AwesomeData(snapshot.data);
} else {
// Future가 바쁠 때(연결중), 적절한 위젯 배치.
return CircularProgressIndicator();
}
}
)
버킷리스트를 Firestore에서 조회해 온 뒤 실행해야 하기 때문에,
ListView.builder()를 FutureBuilder로 감싸줬다.
// FutureBuilder
child: FutureBuilder<QuerySnapshot>(
future: bucketService.read(user.uid),
builder: (context, snapshot) {
final docs = snapshot.data?.docs ?? [];
if (docs.isEmpty) {
return Center(
child: Text('버킷리스트를 작성해주세요.'),
);
}
return ListView.builder(
itemCount: docs.length,
itemBuilder: (context, index) {
final doc = docs[index];
String job = doc.get('job');
bool isDone = doc.get('isDone');
return ListTile(
title: Text(
job,
style: TextStyle(
fontSize: 24,
color: isDone ? Colors.grey : Colors.black,
decoration: isDone
? TextDecoration.lineThrough
: TextDecoration.none,
),
),
// 삭제 아이콘 버튼
trailing: IconButton(
icon: Icon(CupertinoIcons.delete),
onPressed: () {
// 삭제 버튼 클릭시
bucketService.delete(doc.id);
print('$index 번째 버킷 삭제');
},
),
onTap: () {
// 아이템 클릭하여 isDone 업데이트
bucketService.update(doc.id, !isDone);
print('$index 번째 버킷 클릭 $isDone');
},
);
},
);
})
2. 삼항 연산자
*짤막한 복습
isDone이 true인지 false인지에 따라, 취소선 + 그레이 텍스트 스타일을 적용하였다.
style: TextStyle(
fontSize: 24,
color: isDone ? Colors.grey : Colors.black,
decoration: isDone
? TextDecoration.lineThrough
: TextDecoration.none,
),