[Flutter] 스나이퍼팩토리 34일차

KWANWOO·2023년 3월 13일
1
post-thumbnail

스나이퍼팩토리 플러터 34일차

34일차에는 Flutter에 Firebase를 연동하여 사용해 보았다.

학습한 내용

  • BaaS
  • Firebase
  • cloud_firestore CRUD

추가 내용 정리

BaaS

BaaS는 Backend as a Service의 약자로 백엔드 서비스를 대신 해주는 것을 의미한다.

데이터를 안전한 하이브리드 클라우드 또는 오프사이트 클라우드 레포지토리에 저장하여 무단 엑세스, 손상, 해킹 또는 도난으로부터 보호한다.

데이터는 파일 및 이미지부터 전체 어플리케이션 워크로드, 데이터 세트에 이르기까지 비즈니스 가치가 있는 모든 것을 포함할 수 있다.

대표적인 BaaS의 예시로는 Firebase와 AWS Amplify 등이 있다.

Firebase

Firebase는 Google이 만든 BaaS로, 아래의 기능들을 제공한다.

Firebase의 기능

  • Authentication
    • SNS 로그인 (구글, 애플, 페이스북 등)
    • 일반 로그인 (아이디/비밀번호, 전화인증 등)
  • Cloud Firestore
    • 데이터베이스처럼 사용할 수 있는 DB 시스템
    • NoSQL 방식
  • Cloud Storage
    • AWS S3와 같이 저장공간을 제공
    • 이미지, 동영상, 파일 등을 저장할 수 있는 공간
  • Cloud Functions
    • 백엔드에서 실행할 수 있는 함수들
    • 결제처리와 같은 앱 동작에 피룡한 백엔드의 커스텀 함수
    • Blaze만 사용 가능 (유료 결제)
  • In-App messaging
    • 앱이 설치된 사용자들에게 푸시 알림을 보내는 기능
    • 사용자 별 권한/토큰이 필요한 부분

Flutter 프로젝트와 Firebase 연동

Flutter에 Firebase를 연동하는 방법은 우선 [Firebase 홈페이지]에서 콘솔로 이동해 프로젝트를 생성한다.

생성한 프로젝트의 메인에서 Flutter 연동 방법을 보고 그대로 따라서 수행하면 된다.

Flutter 프로젝트와 Firebase를 연동하면 lib 폴더에 firebase_options.dart 파일이 생성된다.

Flutter에서 Firebase를 사용하기 위해서는 Firebase를 초기화 해야한다. 이는 아래와 같이 main()함수를 작성하면 된다.

void main() async {
  //firebase 초기화
  WidgetsFlutterBinding.ensureInitialized();
  await Firebase.initializeApp(
    options: DefaultFirebaseOptions.currentPlatform,
  );
  runApp(const MyApp());
}

WidgetsFlutterBinding.ensureInitialized();는 비동기 메서드를 사용할 때, runApp메소드의 시작 지점에서 플러터 엔진과 위젯의 바인딩이 미리 완료되어 있도록 만들어 주는 코드이다.

Firebase 연동 오류들

Flutter 프로젝트에 Firebase를 연동하면서 발생했던 오류들을 정리하고자 한다.

  • flutterfire 명령어 실행 오류
    Firebase 홈페이지의 설명에 따라 연동을 하면서 npm을 사용해 Firebase CLI를 설치했음에도 flutterfire 명령어를 찾지 못했다는 오류가 발생했다.

    이는 flutterfire가 설치된 폴더의 환경변수가 설정되지 않아서 발생하는 오류이다.

    아래와 같은 디렉토리를 시스템 환경변수로 설정하면 오류가 해결된다.

C:\Users\[사용자명]\AppData\Local\Pub\Cache\bin
  • The plugin cloud_firestore requires a higher Android SDK version.

안드로이드 최소 OS 버전이 낮아서 발생하는 오류이다. 최신 패키지를 사용하기 위해 앱이 구동될 수 있는 환경 최소 조건을 올려주면 된다.

[프로젝트]/android/app/build.gradle에서 minSdkVersion을 아래와 같이 수정한다.

...
defaultConfig {
        // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
        applicationId "com.example.day34_test_firebase"
        // You can update the following values to match your application needs.
        // For more information, see: https://docs.flutter.dev/deployment/android#reviewing-the-gradle-build-configuration.
        minSdkVersion 19 //해당 부분 변경
        targetSdkVersion flutter.targetSdkVersion
        versionCode flutterVersionCode.toInteger()
        versionName flutterVersionName
    }
