Flutter

Flutter Day 13: Riverpod 2.xx로 배우는 MVVM 패턴과 상태관리

@leem 2025. 1. 23. 11:24

1. MVVM 패턴

  • Model: 데이터를 정의하는 곳이에요. (TodoItem)
  • ViewModel: 비즈니스 로직과 데이터를 관리하는 중간 관리자에요. (TodoListViewModel)
  • View: 사용자와 상호작용하는 화면(UI)이에요. (TodoListView)

MVVM의 핵심은 View와 Model이 직접 연결되지 않고 ViewModel을 통해서만 통신한다는 점

  1. 코드가 깔끔하고 유지보수하기 쉬움
  2. 데이터와 UI가 분리되니까 테스트하기 좋음

2. Provider

Provider는 앱에서 데이터를 공유하거나 관리하는 역할을 해요. Flutter 앱에서는 데이터(상태)를 여러 위젯에서 공유해야 할 때가 많은데, Provider는 이런 데이터 공유와 관리를 편리하게 만들어주는 도구입니다.

Provider의 장점

  1. 데이터를 안전하게 공유할 수 있어요.
  2. 상태가 바뀌면 자동으로 UI를 업데이트해줘요.
  3. 복잡한 상태 관리를 간단하게 만들어줘요.

3. 예제 코드 이해

(1) Model: 데이터를 정의

class TodoItem {
  String title;
  bool isDone;

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

 

  • TodoItem은 우리가 관리하려는 데이터, 즉 "할 일"을 나타냅니다.
  • 각 할 일은 title(할 일 제목)과 isDone(완료 여부)을 가지고 있어요.

예시

  • TodoItem(title: "청소하기", isDone: false) → 제목은 "청소하기", 아직 완료 안 됨.

(2) ViewModel: 비즈니스 로직과 상태 관리

class TodoListViewModel extends Notifier<List<TodoItem>> {
  @override
  List<TodoItem> build() {
    return []; // 초기 상태는 빈 리스트
  }

  void addItem(String title) {
    state = [...state, TodoItem(title: title)]; // 새로운 할 일을 추가
  }

  void toggleItem(TodoItem todo) {
    state = state
        .map((item) => item == todo ? item.copyWith(isDone: !item.isDone) : item)
        .toList(); // 완료 상태를 반전
  }
}

 

  1. Notifier
    • Riverpod에서 상태를 관리하는 클래스에요.
    • Notifier<List<TodoItem>>는 "할 일 목록"이라는 상태를 관리합니다.
  2. build 메서드
    • 초기 상태를 정의해요. 여기선 빈 리스트([])로 시작합니다.
  3. addItem 메서드
    • 새 할 일을 추가하는 함수입니다.
    • state = [...state, TodoItem(title: title)] → 기존 상태(state)에 새 할 일을 추가한 새 리스트를 만들어서 업데이트해요.
  4. toggleItem 메서드
    • 특정 할 일의 완료 상태를 반전시키는 함수입니다.
    • 불변성을 유지하기 위해 기존 리스트를 복사해서 새로 만듭니다.

가변 객체(Mutable Object)와 불변 객체(Immutable Object)

특징 가변 객체 (Mutable) 불변 객체 (Immutable)
값 수정 여부 생성된 후 값을 직접 수정할 수 있음. 생성된 후 값을 수정할 수 없고, 새 객체를 만들어야 함.
안정성 여러 곳에서 공유되면, 의도치 않게 값이 바뀔 위험이 있음. 값이 변경되지 않으므로 더 안정적.
Flutter에서의 사용 상태 관리에서 가변 객체를 쓰면 예기치 못한 버그가 생길 가능성 높음. 불변 객체는 상태 변경 시 새 객체를 생성하여 상태를 관리.

Flutter와 Riverpod에서 불변 객체를 사용하는 이유

  1. 안정성: 기존 상태가 보존되기 때문에, 의도치 않은 상태 변경을 방지.
  2. 변화 감지: 새 객체를 생성하면 Flutter가 상태 변화를 확실히 감지해 UI를 업데이트.
  3. 버그 방지: 여러 곳에서 동일한 상태를 공유하더라도, 불변 객체는 수정할 수 없기 때문에 안전.

이해

 

  • 가변 객체: "수정 가능한 노트" → 한 노트를 여러 사람이 수정하면 충돌이 생길 수 있음.
  • 불변 객체: "복사본 노트" → 수정할 때마다 새 노트를 복사해 사용하니 충돌이 없음.

 


(3) NotifierProvider: ViewModel과 상태를 연결

final todoListViewModelProvider =
    NotifierProvider<TodoListViewModel, List<TodoItem>>(() {
  return TodoListViewModel();
});

 

  • NotifierProvider는 Notifier를 사용할 수 있게 연결해주는 도구
  • todoListViewModelProvider는 TodoListViewModel(뷰모델)을 만들어서 앱 어디서나 접근할 수 있게 해줘요.
  • ViewModel을 Provider로 만들어서 View에서 접근 가능하도록 설정.

(4) ProviderScope: 상태 관리 환경

void main() {
  runApp(ProviderScope(child: TodoApp()));
}
  • ProviderScope는 Riverpod 상태 관리의 시작점이에요.
  • 앱 전체에서 Provider를 사용할 수 있게 환경을 만들어줍니다.
  • ProviderScope는 상태(데이터)를 저장하는 "큰 창고" 역할을 해요.

(5) View: 화면(UI) 구성

class TodoListView extends ConsumerWidget {
  TextEditingController _controller = TextEditingController();

