Firebase

김동연·2025년 5월 27일

개발기록일지(Flutter)

목록 보기
10/32

파이어베이스(Firebase)

  • 서버 구현 없이 앱개발을 할 수 있도록 도와주는 플랫폼

기능은 무엇이 있나? 일단 기본적인 것부터 작성하고 다른 기능은 추후에 추가해보자.

1. Authentication

  • 로그인 회원가입 쉽게 구현 ex)구글로그인

2. Firestore

  • NoSQL 기반 데이터베이스로 데이터를 저장
  • 실시간 동기화 가능(값이 업데이트 되면 다시 데이터 달라고 요청할 필요 없이 감지할 수 있음) * 오프라인 지원
  • 문서(Document) - 데이터의 기본단위. JSON 처럼 key - value 형태로 값을 저장.
  • 컬렉션(Collection) - 여러 문서를 그룹으로 묶는 개념

1. Firestore 연동하는 법

  1. Firebase console 에서 프로젝트 만들기
    공식 홈페이지 접속 후 Go to console > 프로젝트 만들기

  2. Firestore 데이터베이스 만들기
    Cloud Firestore > 데이터베이스 만들기 > 가까운 곳 선택(Seoul)


  1. Flutter에 Firebase 연동
  • Firebase 추가
  • Firebase CLI 설치(Firebase 연동 편리하게 해주는 명령줄도구)
curl -sL https://firebase.tools | bash

  • 파워쉘(윈도우), 터미널(맥)에서 firebase login 입력 후 로그인
    • 입력하면 약관동의 입력모드 나오면 y 누르고 엔터 치면 브라우저에서 구글 계정 선택창 열리는데 Firebase 콘솔 웹에서 로그인한 계정 클릭

  • flutterfire_cli 활성화(flutter에 Firebase 연동 편리하게 해주는 도구)
art pub global activate flutterfire_cli 

  • VSCode 터미널에 아래 코드 입력. 프로젝트에 Firebase 자동 구성 시작
flutterfire configure


스페이스를 입력하여 비활성화 가능


  • VSCode 터미널에 아래 코드 입력. firebase_core 패키지 추가
flutter pub add firebase_core

  • main.dart 의 main 함수에서 Firebase 초기화 코드 추가
import 'package:firebase_core/firebase_core.dart';
import 'package:flutter/material.dart';
import 'package:flutter_firebase_blog_app/firebase_options.dart';
import 'package:flutter_firebase_blog_app/ui/pages/home/home_page.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';

void main() async {
  WidgetsFlutterBinding.ensureInitialized();
  await Firebase.initializeApp(
    options: DefaultFirebaseOptions.currentPlatform,
  );
  // ProviderScope 로 앱을 감싸서 RiverPod이 ViewModel 관리할 수 있게 선언
  runApp(const ProviderScope(child: MyApp()));
}

// =========================

  • ios/Podfile 에서 IOS 최소 지원 버전 바꾸기(IOS 13 이상부터 flutter firebase_core 사용할 수 있음)

  • Firebase 콘솔 웹에서 앱개요보기에 안드로이드, IOS 추가된거 확인.

  • Flutter에 firebase_firestore 패키지 추가
flutter pub add cloud_firestore

2. Firebase Firestore 사용법

CRUD 구현(CREATE, READ, UPDATE, DELETE)

  • 모델 클래스 만들기 전 예시 데이터 만들기

1. 컬렉션 내 문서 전체 조회

  Future<void> getAll() async {
    // 1. FirebaseFirestore 객체 가지고오기
    FirebaseFirestore firestore = FirebaseFirestore.instance;
    // 2. FirebaseFirestore 객체에서 collection 메서드를 통해 posts 컬렉션에 대한 참조 가지고 오기
    //    여기까지는 아무런 통신이 이루어 지지 않고 단순히 posts 컬렉션에 대한 참조만 저장!!(posts 컬렉션을 가지고 올거야 하고만 알려주는 단계)
    CollectionReference collectionRef = firestore.collection('posts');
    // 3. 컬렉션 참조에서 모든 문서(Document) 가지고오기
    //    이 때 Firestore에서 데이터 가지고옴
    //    결과가 QuerySnapshot 타입으로 전달됨
    QuerySnapshot snapshot = await collectionRef.get();
    // 4. QuerySnapshot객체 내에 docs 필드에 조회된 문서들의 결과가 들어있는데
    //    QueryDocumentSnapshot 라는 타입임. 즉 문서 조회 결과
    List<QueryDocumentSnapshot> documentSnaphots = snapshot.docs;
    // 5. QueryDocumentSnapshot에서 data 메서드를 통해 진짜 데이터 가지고올 수 있음
    for (var docSnapshot in documentSnaphots) {
      print(docSnapshot.data());
    }
  }
  1. 파이어베이스 객체 가져오기
  2. 컬렉션 참조 만들기
  3. 문서 참조 만들기
  4. 데이터 가져오기
  5. try catch문으로 감싸서 예외처리

