📌 Day 15: Riverpod을 활용한 간단한 CRUD 구현

2025. 3. 17. 13:00같이 공부합시다 - Flutter/Dart & Flutter 기초부터 실전까지

728x90

Flutter에서 할 일 목록(Todo List) 관리 기능을 구현하며, Riverpod을 활용한 CRUD(Create, Read, Update, Delete) 상태 관리

StateNotifierProvider를 사용하여 리스트 형태의 데이터를 관리하고, UI에서 추가, 수정, 삭제 기능을 적용
 
 


 
 
 


🔔 주제

🔸 Riverpod을 활용한 CRUD 상태 관리
🔸 StateNotifierProvider를 사용하여 리스트 데이터 관리
🔸 할 일 목록 추가(Create), 조회(Read), 수정(Update), 삭제(Delete) 구현
🔸 UI에서 데이터를 반영하고 리스트를 동적으로 업데이트
 
 
 
 
 


1️⃣ Riverpod을 활용한 CRUD 상태 관리

🔸 CRUD란?

CRUD는 가장 기본적인 데이터 처리 방식입니다.
🔹Create(생성) → 새로운 데이터를 추가
🔹Read(조회) → 저장된 데이터를 가져오기
🔹Update(수정) → 기존 데이터를 변경
🔹Delete(삭제) → 데이터를 제거
 

🔸Riverpod을 활용하는 이유

Flutter에서 여러 화면에서 데이터를 공유하거나 상태를 관리하려면 Provider 또는 Riverpod을 사용해야 합니다.
Riverpod을 사용하면 더 직관적이고 안전한 방식으로 데이터를 관리할 수 있습니다.
 

구현할 기능:

  • 할 일 추가(Create) → 새로운 할 일을 목록에 추가
  • 할 일 조회(Read) → 현재 할 일 목록을 UI에 표시
  • 할 일 수정(Update) → 특정 할 일의 내용을 변경
  • 할 일 삭제(Delete) → 완료된 할 일을 목록에서 제거

 
 
 
 


2️⃣ 프로젝트 설정 및 Riverpod 패키지 설치

Riverpod을 사용하려면 패키지를 설치해야 합니다.
 
📌 터미널에서 아래 명령어 실행

flutter pub add flutter_riverpod

 
📌 설치 완료 후 pubspec.yaml에 아래 내용이 추가되었는지 확인

dependencies:
  flutter:
    sdk: flutter
  flutter_riverpod: ^2.6.1  # 2025-03-12 기준 최신 버전

 
📌 프로젝트 구조

lib/
│── providers/todo_provider.dart  # Riverpod을 활용한 상태 관리
│── screens/todo_screen.dart    # UI 및 사용자 입력 처리
│── main.dart

 
 
 
 
 
 


3️⃣ main.dart 코드

Flutter에서 Riverpod을 사용하여 할 일 목록(Todo List)을 관리하는 main.dart 코드입니다.
ProviderScope를 설정하여 todoProvider를 사용할 수 있도록 구성합니다.
앱의 첫 화면을 TodoScreen으로 설정합니다.
 

📌 main.dart 코드

import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'screens/todo_screen.dart';

void main() {
  runApp(const ProviderScope(child: MyApp())); // ✅ Riverpod 사용을 위한 ProviderScope 추가
}

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

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      debugShowCheckedModeBanner: false, // 디버그 배너 제거
      title: 'SteadyBuilder Todo App',
      theme: ThemeData(
        colorScheme: ColorScheme.fromSeed(useMaterial3: true, seedColor: Colors.amber), // 앱 기본 테마 색상 설정
      ),
      home: const TodoScreen(), // ✅ 첫 화면을 TodoScreen으로 설정
    );
  }
}

 
 
🔔  코드 설명
ProviderScope(child: MyApp())

  • ProviderScope는 Riverpod을 사용하기 위해 반드시 필요한 설정입니다.
  • 모든 Provider(예: todoProvider)를 앱 전체에서 사용할 수 있도록 전역 상태를 관리합니다.
  • 이 설정이 없으면 ref.watch()나 ref.read()를 사용할 수 없습니다.

 
 
 
 
 