...
  • Cannot fit requested classes in a single dex file

안드로이드 minSdkVersion이 21보다 높으면 multiDex가 기본으로 설정되지만 21보다 낮으면 직접 설정해야 한다.

Firebase에 들어가는 파일이 많아서 수용할 수 있는 한계치를 넘어버리게 되는데 이를 풀어준다고 생각하면 된다.

[프로젝트]/android/app/build.gradle에서 multiDexEnabled를 추가한다.

...
defaultConfig {
        // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
        applicationId "com.example.day34_test_firebase"
        // You can update the following values to match your application needs.
        // For more information, see: https://docs.flutter.dev/deployment/android#reviewing-the-gradle-build-configuration.
        minSdkVersion 19
        targetSdkVersion flutter.targetSdkVersion
        versionCode flutterVersionCode.toInteger()
        versionName flutterVersionName
				multiDexEnabled true                          // 해당부분 추가
    }
...
  • Failed to load providerinstaller module: No acceptable module found. Local version is 0 and remote version is 0.

    연결이 완료되고 앱이 실행되어도 데이터를 가져오지 못하고 위와 같은 에러가 발생할 수 있다.

    이 경우에는 네트워크가 연결된 것을 확인할 수 있도록 [프로젝트]/android/app/main/AndroidMenifest.xml에 권한을 추가하면 된다.

<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.example.firebase_app">
    <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />

cloud_firestore CRUD

CRUD는 Create, Read, Update, Delete의 기능을 구현한 것을 말한다. 여기서는 cloud_firestore의 기본적인 CRUD 사용법에 대해 작성했다.

  • Create : 데이터를 생성한다.

DocId를 자동으로 생성한다. (unique auto-generated)

FirebaseFirestore.instance
				.collection(컬렉션명)
               	.add({데이터});

DocId를 직접 설정한다. (set)

FirebaseFirestore.instance
				.collection(컬렉션명)
                .doc(지정DocId)
               	.set({데이터});
  • Read : 데이터를 읽어온다.

해당 컬렉션의 문서들을 가져온다.

FireBaseFirestore.instance.collcetion(컬렉션명).get();

해당 컬렉션의 선택 문서 하나를 가져온다.

FireBaseFirestore.instance.collcetion(컬렉션명).doc(문서명);

해당 컬렉션에 필터를 걸고, 해당되는 문서들을 가져온다.

FireBaseFirestore.instance
				.collcetion(컬렉션명)
				.where(, isEaualTo:)
				.get();
  • Update : 데이터를 수정한다.
FirebaseFirestore.instance
				.collection(컬렉션명)
                .doc(지정DocId)
                .update({업데이트 할 데이터});
  • Delete : 데이터를 삭제한다.
FirebaseFirestore.instance
				.collection(컬렉션명)
                .doc(지정DocId)
                .delete();

34일차 과제

  1. Firebase의 withConverter
  2. firestore를 사용한 좋아요 기능 어플 제작

1. Firebase의 withConverter

Firebase에서는 데이터 Serialization을 편하게 적용시킬 수 있도록하는 메서드인 withConverter()를 제공한다.

사용 방식은 firestore의 레퍼런스를 가져올 때, withConverter()를 사용해서 JSON이 아닌 객체를 주고 받고, 데이터 타입도 정해줄 수 있다.

아래는 withConverter를 통해 데이터를 가져와 직렬화를 수행하는 예시이다.

  final userTextColRef = FirebaseFirestore.instance
      .collection(FirebaseAuth.instance.currentUser!.email.toString())
      .withConverter(
          //firestore에서 JSON이 아닌 객체로 데이터를 넘겨줌.
          fromFirestore: (snapshot, _) => UserData.fromMap(snapshot.data()!),
          //firestore에 저장할 때 객체를 Json으로 변경해서 넘겨줌.
          toFirestore: (movie, _) => movie.toMap());

위의 예시코드를 살펴보면 withConverter에서 fromFirestore는 firestore에서 값을 가져와 snapshot을 제공한다. 이를 사용해 자신이 만들어 놓은 모델 클래스의 fromMap등의 생성자를 사용해 쉽게 데이터 Serialization을 수행할 수 있다.

반대로 toFirestore에는 firestore에 값을 저장할 때, 객체를 JSON 형태로 바꿔서 넘겨주면 된다. 이 때는 모델 클래스에 toMap과 같은 메서드를 작성하여 사용할 수 있다.

이렇게 하면 userTextColRef 컬렉션을 통해서 얻어온 데이터 타입은 JSON이 아닌 UserData 객체가 된다. 따라서 별도의 변환 없이 바로 객체에 접근 할 수 있다.

