19장 파이어베이스 연동하기

송기영·2024년 1월 16일
0

플러터

목록 보기
21/25

19.1 사전지식

19.1.1 파이어베이스 기능

이름기능
Authentication소셜, 이메일, 핸드폰 인증 등의 인증 기능 연동
App Check보안 기능으로 허가되지 않은 클라이언트가 API 요청을 해 리소스를 낭비하는 걸 막을 수 있다.
Firestore실시간으로 클라이언트와 서버의 데이터를 연동할 수 있고 강력한 쿼리기능을 제공하는 NoSQL DB
Realtime DatabaseFirestore와 비슷한 NoSQL로 빠른 속도와 효율에 초점이 맞춰져 있다.
Sorage이미지, 오디오, 비디오 등 사용자 생성하는 콘텐츠를 저장할 수 있는 저장소
Hosting웹 앱, 정적 및 동적 콘텐츠, 마이크로서비스를 빠르고 안정적으로 호스팅 할 수 있다.
Functions파이어베이스 또는 HTTPS 요청에 의해 서버 코드를 실행할 수 있는 기능, 트래픽에 따라 필요한만큼 자동으로 확장되기 때문에 인프라 관리가 필요 없다.
Machine Learning간단한 몇 줄의 코드만으로 텍스트 인식, 이미지 라벨링, 물체 감지 및 추적, 얼굴 감지 및 윤곽 추적 등 머신러닝 기능 사용
Remote Config앱의 동작 모양과 기능을 앱을 새로 배포하지 않고도 변경하도록 편의성 기능 제공
Crashlytics앱에 충돌이 있을 경우 정확한 문제와 로그를 파악할 수 있는 기능
Performance앱 성능을 모니터링 할 수 있는 기능
Test Lab구글 데이터 센터에 실행되고 있는 여러 실제 프로덕션 기기를 사용해 앱을 테스트할 수 있다.
App Distribution앱을 더 빠르고 쉽게 배로할 수 있다.
Analytics앱의 트래픽과 사용자의 활동 등을 모니터링하고 분석할 수 있다.
Messaging푸시 알림을 쉽게 설정할 수 있다.

19.2 사전준비

19.2.1 pubspec.yaml 파일 설정하기

dependencies:
	cloud_firestore: ^4.14.0
	firebase_core: ^2.24.2

19.2.2 안드로이드 네이티브 설정하기

minSdkVersion을 19로 설정

// android/app/build.gradle

defaultConfig {
    minSdkVersion 19
}

19.2.3 파이어베이스 CLI 설치 및 로그인

맥OS

  • 아래 커맨드 입력
    curl -sL https://firebase.tools | bash
  • 로그인 진행
    firebase login

윈도우

19.2.4 프로젝트에 파이어베이스 설정하기

  • FlutterFire CLI 설치 프로젝트 내에 terminal에서 아래 명령어 입력
    dart pub global activate flutterfire_cli
    아래와 같은 경고 문구가 보이면 다시 아래 작업 후 다시 진행
    A web search for "configure windows path" will show you how
    • 윈도우 환경변수 Path에 C:\Users\{윈도우 사용자이름}\AppData\Local\Pub\Cache\bin을 추가하고 Visualstudio Code를 재실행 후 다시 FlutterFire CLI를 설치
    • 환경변수에 $HOME/.pub-cache/bin 을 추가
  • 파이어베이스 Console 접속
  • 프로젝트 이름을 입력, Google 애널리틱스도 사용하여 프로젝트 생성
  • 플러터 프로젝트에 파이어베이스 적용
    • firebase-tools 설치

      npm install -g firebase-tools
    • 프로젝트 연결

      ```bash
      flutterfire configure -p <프로젝트 ID>
      ```

      프로젝트가 실행이 완료되면 lib/firebase_options.dart파일이 생성된다.

19.2.5 파이어스토어 데이터베이스 생성하기

  1. 파이어베이스 콘솔에서 프로젝트 설정을 선택

  2. 기본 GCP 리소스 위치를 가까운 지역으로 선택

  3. 왼쪽 메뉴에서 빌드 클릭 → Firestore Database 클릭

  4. 데이터베이스 만들기 버튼 클릭

  5. 위치 선택 (asia-northeast3) → 테스트모드에서 시작 선택 후 만들기

    아래와 같은 에러가 나왔지만 Google Clout 콘솔로 이동 하니 정상적으로 생성이 됬다.

💡 Tips: 여기서 생성이 됬더라도 데이터베이스가 Datastore모드로 생성이 됬기때문에 정상적으로 사용이 불가능하다 따라서 Native모드로 변경하면 정상적으로 사용할 수 있다.

19.3 구현하기

19.3.1 main.dart

// lib/main.dart

import 'package:calendar_scheduler_firebase/firebase_options.dart';
import 'package:calendar_scheduler_firebase/screen/home_screen.dart';
import 'package:firebase_core/firebase_core.dart';
import 'package:flutter/material.dart';
import 'package:intl/date_symbol_data_local.dart';

