[Flutter] Cloud Firestore-3

한상욱·2023년 3월 27일
0

Flutter with Firebase

목록 보기
4/10
post-thumbnail
post-custom-banner

들어가며

지난 포스팅에서는 Cloud Firestore에 저장된 데이터를 읽어오는 Read에 대해서 알아보았습니다. 이번 포스팅에서는 앱에서 직접 데이터를 생성(Create)하고, 저장된 데이터를 수정(Update)하고, 삭제(Delete)하는 법에 대해서 알아보겠습니다.

Create

앱에서 데이터를 Cloud Firestore에 생성하기 위해서는 UI에 조그마한 변화를 주어야 할 것 같습니다. 이전 포스팅의 소스코드를 먼저 보겠습니다.

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

  
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Todo 앱'),
      ),
      body: Column(
        children: [
          _buildTop(),
          _buildBody(),
        ],
      ),
    );
  }

  Widget _buildTop() { // 데이터베이스에 값을 생성하는 UI
    return Container();
  }

  Widget _buildBody() { // 데이터베이스에서 값을 가져온 UI
    ...

이전 소스코드에서 일부로 상단부분과 몸체를 분리시켰었습니다. _buildTop을 통해서 데이터베이스에 값을 생성할 수 있도록 하겠습니다. 우선, 반환되는 Container위젯을 지우고, Row위젯을 생성해주겠습니다.

  Widget _buildTop() {
    return Row(
      children: [
        
      ],
    );
  }

Row위젯을 통해서 값을 입력받는 TextField와 생성 기능을 수행할 ElevatedButton을 가로로 정렬시키도록 하겠습니다. 그렇다면, TextField와 ElevatedButton을 children으로 전달하면 되겠죠??

  Widget _buildTop() {
    return Row(
      children: [
        TextField(),
        ElevatedButton(onPressed: () {}, child: ),
      ],
    );
  }

우선, onPressed는 비워두고 child에 Text위젯으로 '생성'을 전달하겠습니다.

오류가 발생하네요. TextField가 Row위젯으로 여러 위젯들과 함께 정렬되면 사이즈로 인해서 오류가 발생합니다. TextField는 기본적으로 너비가 정의되지 않습니다. 그래서 이런 상황에서는 Flexible위젯으로 감싸주어야 TextField가 제대로 랜더링됩니다.

  Widget _buildTop() {
    return Row(
      children: [
        const Flexible(
          child: TextField(),
        ),
        ElevatedButton(onPressed: () {}, child: const Text('생성')),
      ],
    );
  }

기능만을 위한 UI이기 때문에 더 많은 데코레이션은 하지 않겠습니다. 이제 버튼을 통해서 데이터를 생성하면 되겠죠? 이때, TextField의 값으로 데이터를 생성할 것이므로, TextField의 TextEditingController를 전달해야 합니다. 가장 상단부에 TextEditingController를 정의해주겠습니다. controller의 값은 계속 변경될테니 stateful위젯으로 바꾸겠습니다.

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

  
  State<App> createState() => _AppState();
}

class _AppState extends State<App> {
  