withConverter의 동작 원리는 2번 과제를 수행하면서 한 번 더 정리하고자 한다.

2. firestore를 사용한 좋아요 기능 앱 제작

firestore를 사용해 좋아요 기능을 수행하는 앱을 제작해 보고자 한다.

firestore 사전 설정

과제를 수행하기 위해 먼저 Firebase 프로젝트에 컬렉션 (post)를 만들고 2가지 Documnet를 만들어 다음의 값을 넣도록 한다.

  • 문서명은 자동생성을 사용
  • 필드 값은 content, likes, title을 가지도록 하며, 데이터 타입은 다음과 같다.
    • content (String)
    • title (String)
    • likes (Number)

예시 코드

아래의 주어진 예시 코드를 사용하여 앱을 만들고, AssignmentPage의 주석 처리된 부분을 작성하여 앱을 완성한다.

  • lib/model/post.dart
// ignore_for_file: public_member_api_docs, sort_constructors_first
import 'dart:convert';

class Post {
  String? id;
  String title;
  String content;
  int likes;
  Post({
    required this.id,
    required this.title,
    required this.content,
    required this.likes,
  });

  Map<String, dynamic> toMap() {
    return <String, dynamic>{
      'id': id,
      'title': title,
      'content': content,
      'likes': likes,
    };
  }

  factory Post.fromMap(Map<String, dynamic> map) {
    return Post(
      id: map['id'] as String?,
      title: map['title'] as String,
      content: map['content'] as String,
      likes: map['likes'] as int,
    );
  }

  String toJson() => json.encode(toMap());

  factory Post.fromJson(String source) =>
      Post.fromMap(json.decode(source) as Map<String, dynamic>);
}
  • lib/view/page/assignment_page.dart
import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:flutter/material.dart';

class AssignmentPage extends StatefulWidget {
  const AssignmentPage({super.key});

  
  State<AssignmentPage> createState() => _AssignmentPageState();
}

class _AssignmentPageState extends State<AssignmentPage> {
  var ref = FirebaseFirestore.instance.collection('post').withConverter(
    fromFirestore: (snapshot, _) => Post.fromMap(snapshot.data()!),
    toFirestore: (data, _) => data.toMap(),
  );

  Future<List<QueryDocumentSnapshot<Post>>> readData() async {
    var items = await ref.get();
    return items.docs;
  }

  // likesUp(String id) => ref.doc(id).update(...);

  
  Widget build(BuildContext context) {
    return Scaffold(
      body: SafeArea(
        child: FutureBuilder<List<QueryDocumentSnapshot<Post>>>(
          future: readData(),
          builder: (context, snapshot) {
            if (snapshot.hasData &&
                snapshot.connectionState == ConnectionState.done) {
              return ListView.builder(
                itemCount: snapshot.data!.length,
                itemBuilder: (context, index) => ListTile(
                  title: Text(snapshot.data![index].data().title),
                  subtitle: Text(snapshot.data![index].data().content),
                  trailing: IconButton(
                    icon: const Icon(Icons.favorite),
                    onPressed: (){},
                    // onPressed: () => likesUp(snapshot.data![index].id),
                  ),
                ),
              );
            }
            return const SizedBox();
          },
        ),
      ),
    );
  }
}

코드 작성

  • pubspec.yaml
dependencies:
  cloud_firestore: ^4.4.4
  cupertino_icons: ^1.0.2
  firebase_core: ^2.7.1
  flutter:
    sdk: flutter

pubspec.yaml에 필요한 패키지를 설치했다.

  • lib/main.dart
import 'package:firebase_app/firebase_options.dart';
import 'package:firebase_app/view/page/assignment_page.dart';
import 'package:firebase_core/firebase_core.dart';
import 'package:flutter/material.dart';

void main() async {
  //firebase 초기화
  WidgetsFlutterBinding.ensureInitialized();
  await Firebase.initializeApp(
    options: DefaultFirebaseOptions.currentPlatform,
  );
  runApp(const MyApp());
}

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

  // This widget is the root of your application.
  
  Widget build(BuildContext context) {
    return const MaterialApp(
      home: AssignmentPage(),
    );
  }
}

main.dartAssignmentPage를 호출한다. 추가로 main()함수에서는 Firebase를 초기화한다.

WidgetsFlutterBinding.ensureInitialized();는 비동기 메서드를 사용할 때, runApp메소드의 시작 지점에서 플러터 엔진과 위젯의 바인딩이 미리 완료되어 있도록 만들어 주는 코드이다.

  • lib/model/post.dart