2. 컬렉션 내 문서 특정 문서 조회

  Future<void> getOneById() async {
    // 1. FirebaseFirestore 객체 가지고오기
    FirebaseFirestore firestore = FirebaseFirestore.instance;
    // 2. FirebaseFirestore 객체에서 collection 메서드를 통해 posts 컬렉션에 대한 참조 가지고 오기
    //    여기까지는 아무런 통신이 이루어 지지 않고 단순히 posts 컬렉션에 대한 참조만 저장!!
    CollectionReference collectionRef = firestore.collection('posts');
    // 3. 컬렉션 참조에서 문서 ID에 대한 문서(Document) 참조 만들기
    //    파라미터로 아까 만들때 생성된 ID 값 넣으면 됨
    //    ID는 중복되면 안됨!!!!!
    DocumentReference documentRef = collectionRef.doc('문서 ID');
    // 4. 문서 참조의 정보를 기반으로 파이어스토어에서 문서 가지고옴!!!
    //    DocumentSnapshot 타입. 문서 조회결과
    DocumentSnapshot documentSnaphot = await documentRef.get();
    // 5. DocumentSnapshot의 data 메서드를 통해 진짜 데이터 가지고올 수 있음
    print(documentSnaphot.data());
  }
  1. 파이어베이스 객체 가져오기
  2. 컬렉션 참조 만들기
  3. 문서 참조 만들기
  4. 데이터 가져오기
  5. try catch문으로 감싸서 예외처리

3. 문서 생성

  Future<void> insert() async {
    // 1. FirebaseFirestore 객체 가지고오기
    FirebaseFirestore firestore = FirebaseFirestore.instance;
    // 2. FirebaseFirestore 객체에서 collection 메서드를 통해 posts 컬렉션에 대한 참조 가지고 오기
    //    여기까지는 아무런 통신이 이루어 지지 않고 단순히 posts 컬렉션에 대한 참조만 저장!!
    CollectionReference collectionRef = firestore.collection('posts');
    // 3. 컬렉션 참조에서 문서(Document) 참조 만들기
    //    ID를 파라미터로 넣지 않으면 문서 생성 시 새로운 ID 부여!
    DocumentReference documentRef = collectionRef.doc();
    // 4. 문서에 넣을 데이터를 Map 타입으로 생성
    Map<String, dynamic> data = {
      'writer': '김동연',
      'title': '블로그 타이틀',
      'content': '내용',
      'createdAt': DateTime.now().toIso8601String(),
    };
    // 5. 문서 참조의 set 메서드 안에 생성할 데이터 전달해주면 이때 생성!!!
    await documentRef.set(data);
  }
  1. 파이어베이스 객체 가져오기
  2. 컬렉션 참조 만들기
  3. 문서 참조 만들기
  4. 값 쓰기
  5. try catch문으로 감싸서 예외처리