  Widget build(BuildContext context) {

    return Scaffold(
      appBar: AppBar(
        title: const Text('Todo 앱'),
      ),
      body: Column(
        children: [
          _buildTop(),
          _buildBody(),
        ],
      ),

이제, controller를 정의합니다.

class _AppState extends State<App> {
  late final _todoController;

  
  void initState() {
    super.initState();
    _todoController = TextEditingController();
  }

  
  void dispose() {
    super.dispose();
    _todoController.dispose();
  }

그냥 final 타입으로 controller를 생성하게 되면, 페이지를 벗어날때도 controller가 삭제가 안되고 남아있습니다. 불필요한 메모리 차지는 없애는게 바람직하기에 dispose를 통해서 controller를 dispose시킵니다. 이제, textField의 값은 _todoController를 통해서 접근할 수 있습니다. 생성 기능을 위한 함수를 만들어볼까요?

  void _createTodos() {
    var db = FirebaseFirestore.instance;
    db.collection('Todos').add({
      'todo': _todoController.text,
      'time': Timestamp.now(),
      'isDone': false,
    });
  }

데이터를 생성하는 함수는 무엇을 반환할 필요가 없겠죠? 그래서 void 함수로 지정해줍니다. 위 형식을 통해서 데이터베이스에 값을 생성할 수 있습니다. 'time' 필드에 대해서 cloud_firestore패키지는 Timestamp를 제공해줍니다. 그걸 이용해서 현재 시간을 전달하고, 기본적으로 'isDone'필드는 false가 되도록하겠습니다. 이제 ElevatedButton의 onPressed로 전달해줍니다. 그리고 마찬가지로 TextField에 controller도 전달해줍니다.

Widget _buildTop() {
    return Row(
      children: [
        Flexible(
          child: TextField(
            controller: _todoController,
          ),
        ),
        ElevatedButton(onPressed: _createTodos, child: const Text('생성')),
      ],
    );
  }

한번 해볼까요? '게임하기'라는 Todo를 생성해보겠습니다.

잘 작동하는 것을 알 수 있습니다. 콘솔에서도 확인해볼까요?

콘솔에서도 역시 데이터가 추가된것을 확인할 수 있습니다.

Update

이번에는 수정을 해보겠습니다. 수정을 하기 위해서 isDone필드가 false에서 true로, true에서 false로 변하게끔 해볼게요. 새로운 함수를 만들어줍니다.

  Future<void> _updateTodos(String docID, bool isDone) async {
    try {
      db.collection('Todos').doc(docID).update({
        'isDone': !isDone,
      });
    } on FirebaseException catch (e) {
      print(e.message);
    }
  }

생성과는 다르게 함수 타입이 Future죠? 이 함수 안에서 Future 타입인 update메소드를 사용하기 때문입니다. 수정을 위해서는 도큐멘트의 고유 id와 도큐멘트가 갖고있는 isDone필드의 boolean값이 필요합니다. 이번에는 try catch문을 통해서 예외처리를 해주었습니다. 이제 이 함수를 통해서 isDone필드를 수정할 수 있겠죠? 각각의 데이터에서 이 함수를 동작시키기 위해서 ListTile에 onTap프로퍼티에 전달하겠습니다.

: ListView.builder(
                    itemCount: snapshot.data!.docs.length,
                    itemBuilder: (context, index) {
                      final data = snapshot.data!.docs[index];
                      final id = data.id; //id 전달을 위해 선언
                      final todo = data['todo'].toString();
                      final isDone = data['isDone'];
                      return ListTile(
                        onTap: () {
                          _updateTodos(id, isDone); // 함수 전달
                        },
                        title: Text(todo),
                        leading: (isDone)
                            ? const Icon(Icons.done)
                            : const Icon(Icons.close),
                      );
                    });

이제 ListTile을 탭하게 되면, isDone필드의 값이 수정되어 leading부분의 아이콘이 변하겠죠?

수정이 잘 되고 있습니다. 콘솔에서도 마찬가지로 잘 수정되었습니다.

Delete

이번에는 데이터 삭제를 해보겠습니다. 수정과 굉장히 유사해요. 이 기능도 역시 도큐멘트의 id가 필요합니다. ListTile에서 trail부에 IconButton을 추가해서 클릭하면 삭제하게끔 만들어볼게요.

				return ListTile(
                        onTap: () {
                          _updateTodos(id, isDone);
                        },
                        title: Text(todo),
                        leading: (isDone)
                            ? const Icon(Icons.done)
                            : const Icon(Icons.close),
                        trailing: IconButton( //데이터 삭제 버튼
                          onPressed: ,
                          icon: Icon(Icons.delete_forever),
                        ),
                      );

이제 onPressed에 전달할 함수를 만들어보겠습니다.

  Future<void> _deleteTodos(String docID) async {
    try {
      db.collection('Todos').doc(docID).delete();
    } on FirebaseException catch (e) {
      print(e.message);
    }
  }

update 함수와 유사하면서 더 간단하죠? 삭제 기능은 이게 끝입니다. 이제 함수를 전달하겠습니다.

  			trailing: IconButton(
                          onPressed: () {
                            _deleteTodos(id); //데이터 삭제
                          },
                          icon: const Icon(Icons.delete_forever),
                        ),

잘 되는지 확인해볼까요?

역시 잘 삭제되었습니다. 콘솔에서도 확인해볼까요?

콘솔에서도 데이터가 삭제되어 하나만 남은 것을 알 수 있습니다. 이번 포스팅까지 CRUD였습니다. 다음 포스팅에서는 NoSQL에서 쿼리처럼 특수한 조건을 가진 데이터들을 조회하는 것에 대해서 알아보겠습니다.

profile
자기주도적, 지속 성장하는 모바일앱 개발자가 되기 위해
post-custom-banner

0개의 댓글