// ignore_for_file: public_member_api_docs, sort_constructors_first
import 'dart:convert';

class Post {
  String? id;
  String title;
  String content;
  int likes;
  Post({
    required this.id,
    required this.title,
    required this.content,
    required this.likes,
  });

  Map<String, dynamic> toMap() {
    return <String, dynamic>{
      'id': id,
      'title': title,
      'content': content,
      'likes': likes,
    };
  }

  factory Post.fromMap(Map<String, dynamic> map) {
    return Post(
      id: map['id'] as String?,
      title: map['title'] as String,
      content: map['content'] as String,
      likes: map['likes'] as int,
    );
  }

  String toJson() => json.encode(toMap());

  factory Post.fromJson(String source) =>
      Post.fromMap(json.decode(source) as Map<String, dynamic>);
}

Post는 주어진 예시 코드를 그대로 사용했다. 해당 모델은 문서의 아이디인 id, 타이틀 메세지인 title, 내용인 content, 좋아요 수인 likes를 멤버 변수로 가진다.

  • lib/view/page/assignment_page.dart
import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:firebase_app/model/post.dart';
import 'package:flutter/material.dart';

class AssignmentPage extends StatefulWidget {
  const AssignmentPage({super.key});

  
  State<AssignmentPage> createState() => _AssignmentPageState();
}

class _AssignmentPageState extends State<AssignmentPage> {
  var ref = FirebaseFirestore.instance.collection('post').withConverter(
        fromFirestore: (snapshot, _) => Post.fromMap(snapshot.data()!),
        toFirestore: (data, _) => data.toMap(),
      );

  Future<List<QueryDocumentSnapshot<Post>>> readData() async {
    var items = await ref.get();
    return items.docs;
  }

  likesUp(String id) => ref.doc(id).update({'likes': FieldValue.increment(1)});

  
  Widget build(BuildContext context) {
    return Scaffold(
      body: SafeArea(
        child: FutureBuilder<List<QueryDocumentSnapshot<Post>>>(
          future: readData(),
          builder: (context, snapshot) {
            if (snapshot.hasData &&
                snapshot.connectionState == ConnectionState.done) {
              return ListView.builder(
                itemCount: snapshot.data!.length,
                itemBuilder: (context, index) => ListTile(
                  title: Text(snapshot.data![index].data().title),
                  subtitle: Text(snapshot.data![index].data().content),
                  trailing: IconButton(
                    icon: const Icon(Icons.favorite),
                    onPressed: () => likesUp(snapshot.data![index].id),
                  ),
                ),
              );
            }
            return const SizedBox();
          },
        ),
      ),
    );
  }
}

AssignmentPage는 firestore에 저장된 데이터를 가져와 보여주는 페이지이다.

refpost 컬렉션의 데이터를 가져와 withConverter를 사용해 데이터를 Serialization 한다.

withConverter에서 fromFirestoresnapshot을 미리 작성한 모델의 fromMap을 사용해 직렬화를 할 수 있다. 이를 통해 해당 컬렉션에서 가져온 데이터는 바로 멤버 변수에 접근이 가능하다.

toFirestore는 데이터를 저장할 때 맵 형태로 바꿔주어 전달하도록 설정한다.

readData()는 firestore에 저장된 데이터를 get()으로 가져온다. 앞에서 withConverter로 데이터 직렬화를 수행했기 때문에 따로 코드를 작성할 필요 없이 Post 객체를 사용할 수 있다.

likesUpid를 매개변수로 전달받고, 해당 id의 문서의 좋아요 수인 likes를 1 증가시키도록 작성했다.

본문에서는 가져온 데이터를 FutureBuilder를 사용해 그려주었다.

좋아요를 아이콘을 눌렀을 때는 likesUp 메서드를 사용해 좋아요 수를 1 증가시킨 값을 firestore에 저장했다.

결과


34일차 마무리 단계...

8주차가 시작되었다. 오늘은 Firebase를 사용해 보았다. 예전에 2020년도 쯤에 플러터로 앱 만들면서 Firebase를 간단하게 사용했었는데 오랜만에 다시 사용하니까 하나도 기억이 안난다. ㅋㅋㅋㅋ 오늘은 강의에서 배운 간단한 사용방법만 정리했지만 Firebase의 사용법과 코드들이 정말 많기 때문에 추가로 더 많이 찾아봐야겠다.

📄Reference

profile
관우로그

0개의 댓글

관련 채용 정보