void main() async {
  WidgetsFlutterBinding.ensureInitialized();

  // 파이어베이스 프로젝트 설정 함수
  await Firebase.initializeApp(options: DefaultFirebaseOptions.currentPlatform);

  // intl 패키지 초기화(다국어화)
  await initializeDateFormatting("ko_kr", null);

  // Provider 하위 위젯에 제공하기
  runApp(MaterialApp(debugShowCheckedModeBanner: false, home: HomeScreen()));
}

19.3.2 schedule_bottom_sheet.dart

// lib/component/schedule_bottom_sheet.dart

import 'package:calendar_scheduler_firebase/component/custom_text_field.dart';
import 'package:calendar_scheduler_firebase/const/colors.dart';
import 'package:calendar_scheduler_firebase/model/schedule_model.dart';
import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:flutter/material.dart';
import 'package:uuid/uuid.dart';

class ScheduleBottomSheet extends StatefulWidget {
  // 선택된 날짜 상위 위젯에서 입력받기
  final DateTime selectedDate;
  const ScheduleBottomSheet({required this.selectedDate, super.key});

  
  State<ScheduleBottomSheet> createState() => _ScheduleBottomSheetState();
}

class _ScheduleBottomSheetState extends State<ScheduleBottomSheet> {
  final GlobalKey<FormState> formKey = GlobalKey();

  int? startTime; // 시작 시간 저장 변수
  int? endTime; // 종료 시간 저장 변수
  String? content; // 일정 내용 저장 변수

  
  Widget build(BuildContext context) {
    // 키보드 높이 가져오기
    final bottomInset = MediaQuery.of(context).viewInsets.bottom;
    return Form(
      key: formKey,
      child: SafeArea(
        child: Container(
          // 화면의 반을 차지
          height: MediaQuery.of(context).size.height / 2 + bottomInset,
          color: Colors.white,
          child: Padding(
            padding:
                // 패딩에 키보드 높이를 추가해서 위젯 전반적으로 위로 올려주기
                EdgeInsets.only(left: 8, right: 8, top: 8, bottom: bottomInset),
            child: Column(
              children: [
                Row(
                  children: [
                    Expanded(
                        child: CustomTextField(
                      label: "시작 시간",
                      isTime: true,
                      onSaved: (String? val) {
                        // 저장이 실행되면 startTime 변수에 텍스트 필드 값 저장
                        startTime = int.parse(val!);
                      },
                      validator: timeValidator,
                    )),
                    SizedBox(
                      width: 16.0,
                    ),
                    Expanded(
                        child: CustomTextField(
                      label: "종료 시간",
                      isTime: true,
                      onSaved: (String? val) {
                        // 저장이 실행되면 endTime 변수에 텍스트 필드 값 저장
                        endTime = int.parse(val!);
                      },
                      validator: timeValidator,
                    )),
                  ],
                ),
                const SizedBox(
                  height: 8.0,
                ),
                Expanded(
                    child: CustomTextField(
                  label: "내용",
                  isTime: false,
                  onSaved: (String? val) {
                    // 저장이 실행되면 content 변수에 텍스트 필드 값 저장
                    content = val;
                  },
                  validator: contentValidator,
                )),
                SizedBox(
                  width: double.infinity,
                  child: ElevatedButton(
                    onPressed: () => onSavePressed(context),
                    style: ElevatedButton.styleFrom(
                      shape: RoundedRectangleBorder(
                          borderRadius: BorderRadius.circular(1)),
                      backgroundColor: PRIMARY_COLOR,
                    ),
                    child: const Text(
                      "저장",
                      style: TextStyle(color: Colors.white),
                    ),
                  ),
                )
              ],
            ),
          ),
        ),
      ),
    );
  }

  void onSavePressed(BuildContext context) async {
    if (formKey.currentState!.validate()) {
      formKey.currentState!.save();

      // 스케줄 모델 생성하기
      final schedule = ScheduleModel(
          id: Uuid().v4(),
          content: content!,
          date: widget.selectedDate,
          startTime: startTime!,
          endTime: endTime!);

      // 스케줄 모델 파이어스토어에 삽입하기
      await FirebaseFirestore.instance
          .collection('schedule')
          .doc(schedule.id)
          .set(schedule.toJson());

      // 일정 생성 후 화면 뒤로가기
      Navigator.of(context).pop();
    }
  }

  String? timeValidator(String? val) {
    if (val == null) {
      return "값을 입력해주세요.";
    }
    int? number;
    try {
      number = int.parse(val);
    } catch (e) {
      return "숫자를 입력해주세요.";
    }

    if (number < 0 || number > 24) {
      return "0시부터 24시 사이를 입력해주세요.";
    }
    return null;
  }