4. 문서 수정

  Future<void> udpate() async {
    // 1. FirebaseFirestore 객체 가지고오기
    FirebaseFirestore firestore = FirebaseFirestore.instance;
    // 2. FirebaseFirestore 객체에서 collection 메서드를 통해 posts 컬렉션에 대한 참조 가지고 오기
    //    여기까지는 아무런 통신이 이루어 지지 않고 단순히 posts 컬렉션에 대한 참조만 저장!!
    CollectionReference collectionRef = firestore.collection('posts');
    // 3. 컬렉션 참조에서 문서(Document) 참조 만들기
    DocumentReference documentRef = collectionRef.doc('수정할 문서의 ID');
    // 4. 수정할 데이터를 Map 타입으로 생성
    Map<String, dynamic> data = {
      'writer': '오상구',
    };
    // 5. 문서 참조의 set 메서드 안에 생성할 데이터 전달해주면 이때 수정!!!
    //업데이트 값 Map형태로 넣어주기
    await documentRef.set(data); //id에 해당하는 문서가 없을 때 새로생성
    // await docunentRef.update(data); << id에 해당하는 문서가 없을 때 에러발생 
  }
  1. 파이어베이스 객체 가져오기
  2. 컬렉션 참조 만들기
  3. 문서 참조 만들기
  4. 값을 업데이트 해주기
  5. try catch문으로 감싸서 예외처리
  • set vs update
    • set: id에 해당하는 문서가 없을 때 새로 생성
    • update: id에 해당하는 문서가 없을 때 에러 발생

5. 문서 삭제

  Future<void> delete() async {
    // 1. FirebaseFirestore 객체 가지고오기
    FirebaseFirestore firestore = FirebaseFirestore.instance;
    // 2. FirebaseFirestore 객체에서 collection 메서드를 통해 posts 컬렉션에 대한 참조 가지고 오기
    //    여기까지는 아무런 통신이 이루어 지지 않고 단순히 posts 컬렉션에 대한 참조만 저장!!
    CollectionReference collectionRef = firestore.collection('posts');
    // 3. 컬렉션 참조에서 문서(Document) 참조 만들기
    DocumentReference documentRef = collectionRef.doc('삭제할 문서의 ID');
    // 4. 참조하고 있는 문서 삭제!!!
    await documentRef.delete();
  }
  1. 파이어베이스 객체 가져오기
  2. 컬렉션 참조 만들기
  3. 문서 참조 만들기
  4. 삭제
  5. try catch문으로 감싸서 예외처리

6. 데이터 CRUD가 일어난 뒤 화면에 반영

  1. 업데이트 일어날때마다 다른 뷰모델을 참조해서 업데이트 해준다.
  2. 파이어베이스 실시간 적용한다.
  • PostRepository에 snapshots을 추가
  Stream<List<Post>> postListStream() {
    // 1. 컬렉션 참조를 만들어줍니다!
    final collectionRef = FirebaseFirestore.instance
        .collection('posts')
        // 여기서 리스트 가지고 올 때 정렬 방식도 정할수 있어요!
        // .count 나 .where과 같은 조건으로 가지고 올 수도 있어요!
        .orderBy('createdAt', descending: true);
    // 2. 참조의 스냅샷 메서드는 변경이 일어날 때마다 스트림에 값을 하나씩 넣어주는 역할을 해요!
    final snapshotStream = collectionRef.snapshots();
    // 3. 스트림을 가공해서 다른 List<Post> 타입의 스트림으로 만들게요!
    return snapshotStream.map(
      (event) {
        return event.docs.map(
          (doc) {
            return Post.fromJson({'id': doc.id, ...doc.data()});
          },
        ).toList();
      },
    );
  }

  Stream<Post?> postStream(String id) {
    final snapshot = FirebaseFirestore.instance.collection('posts').doc(id);
    return snapshot.snapshots().map(
      (e) {
        if (e.data() == null) {
          return null;
        }
        return Post.fromJson({'id': e.id, ...e.data()!});
      },
    );
  }

각자 맞는 View, ViewModel 수정(아래는 예시)

  • HomeViewModel 수정
  Future<void> fetchData() async {
    // state = await postRepository.getAll();
    // 1. 스트림을 받아옵니다
    final stream = postRepository.postListStream();
    // 2. 스트림의 변경사항을 구독하고 상태를 업데이트 해줍니다!
    final streamSubscription = stream.listen(
      (newList) {
        state = newList;
      },
    );

    // 2. 이 뷰모델이 없어질 때 구독을 끝내주어야 메모리에서 안전하게 제거가 돼요!
    ref.onDispose(
      () {
        streamSubscription.cancel();
      },
    );
  }
  • DetailPage 수정
    • 기존 참조하고있던 파라미터로 넘어온 post를 뷰모델에서 가져온 값으로 바꾸어 줍니다