4️⃣ StateNotifierProvider를 사용하여 리스트 데이터 관리

 

1. Todo 모델 만들기

할 일 목록을 관리하려면, 할 일(Todo) 데이터를 저장하는 모델이 필요합니다.
 

📌 todo_provider.dart 에 모델 추가

class Todo {
  final String id; // 각 할 일의 고유 ID
  final String title; // 할 일의 제목
  bool isDone; // 완료 여부

  Todo({required this.id, required this.title, this.isDone = false});
}

id → 할 일을 식별하기 위한 고유값 (DateTime을 활용)
title → 사용자가 입력한 할 일 제목
isDone → 할 일이 완료되었는지 여부 (체크박스 관리)
 
💡 id가 필요한 이유
🔸 같은 제목을 가진 할 일이 여러 개 있을 수 있습니다.
🔸id 없이 특정 할 일을 수정하거나 삭제할 경우, 동일한 제목을 가진 모든 할 일이 변경될 수 있습니다.
🔸예 :

Todo(title: "운동하기"); // 1번 항목
Todo(title: "운동하기"); // 2번 항목

→ 어떤 '운동하기'를 삭제해야 할까? id 없이는 구분 불가.
🔸 이와 같이 정확한 항목을 특정할 수 있으므로 수정(업데이트)하거나 삭제할 때 id 값을 사용합니다.
 
 
 

2. 할 일 목록을 관리하는 StateNotifier 구현

Riverpod에서는 StateNotifier를 사용하여 상태를 관리합니다.
여기서는 StateNotifier<List<Todo>>를 사용하여 할 일 목록을 관리합니다.
 

📌 todo_provider.dart 에 StateNotifier 추가

import 'package:flutter_riverpod/flutter_riverpod.dart';

// 할 일 데이터 모델
class Todo {
  final String id; // 각 할 일의 고유 ID
  final String title; // 할 일의 제목
  bool isDone; // 완료 여부

  Todo({required this.id, required this.title, this.isDone = false});
}

// StateNotifier를 사용한 Todo 리스트 관리
class TodoNotifier extends StateNotifier<List<Todo>> {
  TodoNotifier() : super([]);

  // 할 일 추가
  void addTodo(String title) {
    final newTodo = Todo(id: DateTime.now().toString(), title: title);
    state = [...state, newTodo];
  }

  // 할 일 상태 업데이트 (완료 처리)
  void toggleTodo(String id) {
    state = state.map((todo) {
      if (todo.id == id) {
        return Todo(id: todo.id, title: todo.title, isDone: !todo.isDone);
      }
      return todo;
    }).toList();
  }

  // 할 일 삭제
  void deleteTodo(String id) {
    state = state.where((todo) => todo.id != id).toList();
  }
}

// StateNotifierProvider로 상태 등록
final todoProvider = StateNotifierProvider<TodoNotifier, List<Todo>>((ref) {
  return TodoNotifier();
});

🔔 코드 설명
✅ Todo 클래스 → 할 일의 ID, 제목, 완료 여부를 저장하는 데이터 모델
✅ TodoNotifier → StateNotifier<List<Todo>>를 상속받아 할 일 목록을 관리
✅ addTodo() → 새로운 할 일을 추가
✅ toggleTodo() → 완료 여부를 변경
✅ deleteTodo() → 할 일을 삭제
✅ todoProvider → Riverpod의 StateNotifierProvider를 사용하여 상태를 등록
 

🔥 class TodoNotifier 코드 상세 설명

🔔StateNotifier<List<Todo>>란?