  TodoListView({super.key});

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final todos = ref.watch(todoListViewModelProvider); // 상태 가져오기
    final todoNotifier = ref.read(todoListViewModelProvider.notifier); // 로직 가져오기

    return Flexible(
      child: Column(
        children: [
          Padding(
            padding: const EdgeInsets.all(16.0),
            child: TextField(
              controller: _controller,
              decoration: InputDecoration(
                hintText: 'Enter todo item...',
                suffixIcon: IconButton(
                  onPressed: () {
                    todoNotifier.addItem(_controller.text); // 할 일 추가
                    _controller.clear();
                  },
                  icon: Icon(Icons.add),
                ),
              ),
            ),
          ),
          Expanded(
            child: ListView.builder(
              itemCount: todos.length,
              itemBuilder: (context, index) {
                final item = todos[index];
                return ListTile(
                  title: Text(item.title),
                  trailing: Checkbox(
                    value: item.isDone,
                    onChanged: (value) {
                      todoNotifier.toggleItem(item); // 완료 상태 변경
                    },
                  ),
                );
              },
            ),
          ),
        ],
      ),
    );
  }
}
  1. ConsumerWidget
    • Provider에서 데이터를 가져와서 UI에 사용할 수 있도록 도와주는 위젯이에요.
  2. ref.watch와 ref.read
    • ref.watch: 상태를 구독합니다. 상태가 바뀌면 UI가 자동으로 업데이트돼요.
    • ref.read: 상태를 직접 읽고, 업데이트하는 로직에 접근할 수 있어요.
  3. todos
    • ref.watch(todoListViewModelProvider) → ViewModel에서 관리하는 할 일 목록 상태를 가져옵니다.
  4. todoNotifier
    • ref.read(todoListViewModelProvider.notifier) → ViewModel의 메서드(addItem, toggleItem)를 호출할 수 있게 합니다.

4. 앱 실행 흐름

  1. 앱이 실행되면 ProviderScope가 생성되고, todoListViewModelProvider를 통해 상태를 관리할 준비를 합니다.
  2. 사용자가 할 일을 입력하고 추가 버튼을 누르면:
    • todoNotifier.addItem이 호출되어 상태가 업데이트됩니다.
    • Riverpod은 상태가 업데이트된 것을 감지하고 UI를 자동으로 다시 그립니다.
  3. 사용자가 체크박스를 누르면:
    • todoNotifier.toggleItem이 호출되어 상태가 업데이트됩니다.
    • 마찬가지로 UI가 자동으로 업데이트됩니다.
1. 앱 실행 앱 시작. 빈 상태([])를 초기화. 빈 리스트 화면 표시.
2. 할 일 추가 "청소하기" 입력 후 추가 버튼 클릭. addItem 호출 → 새로운 할 일 상태로 업데이트. ref.watch가 상태 변경 감지 → "청소하기"가 화면에 추가됨.
3. 완료 상태 변경 "청소하기" 체크박스 클릭. toggleItem 호출 → 완료 상태 반전. ref.watch가 상태 변경 감지 → 체크박스 상태가 화면에 반영됨.

 


개념 설명 비유 이해
Model 데이터를 정의하는 역할. 앱에서 관리하려는 실제 데이터. 창고의 상품: 예를 들어, "청소하기", "공부하기" 같은 할 일 목록.
ViewModel 비즈니스 로직과 상태를 관리. 데이터(Model)를 조작하고 UI(View)에 필요한 데이터를 제공. 창고 관리자: "할 일 목록"이라는 상품을 추가하거나 수정하는 사람.
View 사용자가 볼 수 있는 UI. 사용자와 상호작용하고 ViewModel에서 데이터를 가져와 화면에 표시. 창고 쇼룸: 사용자가 "청소하기"를 보고, 상태를 수정하거나 새로 추가할 수 있는 공간.
ProviderScope Riverpod 상태 관리의 시작점. 앱 전체에서 Provider를 사용할 수 있도록 환경을 제공. 도시의 경계선: 도시 안에 창고, 관리자, 상품 등이 모두 들어가게 하는 큰 틀.
Notifier 상태를 관리하고 비즈니스 로직을 처리하는 Riverpod의 클래스. 창고 규칙서: 상품(상태)을 어떻게 관리할지 정리한 규칙서. 예를 들어 "상품 추가"와 "상태 수정".
NotifierProvider Notifier를 View와 연결해주는 역할. Notifier를 사용할 수 있는 Provider를 생성. 관리자를 배정하는 창구: View에서 "창고 관리자(ViewModel)"를 호출하거나 데이터를 읽어올 수 있도록 연결.
ref.watch Provider의 상태를 구독. 상태가 변경되면 UI(View)가 자동으로 업데이트됨. 창고에서 상품을 지켜보는 CCTV: 상품이 추가되거나 수정되면 바로 쇼룸(View)에 반영.
ref.read Provider의 상태를 읽거나, Notifier(ViewModel)에 있는 비즈니스 로직(메서드)을 호출할 때 사용. 창고 관리자에게 직접 명령: "청소하기를 추가해주세요"라고 말하는 것과 같음.
ConsumerWidget View에서 Provider 데이터를 쉽게 가져와 사용할 수 있도록 도와주는 위젯. 쇼룸 운영자: 창고에서 가져온 상품(데이터)을 화면에 배치하고 사용자와의 상호작용을 처리.