지난 포스팅에서는 콘솔에서 직접 데이터베이스를 생성해서 데이터를 입력해봤습니다. 이번 포스팅에서는 flutter todo앱을 만들어서 앱에서 직접 Cloud Firebase에 접근해서 데이터를 CRUD하는 법에 대해서 알아보겠습니다.
CRUD는 데이터베이스의 데이터를 접근하는 방법들입니다. 즉, 데이터를 생성(Create), 읽기(Read), 수정(Update), 삭제(Delete)를 하는 것을 의미합니다.
일단 앱에서 데이터베이스를 사용하려면, 데이터베이스에 접근할 수 있는지를 알아야겠죠. 그래서 데이터베이스의 데이터를 읽어오는 것을 먼저 할겁니다. 이전 포스팅에서 생성한 데이터는 모두 지웠습니다. 대신, 새로운 데이터 todo를 생성할겁니다.
콘솔에서 Todos라는 컬렉션을 만들고 하위 도큐멘트로 todo정보들을 넣어줄겁니다. todo, isDone, time 필드를 생성해서 각각 할일, 진행상태, 생성시간 정보를 담는 데이터베이스입니다.
Read 테스트를 위한 데이터 준비가 끝났습니다. 이제 flutter 프로젝트에서 Cloud Firestore를 사용해볼까요? 그전에 우리는 todo앱 형식으로 데이터를 불러올겁니다. 화면에서는 현재 데이터베이스의 저장되어있는 todo 목록을 불러와서 ListView 형식으로 보여주겠습니다. 그렇다면 사전 UI 제작을 해야겠죠? 일단, 파이어베이스에 등록한 프로젝트를 실행시킵니다.
Cloud Firestore를 사용하기 위해서는 cloud_firestore패키지를 적용시켜야 합니다. 그래서 터미널을 이용해서 cloud_firestore패키지를 다운로드 받겠습니다.
$flutter pub add cloud_firestore
이제 flutter clean 후 앱을 재실행 시켜야하는데, 선택적인 사항으로 Xcode에서 빌드과정이 조금 지연된다고 합니다. 그래서
podfile에 다음 구문을 추가해주면 미리 컴파일된 파일을 사용하는 방법이라고 하네요.
# ...
target 'Runner' do
pod 'FirebaseFirestore', :git => 'https://github.com/invertase/firestore-ios-sdk-frameworks.git', :tag => '8.15.0'
# ...
end
이후, flutter clean을 해줘야 합니다. 그리고 만약 위 설정도 하고 ios앱으로 빌드를 한다고 하면, gem install cocoapods 명령을 해서 cocoapods의 버전을 최신버전으로 업그레이드 시켜야 합니다. 여기서, 에러가 발생하는 사람들은 보통 m1 사용자들의 에러인데, 간단하게 해결할 수 있습니다.
- podfile.lock 지우기
- gem install ffi
- cd ios -> pod install
이랬는데도 에러뜬다 하면
- sudo arch -x86_64 install ffi
- cd ios -> arch -x86_64 pod install
이러면 해결할 수 있을겁니다. 이제 프로젝트를 실행해야 합니다.
$flutter clean
그리고 디버깅 없이 실행을 하면 !
저는 macOS를 사용하고 있어서, 설정을 해주니까 빌드속도가 정말 빨라진게 체감이 됩니다. 보통 몇분씩 걸렸는데 34초만에 빌드가 되었어요.
UI 제작
우선, Cloud Firebase의 데이터들을 화면으로 불러와야 합니다. Cloud Firebase의 강점은 Stream 타입의 데이터로 불러올 수 있어서, 실시간 랜더링이 가능합니다. lib>src>page순으로 디렉터리를 먼저 만들어주겠습니다. 그리고 src 디렉터리에는 app.dart를 생성해줍니다.
app.dart파일은 데이터를 가져온 화면을 구성해줘야 합니다. 우선은, Stateless 위젯으로 페이지를 구성하겠습니다. 아래는 App클래스 소스코드입니다.
import 'package:flutter/material.dart';
class App extends StatelessWidget {
const App({super.key});
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Todo 앱'),
),
body: Container(),
);
}
}
이제 main.dart파일에서 MyApp 클래스에서 기본적으로 생성된 카운터 앱 화면 클래스를 전체 삭제하고, home에 App()을 전달합니다.
...
class MyApp extends StatelessWidget {
const MyApp({super.key});
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: const App(),
);
}
}
...
이제 핫리로딩 시키면 구성한 UI가 등장합니다.
이제 데이터를 읽어올 차례입니다. 전체적인 body를 구성해야 하는데요. Create기능을 위해서 상단에는 TextField, ElavatedButton을 놓을거고, 나머지 부분은 todo 목록을 불러오게 디자인하겠습니다. 한번에 body에 Column을 이용해서 위젯을 나열하면, 랜더링은 상관 없지만, 코드가 굉장히 가독성이 떨어질 것 같습니다. 그래서 메소드로 상단, 몸 부분으로 분리하겠습니다.
build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Todo 앱'),
),
body: Column(
children: [
_buildTop(),
_buildBody(),
],
),
);
}
Widget _buildTop() {
return Container();
}
Widget _buildBody() {
return Container();
}
}
Widget
이렇게 하면, 복잡한 UI에 대한 코드가 좀더 가독성이 좋게될 것 같습니다. 일단 _buildTop()은 건들지 않고, _buildBody()메소드를 이용해서 데이터를 불러오겠습니다. StreamBuilder를 이용해서 말이죠.
Widget _buildBody() {
return StreamBuilder(
stream: ,
builder: (context, snapshot) {
return Container();
});
}
StreamBuilder는 stream값을 이용한 builder가 있습니다. 여기 stream에 데이터를 불러오는 stream데이터를 집어넣으면 됩니다.
FirebaseFirestore.instance.collection('Todos').snapshots()
이 값을 전달해주면 현재 파이어베이스에 등록된 Cloud Firestore 데이터베이스에서 Todos 컬렉션의 모든 데이터를 스냅샷으로 가져올 수 있습니다.
Widget _buildBody() {
return StreamBuilder(
stream: FirebaseFirestore.instance.collection('Todos').snapshots(),
builder: (context, snapshot) {
return Container();
});
}
이제 임의로 반환한 Container위젯을 지우고, 실제로 데이터는 ListView형식으로 가져올 것이기 때문에, ListView.builder를 전달합니다.
Widget _buildBody() {
return StreamBuilder<QuerySnapshot>(
stream: FirebaseFirestore.instance.collection('Todos').snapshots(),
builder: (context, snapshot) {
return ListView.builder(
itemCount: ,
itemBuilder: (context, index) {
return Container();
});
});
}
ListView.builder는 itemCount가 필요하죠. 여기서는 컬렉션의 하위 도큐멘트의 갯수를 전달해줘야 합니다. 지금 컬렉션 정보는 snapshot을 통해서 가져올 수 있어요. snapshot이 가진 data에서 문서들의 길이가 곧 데이터의 갯수입니다.
Widget _buildBody() {
return StreamBuilder<QuerySnapshot>(
stream: FirebaseFirestore.instance.collection('Todos').snapshots(),
builder: (context, snapshot) {
return ListView.builder(
itemCount: snapshot.data!.docs.length,
itemBuilder: (context, index) {
return Container();
});
});
}
data는 null값이면 성립하지 않습니다. 따라서 !를 통해서 null이 아님을 명시합니다. 음 이상태로 데이터가 오는지 바로 확인하려는데, Streambuilder는 말 그대로 현재의 상황을 바로바로 화면에 전달해버립니다. 데이터베이스에서 데이터를 가져올려면 시간이 조금 필요하죠. 만약 현재 상태가 데이터를 받지 못했다면, 빨갛게 화면이 변하면서 에러가 등장할겁니다. 그래서 삼항연산을 통해서 데이터를 가져오는 중이라면 로딩을 보여줄게요.
Widget _buildBody() {
return StreamBuilder<QuerySnapshot>(
stream: FirebaseFirestore.instance.collection('Todos').snapshots(),
builder: (context, snapshot) {
return (snapshot.connectionState == ConnectionState.waiting)
? const Center(
child: CircularProgressIndicator(),
)
: ListView.builder(
itemCount: snapshot.data!.docs.length,
itemBuilder: (context, index) {
return Container();
});
});
}
snapshot.connectionState는 현재 데이터가 전달되는 중인지를 판별할 수 있습니다. 이 값이 ConnectionState.waiting 즉, 기다리는 중이라면 로딩을 보여주게끔 코드를 짰습니다. 이제 Container 대신, ListTile위젯을 이용해서 데이터를 가져올게요.
return ListTile(
title: Text(todo),
leading: (isDone)
? const Icon(Icons.done)
: const Icon(Icons.close),
);
todo는 데이터베이스에서 String, isDone은 boolean으로 지정해놨습니다. 그래서 내용을 보여주고 Icon 위젯으로 isDone을 구분할게요. isDone이 true면, 완료 아이콘, false면 완료하지 않은 아이콘이 삼항연산을 통해 나타날겁니다. 여기서 todo, isDone을 각각 정의해야겠죠? 각각의 index별로 도큐멘트의 데이터를 받을 수 있습니다. 현재의 도큐멘트 정보를 data라는 변수로 선언을 할게요.
: ListView.builder(
itemCount: snapshot.data!.docs.length,
itemBuilder: (context, index) {
final data = snapshot.data!.docs[index];
return ListTile(
title: Text(todo),
leading: (isDone)
? const Icon(Icons.done)
: const Icon(Icons.close),
);
});
이 data는 map의 자료형으로 데이터를 가져왔습니다. 그리고 우리가 지정한 필드가 곧 key이고, 데이터는 value입니다. 그러니까 todo와 isDone은 다음으로 표현될 수 있습니다.
: ListView.builder(
itemCount: snapshot.data!.docs.length,
itemBuilder: (context, index) {
final data = snapshot.data!.docs[index];
final todo = data['todo'].toString(); // todo 목록
final isDone = data['isDone']; // 진행상태
return ListTile(
title: Text(todo),
leading: (isDone)
? const Icon(Icons.done)
: const Icon(Icons.close),
);
});
자, 이제 준비는 다 끝났습니다. 핫리로딩 해보겠습니다.
그런데, 에러가 발생했습니다. 두가지 에러를 확인할 수 있네요. 하나는 랜더링 에러이고, 두번째는 데이터베이스에 접근하지 못해서 데이터를 받아오지 못한 에러입니다. 하나하나 해결합시다.
- 랜더링 에러
ListView는 Column내부에서 사용하려면, Expanded위젯으로 감싸주어야 합니다. 현재는 _buildBody()메소드가 랜더되는 것이기 때문에 메소드 내부에서 최상위 위젯을 Expanded로 감싸주어야 합니다. 바로 StreamBuilder죠.
- 데이터베이스 접근 에러
일단, 랜더링 에러는 해결이 되었네요. 다음 에러를 고쳐봅시다. 이건 처음에는 왜 발생하는지 못찾을 수 있습니다. 데이터베이스의 보안규칙을 모르면 말이죠. 처음으로 생성한 데이터베이스가 프로덕션 모드에서는 모든 접근을 제한하는 규칙이 적용됩니다. 콘솔로 가서 보안규칙을 확인해봅시다.
현재의 보안규칙이 보일겁니다.
...
allow read, write: if false;
...
if false; 구문을 삭제해서 일단 전체 공개로 해놓겠습니다.
이제 데이터베이스 접근이 가능합니다 ! 앱을 핫리스타트 해보겠습니다.
데이터베이스의 내용을 아주 잘 갖고오는군요 ! 포스팅이 길어져서, 이번엔 여기서 끊고, 다음 포스팅에서 이어서 나머지 CRUD를 해보도록 하겠습니다.