class TodoNotifier extends StateNotifier<List<Todo>> {

✅ StateNotifier<T>는 Riverpod에서 상태를 관리하기 위한 클래스입니다.
✅ 여기서 T는 상태로 사용할 데이터 타입을 의미합니다.
✅ StateNotifier<List<Todo>>이므로, 할 일 목록을 관리하는 역할을 하게 됩니다.
 
📌super([])는 무엇을 의미할까?

TodoNotifier() : super([]);

✅ super([])는 초기 상태를 빈 리스트([])로 설정하는 코드입니다.
✅ 즉, TodoNotifier가 처음 생성될 때 할 일 목록이 없는 상태로 시작합니다.
 
📌 StateNotifier를 사용하는 이유
Flutter에서 상태를 관리하는 방법 중 하나
기존의 setState()를 대체하여 상태를 더 효율적으로 관리
앱의 여러 부분에서 같은 상태를 공유할 수 있도록 함
 

🔔 addTodo(String title) 코드 상세 설명

이 함수는 사용자가 새로운 할 일을 입력하면 목록에 추가하는 역할을 합니다.
사용자가 할 일을 입력하고 추가 버튼을 클릭하면 새로운 Todo 객체가 생성되어 리스트에 추가됩니다.
 
📌 함수 전체 코드

void addTodo(String title) {
  final newTodo = Todo(id: DateTime.now().toString(), title: title);
  state = [...state, newTodo];
}

 
1️⃣ final newTodo = Todo(...)

final newTodo = Todo(id: DateTime.now().toString(), title: title);

✅ Todo 모델을 기반으로 새로운 할 일 객체를 만듭니다.
✅ id: DateTime.now().toString() → 현재 시간을 문자열로 변환하여 고유한 ID 생성
✅ title: title → 사용자가 입력한 제목을 저장
✅ isDone 값은 기본값 false (미완료 상태)로 설정됨
 
💡 예제:
사용자가 "책 읽기"를 입력하면 다음과 같은 Todo 객체가 생성됩니다.

Todo(id: "2024-03-12 14:30:45.123456", title: "책 읽기", isDone: false)

 
2️⃣ state = [...state, newTodo];

state = [...state, newTodo];

✅ state는 현재 할 일 목록을 저장하는 리스트입니다.
✅ 기존 할 일 리스트에 새로운 newTodo를 추가하는 코드입니다.
✅ 여기서 newTodo 는 방금 전에 final 로 만든 새로운 Todo 입니다.
 
💡 설명:

  • state는 Riverpod의 StateNotifier에서 관리하는 상태값 ⇒ 할 일 목록 리스트(List) 입니다.
  • ...state는 기존 리스트의 모든 항목을 복사하여 유지합니다.
  • [...state, newTodo] → 기존 리스트를 유지하면서 newTodo를 추가한 새로운 리스트를 만든다는 의미입니다.

 
📢 중요: state를 직접 수정하는 것이 아니라, 새로운 리스트를 만들어서 state에 할당해야 합니다. (Flutter에서는 상태를 불변(immutable)하게 유지하는 것이 중요합니다.)
 
3️⃣ 동작 방식
 
기존 할 일 목록:

[
  Todo(id: "2024-03-12 14:30:45.123456", title: "운동하기", isDone: false),
  Todo(id: "2024-03-12 14:32:48.123456", title: "책 읽기", isDone: false),
]

 
사용자가 "Flutter 공부하기" 추가 → addTodo("Flutter 공부하기") 실행 후 상태 변경:

[
  Todo(id: "2024-03-12 14:30:45.123456", title: "운동하기", isDone: false),
  Todo(id: "2024-03-12 14:32:48.123456", title: "책 읽기", isDone: false),
  Todo(id: "2024-03-12 14:34:21.123456", title: "Flutter 공부하기", isDone: false), // 추가됨!
]

 
✅ 새로운 할 일이 기존 리스트에 추가되었고, 화면이 업데이트됩니다.
 

🔔 toggleTodo(String id) 코드 상세 설명

이 함수는 특정 id를 가진 할 일(Todo)의 완료 상태를 변경하는 역할을 합니다.
즉, 사용자가 체크박스를 클릭하면 isDone 값이 true → false 또는 false → true로 변경됩니다.
 
📌 함수 전체 코드

void toggleTodo(String id) {
  state = state.map((todo) {
    if (todo.id == id) {
      return Todo(id: todo.id, title: todo.title, isDone: !todo.isDone);
    }
    return todo;
  }).toList();
}

state.map((todo) { ... }).toList();