import 'package:flutter/material.dart';
import 'package:flutter_firebase_blog_app/data/model/post.dart';
import 'package:flutter_firebase_blog_app/ui/pages/detail/detail_view_model.dart';
import 'package:flutter_firebase_blog_app/ui/pages/write/write_page.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';

class DetailPage extends StatelessWidget {
  DetailPage(this.post);
  Post post;

  
  Widget build(BuildContext context) {
    // Consumer로 감싸주기!

    return Consumer(builder: (context, ref, child) {
      // 상태 변화를 감지하기 위해!
      final state = ref.watch(detailViewModel(post));
      // 삭제된 포스트면 로딩창 나오게!!
      if (state == null) {
        return Scaffold(
          body: Center(
            child: CircularProgressIndicator(),
          ),
        );
      }
      return Scaffold(
        appBar: AppBar(
          actions: [
            button(Icons.delete, () async {
              final result =
                  await ref.read(detailViewModel(post).notifier).delete();
              if (result) {
                Navigator.pop(context);
              }
            }),
            button(Icons.edit, () {
              Navigator.push(context, MaterialPageRoute(
                builder: (context) {
                  return WritePage(post: post);
                },
              ));
            }),
          ],
        ),
        body: ListView(
          padding: EdgeInsets.only(bottom: 300),
          children: [
            Image.network(
							state.imgUrl,
              fit: BoxFit.cover,
            ),
            SizedBox(height: 20),
            // 이미지에는 패딩이 적용되지 않기 때문에 아래 영역만 Padding 위젯
            // 감싸서 패딩 지정!
            Padding(
              padding: EdgeInsets.symmetric(horizontal: 20),
              child: Column(
                crossAxisAlignment: CrossAxisAlignment.start,
                children: [
                  Text(
                    state.title,
                    style: TextStyle(
                      fontWeight: FontWeight.bold,
                      fontSize: 20,
                    ),
                  ),
                  SizedBox(height: 14),
                  Text(
                    state.writer,
                    style: TextStyle(
                      fontSize: 16,
                    ),
                  ),
                  Text(
                    '${state.createdAt.year}.${state.createdAt.month}.${state.createdAt.day} ${state.createdAt.hour}:${state.createdAt.minute}',
                    style: TextStyle(
                      fontWeight: FontWeight.w200,
                      fontSize: 14,
                    ),
                  ),
                  SizedBox(height: 14),
                  Text(
                    state.content,
                    style: TextStyle(
                      fontSize: 16,
                    ),
                  ),
                ],
              ),
            ),
          ],
        ),
      );
    });
  }

  GestureDetector button(IconData icon, VoidCallback onTap) {
    return GestureDetector(
      onTap: onTap,
      child: Container(
        // Icon 크기가 기본 24 이기 때문에 컨테이너 색상을 지정해주지 않으면
        // 터치 반경 24x24 됨
        color: Colors.transparent,
        width: 50,
        height: 50, // 중요!!! 사람이 터치할때 터치 잘되게 하는 최소 반경이 44 디바이스 픽셀!
        child: Icon(icon),
      ),
    );
  }
}

사용법 정리

  • 컬렉션 전체 리스트(코드는 위에 참고, 버전에 따라 달라질 수 있음)

    • Firestore 객체 가지고 오기컬렉션 참조 만들기전체 문서 가져오기

    • Firestore 객체 가지고 오기컬렉션 참조 만들기문서 참조 만들기읽기,수정,삭제,생성

    • main.dart 의 main함수 에서 호출해서 테스트

    • 데이터 CRUD가 일어난 뒤 화면에 반영


3. Firebase Storage

  • 사진, 동영상 및 각종 파일을 저장
  • Bytes 형식을 저장할 순 있지만 1MB만 지원
    • Storage에 저장하면 접근할 수 있는 URL이 제공
    • 이 URL을 Firestore에 저장

Firebase storage 생성

  • storage > 시작하기 클릭 > 기본 버킷 설정 완료 > 아래코드 입력 > 추가 후 실행(네이티브 라이브러리를 받아와 오래걸림)
  • image_picker를 통해 갤러리 이미지 가져오기
flutter pub add firebase_storage