  String? contentValidator(String? val) {
    if (val == null || val.length == 0) {
      return "값을 입력해주세요.";
    }
    return null;
  }
}

19.3.3 home_screen.dart

// lib/screen/home_screen.dart

import 'package:calendar_scheduler_firebase/component/main_calendar.dart';
import 'package:calendar_scheduler_firebase/component/schedule_bottom_sheet.dart';
import 'package:calendar_scheduler_firebase/component/schedule_card.dart';
import 'package:calendar_scheduler_firebase/component/today_banner.dart';
import 'package:calendar_scheduler_firebase/const/colors.dart';
import 'package:calendar_scheduler_firebase/model/schedule_model.dart';
import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:flutter/material.dart';

class HomeScreen extends StatefulWidget {
  
  State<HomeScreen> createState() => _HomeScreenState();
}

class _HomeScreenState extends State<HomeScreen> {
  DateTime selectedDate = DateTime.utc(
    // ➋ 선택된 날짜를 관리할 변수
    DateTime.now().year,
    DateTime.now().month,
    DateTime.now().day,
  );

  
  Widget build(BuildContext context) {
    return Scaffold(
      floatingActionButton: FloatingActionButton(
        shape: const RoundedRectangleBorder(
            borderRadius: BorderRadius.all(Radius.circular(30.0))),
        backgroundColor: PRIMARY_COLOR,
        onPressed: () {
          showModalBottomSheet(
              context: context,
              isDismissible: true, // 배경 탭 했을때 화면닫기
              builder: (_) => ScheduleBottomSheet(
                    selectedDate: selectedDate,
                  ),
              // BottomSheet의 높이를 화면의 최대 높이로 정의하고 스크롤 가능하게 변경
              isScrollControlled: true);
        },
        child: const Icon(Icons.add),
      ),
      body: SafeArea(
          child: Column(
        children: [
          MainCalendar(
            selectedDate: selectedDate,
            onDaySelected: (selectedDate, focusedDate) =>
                onDaySelected(selectedDate, focusedDate, context),
          ),
          const SizedBox(
            height: 8.0,
          ),
          StreamBuilder<QuerySnapshot>(
            // ListView에 적용했던 같은 쿼리
            stream: FirebaseFirestore.instance
                .collection(
                  'schedule',
                )
                .where("date",
                    isEqualTo:
                        "${selectedDate.year}${selectedDate.month.toString().padLeft(2, "0")}${selectedDate.day.toString().padLeft(2, "0")}")
                .snapshots(),
            builder: (context, snapshot) {
              return TodayBanner(
                selectedDate: selectedDate,

                // ➊ 개수 가져오기
                count: snapshot.data?.docs.length ?? 0,
              );
            },
          ),
          const SizedBox(
            height: 8.0,
          ),
          Expanded(
            child: StreamBuilder<QuerySnapshot>(
              // ➊ 파이어스토어로부터 일정 정보 받아오기
              stream: FirebaseFirestore.instance
                  .collection(
                    'schedule',
                  )
                  .where("date",
                      isEqualTo:
                          "${selectedDate.year}${selectedDate.month.toString().padLeft(2, "0")}${selectedDate.day.toString().padLeft(2, "0")}")
                  .snapshots(),
              builder: (context, snapshot) {
                // Stream을 가져오는 동안 에러가 났을 때 보여줄 화면
                if (snapshot.hasError) {
                  return Center(
                    child: Text('일정 정보를 가져오지 못했습니다.'),
                  );
                }

                // 로딩 중일 때 보여줄 화면
                if (snapshot.connectionState == ConnectionState.waiting) {
                  return Container();
                }

                final schedules = snapshot.data!.docs
                    .map((e) => ScheduleModel.formJson(
                        json: (e.data() as Map<String, dynamic>)))
                    .toList();

                return ListView.builder(
                  itemCount: schedules.length,
                  itemBuilder: (context, index) {
                    final schedule = schedules[index];

                    return Dismissible(
                      key: ObjectKey(schedule.id),
                      direction: DismissDirection.startToEnd,
                      onDismissed: (DismissDirection direction) {
                        FirebaseFirestore.instance
                            .collection('schedule')
                            .doc(schedule.id)
                            .delete();
                      },
                      child: Padding(
                        padding: const EdgeInsets.only(
                            bottom: 8.0, left: 8.0, right: 8.0),
                        child: ScheduleCard(
                          startTime: schedule.startTime,
                          endTime: schedule.endTime,
                          content: schedule.content,
                        ),
                      ),
                    );
                  },
                );
              },
            ),
          ),
        ],
      )),
    );
  }

  void onDaySelected(
      DateTime selectedDate, DateTime focusedDate, BuildContext context) {
    setState(() => this.selectedDate = selectedDate);
  }
}
profile
업무하면서 쌓인 노하우를 정리하는 블로그🚀 풀스택 개발자를 지향하고 있습니다👻

0개의 댓글