  • map() 함수를 사용하여 리스트의 모든 항목을 검사합니다.
  • id가 일치하는 항목만 상태를 변경하고, 나머지는 그대로 둡니다.
  • 최종적으로 .toList();를 사용하여 새로운 리스트로 변환하여 state에 저장합니다.
  • ⭐ Flutter에서는 기존 리스트를 직접 수정하지 않고, 항상 새로운 리스트를 만들어 state에 할당해야 합니다.

1️⃣ state.map((todo) { ... }).toList();

state = state.map((todo) {

  • 기존 할 일 리스트(state)의 각 항목을 검사합니다.
  • todo는 현재 리스트에서 가져온 **각 할 일 객체(Todo)**를 의미합니다.

2️⃣ 특정 id와 일치하는 항목 찾기

if (todo.id == id) {

  • 사용자가 클릭한 할 일의 id와 현재 todo의 id를 비교합니다.
  • id가 일치하는 경우, 완료 상태를 변경해야 합니다.

3️⃣ 새로운 Todo 객체를 생성하여 완료 상태 변경

return Todo(id: todo.id, title: todo.title, isDone: !todo.isDone);

  • 새로운 Todo 객체를 생성합니다.
  • isDone: !todo.isDone
    • true → false,
    • false → true
    • 즉, 체크박스를 클릭할 때마다 상태가 반전됩니다.

4️⃣ 기존 할 일 유지

return todo;

  • id가 일치하지 않는 경우 기존 할 일을 그대로 반환합니다.

5️⃣ 최종적으로 toList()를 사용하여 새로운 리스트 생성

}).toList();

  • map() 함수는 리스트가 아니라 Iterable(반복 가능한 데이터 구조)을 반환합니다.
  • .toList()를 사용하여 새로운 리스트(List)로 변환한 후 state에 저장합니다.

6️⃣ 동작 방식 (예제)


초기 할 일 목록 (state)

[
  Todo(id: "2024-03-12 14:30:45.123456", title: "운동하기", isDone: false),
  Todo(id: "2024-03-12 14:32:48.123456", title: "책 읽기", isDone: false),
]

사용자가 "운동하기"(id: “2024-03-12 14:30:45.123456")의 체크박스를 클릭
toggleTodo("2024-03-12 14:30:45.123456") 실행 후 상태 변화

[
  Todo(id: "2024-03-12 14:30:45.123456", title: "운동하기", isDone: true), // ✅ 변경됨
  Todo(id: "2024-03-12 14:32:48.123456", title: "책 읽기", isDone: false),
]

다시 "운동하기" 체크박스를 클릭하면 isDone이 false로 변경됨
🔔 deleteTodo(String id) 코드 상세 설명
이 함수는 특정 id를 가진 할 일(Todo)을 목록에서 삭제하는 역할을 합니다.
즉, 사용자가 삭제 버튼(🗑️)을 클릭하면 해당 항목이 리스트에서 제거됩니다.
📌 함수 전체 코드

void deleteTodo(String id) {
  state = state.where((todo) => todo.id != id).toList();
}

state.where((todo) => todo.id != id).toList();

  • where() 함수를 사용하여 리스트에서 특정 조건을 만족하는 항목만 유지합니다.
  • todo.id != id → 삭제하려는 id와 일치하지 않는 항목만 남깁니다.
  • 최종적으로 .toList();를 사용하여 새로운 리스트로 변환하여 state에 저장합니다.
  • ⭐ Flutter에서는 기존 리스트를 직접 수정하지 않고, 항상 새로운 리스트를 만들어 state에 할당해야 합니다. (중요하기 때문에 계속 반복 강조)

1️⃣ 특정 id를 가진 항목을 제외한 새로운 리스트 생성

state = state.where((todo) => todo.id != id).toList();

  • where() 함수는 주어진 조건을 만족하는 항목만 남겨 새로운 리스트를 반환하는 함수입니다.
  • todo.id != id → 삭제할 항목의 id와 일치하지 않는 항목들만 남깁니다.

💡 예제:
초기 할 일 목록 (state)
⚠️ id 가 너무 길어서 예제에서만 1, 2, 3 으로 대체합니다.

[
  Todo(id: "1", title: "운동하기", isDone: false),
  Todo(id: "2", title: "책 읽기", isDone: false),
  Todo(id: "3", title: "Flutter 공부", isDone: true),
]

사용자가 "책 읽기"(id: "2") 삭제 버튼 클릭 → deleteTodo("2") 실행 후 상태 변화

[
  Todo(id: "1", title: "운동하기", isDone: false),
  Todo(id: "3", title: "Flutter 공부", isDone: true),
]

id가 "2"인 항목이 삭제됨
 
2️⃣ 기존 방식(for 루프)과의 차이점
일반적인 for 루프를 사용한 삭제 방법

void deleteTodo(String id) {
  List<Todo> newList = [];
  for (var todo in state) {
    if (todo.id != id) {
      newList.add(todo);
    }
  }
  state = newList;
}

✔️ where()을 사용하면 위와 같은 for 루프 없이 더 간결하게 삭제 가능
✔️ 성능상 where()가 더 효율적이며, 유지보수가 쉬움
🔔 StateNotifierProvider로 상태 등록하는 코드 상세 설명
이 코드는 Riverpod에서 할 일 목록(Todo)의 상태를 관리하기 위해 StateNotifierProvider를 설정하는 부분입니다.
즉, 앱 내에서 todoProvider를 사용하면 TodoNotifier가 관리하는 상태(할 일 목록)를 접근하고 변경할 수 있습니다.
 
📌 해당 코드 분석

final todoProvider = StateNotifierProvider<TodoNotifier, List<Todo>>((ref) {
  return TodoNotifier();
});

StateNotifierProvider<TodoNotifier, List<Todo>>

  • StateNotifierProvider는 Flutter Riverpod에서 상태(State)를 전역적으로 관리하는 방법 중 하나입니다.
  • <TodoNotifier, List<Todo>> → TodoNotifier 클래스가 List<Todo> 타입의 상태를 관리한다는 의미입니다.

(ref) { return TodoNotifier(); }

  • ref는 Provider 내부에서 다른 Provider를 참조할 수 있도록 해주는 객체입니다.
  • return TodoNotifier(); → TodoNotifier를 생성하여 상태 관리를 시작합니다.

1️⃣ final todoProvider

final todoProvider = ...

  • todoProvider는 전역적으로 상태를 관리하는 Provider입니다.
  • 앱의 어느 곳에서든 todoProvider를 사용하여 할 일 목록을 불러오거나 업데이트할 수 있습니다.

2️⃣ StateNotifierProvider<TodoNotifier, List<Todo>>

StateNotifierProvider<TodoNotifier, List<Todo>>

  • StateNotifierProvider를 사용하면 Riverpod이 StateNotifier 기반 상태를 자동으로 관리해 줍니다.
  • <TodoNotifier, List<Todo>>
    • TodoNotifier → 할 일 목록을 관리하는 클래스 (상태 로직 포함)
    • List<Todo> → TodoNotifier가 관리하는 상태의 데이터 타입

3️⃣ (ref) { return TodoNotifier(); }

(ref) {
  return TodoNotifier();
}

  • ref: Riverpod에서 다른 Provider를 참조할 때 사용되는 객체
  • return TodoNotifier(); → TodoNotifier 인스턴스를 생성하여 todoProvider로 등록

 

📌 todoProvider가 동작하는 방식 (전체 흐름)

1️⃣ 사용자가 할 일을 추가하면

  • todoProvider.notifier.addTodo("책 읽기") 실행
  • TodoNotifier의 addTodo()가 호출됨
  • 새로운 Todo 객체가 state에 추가됨
  • Riverpod이 상태 변경을 감지하여 UI를 자동 업데이트

2️⃣ 사용자가 할 일을 삭제하면

  • todoProvider.notifier.deleteTodo(id) 실행
  • TodoNotifier의 deleteTodo()가 호출됨
  • id가 일치하는 항목이 리스트에서 제거됨
  • 상태가 변경되면서 UI가 자동으로 업데이트됨

3️⃣ 사용자가 할 일을 완료하면

  • todoProvider.notifier.toggleTodo(id) 실행
  • TodoNotifier의 toggleTodo()가 호출됨
  • 특정 id를 가진 Todo의 isDone 값이 true ↔ false로 변경됨
  • UI가 즉시 변경됨

 
 
 
 


4️⃣ UI에서 할 일 목록 관리 (CRUD 기능 구현)

할 일 목록을 UI에 표시하고, 사용자가 입력한 데이터를 반영합니다.
추가, 수정, 삭제 버튼을 눌러 데이터를 변경할 수 있습니다.
 

📌 todo_screen.dart UI 코드 작성

import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../providers/todo_provider.dart';

class TodoScreen extends ConsumerWidget {
  const TodoScreen({super.key});

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final todoList = ref.watch(todoProvider);
    final todoNotifier = ref.read(todoProvider.notifier);

    final TextEditingController controller = TextEditingController();

    return Scaffold(
      appBar: AppBar(title: const Text("Todo List (Riverpod)")),
      body: Column(
        children: [
          Padding(
            padding: EdgeInsets.all(8.0),
            child: Row(
              children: [
                // TextField
                Expanded(
                  child: TextField(
                    controller: controller,
                    decoration: const InputDecoration(
                      border: OutlineInputBorder(),
                      labelText: "할 일 입력",
                    ),
                  ),
                ),
                const SizedBox(width: 10),
                // ElevatedButton
                ElevatedButton(
                  onPressed: () {
                    if (controller.text.isNotEmpty) {
                      todoNotifier.addTodo(controller.text);
                    }
                    controller.clear();
                  },
                  child: const Text("추가"),
                ),
              ],
            ),
          ),
          const SizedBox(height: 20),
          // ListView.builder
          Expanded(
            child: ListView.builder(
              itemCount: todoList.length,
              itemBuilder: (context, index) {
                final todo = todoList[index];

                return Card(
                  child: ListTile(
                    title: Text(
                      todo.title,
                      style: TextStyle(
                        decoration:
                            todo.isDone ? TextDecoration.lineThrough : null,
                      ),
                    ),
                    leading: Checkbox(
                      value: todo.isDone,
                      onChanged: (value) {
                        todoNotifier.toggleTodo(todo.id);
                      },
                    ),
                    trailing: IconButton(
                      icon: Icon(Icons.delete, color: Colors.red),
                      onPressed: () {
                        todoNotifier.deleteTodo(todo.id);
                      },
                    ),
                  ),
                );
              },
            ),
          ),
        ],
      ),
    );
  }
}

🔔 코드 설명
✅ TextField → 사용자가 할 일 입력
✅ IconButton (추가) → todoNotifier.addTodo(controller.text) 실행하여 새 할 일 추가
✅ ListView.builder → todoList를 불러와 화면에 표시
✅ Checkbox → 체크박스를 클릭하면 toggleTodo(todo.id) 실행하여 완료 처리
✅ IconButton (삭제) → todoNotifier.deleteTodo(todo.id) 실행하여 삭제
 
 
 
 
 


📌 코드 요약

todo_provider.dart
🔸Todo 모델 만들기 (id, title, isDone)
🔸**class TodoNotifier 만들기 (상속 : StateNotifier<List<Todo>>)**
🔸**addTodo(String title) 함수 만들기**
🔸**toggleTodo(String id) 함수 만들기 (map() 사용)**
🔸**deleteTodo(String id) 함수 만들기 (where() 사용)**
🔸**StateNotifierProvider로 상태 등록하기**
 
todo_screen.dart
🔸class TodoScreen 만들기 (상속 : ConsumerWidget)
🔸provider 구독 , 참조하는 변수 선언 (ref.watch , ref.read)
🔸할 일 추가 UI 섹션 만들기 (입력 , 추가)
🔸할 일 목록 UI 섹션 만들기 (리스트 , 완료체크박스 , 삭제버튼)
 
 
 
 
 


📌 내용 요약

Riverpod의 StateNotifierProvider를 활용하여 CRUD 기능을 구현할 수 있다.
할 일 목록을 리스트 형태로 관리하며, add, update, delete 메서드를 활용할 수 있다.
ref.watch()를 사용하여 상태를 UI에 반영하고, ref.read()를 통해 상태를 변경할 수 있다.
UI에서 사용자의 입력을 받아 StateNotifier를 통해 상태를 업데이트할 수 있다.

728x90