Study/Development

[개발일지] flutter 앱개발 5주차

titann 2022. 10. 3. 15:33

<스파르타코딩 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에 관련된 버전과 기기의 버전이 맞지 않아서 일 수 있다고 한다.

Simulater 우클릭 > Device > iOS 15.5 > 다른 기기 선택

 

 

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,
),

 

반응형