Firebase Storage 사용법

  1. Firebase Storage가져오기
  2. 스토리지 참조 만들기
  3. 파일 참조 만들기
  4. 쓰기
  5. 파일에 접근할 수 있는 URL 만들기
  Future<void> uploadImage(XFile xFile) async {
    try {
      // FirebaseStorage 객체 가지고오기
      FirebaseStorage storage = FirebaseStorage.instance;
      // 스토리지 참조 가지고 오기
      Reference storageRef = storage.ref();
      // 스토리지 참조의 child 메서드를 사용하면 파일 참조 만들어짐
      // 파라미터는 파일 이름!!
      // 중복되면 안되니까 현재시간이랑 기존 파일이름 섞을게요!
      final imageRef = storageRef
          .child('${DateTime.now().microsecondsSinceEpoch}_${xFile.name}');
      // 참조가 만들어졌으니 파일 업로드!!
      await imageRef.putFile(File(xFile.path));
      // 만들어진 파일의 url 가져오기
      final url = await imageRef.getDownloadURL();
      state = WritePageState(false, url);
    } catch (e) {
      print(e);
    }
  }

image_picker

`image_picker`사용하여 디바이스에 있는 갤러리 접근가능.
```bash
flutter pub add image_picker
  • IOS 권한설정 (안드로이드는 별다른 설정 필요X)
    • ios/Runner/info.plist
	<key>NSPhotoLibraryUsageDescription</key>
	<string>사진 업로드를 위한 라이브러리 권한을 허용해 주세요</string>

  • info.plist 란?
    • IOS 앱에 관한 설정정보를 키, 밸류 타입으로 구성(xml 방식. < > 형태로 감싸고 HTML과 유사)
    • 앱의 보여지는 이름, 각종 권한 물어볼 때 사용할 문자열 정의 가능

  • 사용방법
  // 이미지피커 객체 생성!
  ImagePicker imagePicker = ImagePicker();
  // pickImage 메서드호출해서 가지고옴
  // 갤러리 권한만 넣어줬으니 ImageSource.gallery!
  // 호출하면 폰에서 앨범이 열림
  // 사용자가 사진을 선택하지 않을 떈 null
  // 선택하면 XFile이라는 타입으로 돌려주는데 
  // 사진의 경로를 가지고 있는 path, 경로에서 바이트 데이터를 얻을 수 있는 readAsBytes() 메서드 등이 있음
  XFile? xFile = await imagePicker.pickImage(source: ImageSource.gallery);

사진 불러오기 및 업로드 구현

  1. 이미지 피커 객체 생성
                Align(
                  alignment: Alignment.centerRight,
                  child: GestureDetector(
                    onTap: () async {
                      final xFile = await ImagePicker()
                          .pickImage(source: ImageSource.gallery);
                      if (xFile != null) {
                        // 뷰모델에 넘겨주기!
                        vm.uploadImage(xFile);
                      }
                    },
                    child: Container(
                      width: 100,
                      height: 100,
                      color: Colors.grey,
                      child: Icon(Icons.image),
                    ),
                  ),
                ),

  1. 이미지 피커 객체의 pickImage 메서드 호출(스토리지 사용법 참고)
  Future<void> uploadImage(XFile xFile) async {
    try {
      // FirebaseStorage 객체 가지고오기
      FirebaseStorage storage = FirebaseStorage.instance;
      // 스토리지 참조 가지고 오기
      Reference storageRef = storage.ref();
      // 스토리지 참조의 child 메서드를 사용하면 파일 참조 만들어짐
      // 파라미터는 파일 이름!!
      // 중복되면 안되니까 현재시간이랑 기존 파일이름 섞을게요!
      final imageRef = storageRef
          .child('${DateTime.now().microsecondsSinceEpoch}_${xFile.name}');
      // 참조가 만들어졌으니 파일 업로드!!
      await imageRef.putFile(File(xFile.path));
      // 만들어진 파일의 url 가져오기
      final url = await imageRef.getDownloadURL();
      state = WritePageState(false, url);
    } catch (e) {
      print(e);
    }
  }

  1. ViewModel 에서 XFile 받기
Future<void> uploadImage(XFile? xFile) async {
}

  1. 상태 클래스, 메서드 각각 수정

0개의 댓글