ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [개발일지] flutter 앱개발 5주차
    Study/Development 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,
    ),

     

    반응형

    댓글

© 2023. titann all rights reserved