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을 통해서만 통신한다는 점
- 코드가 깔끔하고 유지보수하기 쉬움
- 데이터와 UI가 분리되니까 테스트하기 좋음
2. Provider
Provider는 앱에서 데이터를 공유하거나 관리하는 역할을 해요. Flutter 앱에서는 데이터(상태)를 여러 위젯에서 공유해야 할 때가 많은데, Provider는 이런 데이터 공유와 관리를 편리하게 만들어주는 도구입니다.
Provider의 장점
- 데이터를 안전하게 공유할 수 있어요.
- 상태가 바뀌면 자동으로 UI를 업데이트해줘요.
- 복잡한 상태 관리를 간단하게 만들어줘요.
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(); // 완료 상태를 반전
}
}
- Notifier
- Riverpod에서 상태를 관리하는 클래스에요.
- Notifier<List<TodoItem>>는 "할 일 목록"이라는 상태를 관리합니다.
- build 메서드
- 초기 상태를 정의해요. 여기선 빈 리스트([])로 시작합니다.
- addItem 메서드
- 새 할 일을 추가하는 함수입니다.
- state = [...state, TodoItem(title: title)] → 기존 상태(state)에 새 할 일을 추가한 새 리스트를 만들어서 업데이트해요.
- toggleItem 메서드
- 특정 할 일의 완료 상태를 반전시키는 함수입니다.
- 불변성을 유지하기 위해 기존 리스트를 복사해서 새로 만듭니다.
가변 객체(Mutable Object)와 불변 객체(Immutable Object)
특징 | 가변 객체 (Mutable) | 불변 객체 (Immutable) |
값 수정 여부 | 생성된 후 값을 직접 수정할 수 있음. | 생성된 후 값을 수정할 수 없고, 새 객체를 만들어야 함. |
안정성 | 여러 곳에서 공유되면, 의도치 않게 값이 바뀔 위험이 있음. | 값이 변경되지 않으므로 더 안정적. |
Flutter에서의 사용 | 상태 관리에서 가변 객체를 쓰면 예기치 못한 버그가 생길 가능성 높음. | 불변 객체는 상태 변경 시 새 객체를 생성하여 상태를 관리. |
Flutter와 Riverpod에서 불변 객체를 사용하는 이유
- 안정성: 기존 상태가 보존되기 때문에, 의도치 않은 상태 변경을 방지.
- 변화 감지: 새 객체를 생성하면 Flutter가 상태 변화를 확실히 감지해 UI를 업데이트.
- 버그 방지: 여러 곳에서 동일한 상태를 공유하더라도, 불변 객체는 수정할 수 없기 때문에 안전.
이해
- 가변 객체: "수정 가능한 노트" → 한 노트를 여러 사람이 수정하면 충돌이 생길 수 있음.
- 불변 객체: "복사본 노트" → 수정할 때마다 새 노트를 복사해 사용하니 충돌이 없음.
(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); // 완료 상태 변경
},
),
);
},
),
),
],
),
);
}
}
- ConsumerWidget
- Provider에서 데이터를 가져와서 UI에 사용할 수 있도록 도와주는 위젯이에요.
- ref.watch와 ref.read
- ref.watch: 상태를 구독합니다. 상태가 바뀌면 UI가 자동으로 업데이트돼요.
- ref.read: 상태를 직접 읽고, 업데이트하는 로직에 접근할 수 있어요.
- todos
- ref.watch(todoListViewModelProvider) → ViewModel에서 관리하는 할 일 목록 상태를 가져옵니다.
- todoNotifier
- ref.read(todoListViewModelProvider.notifier) → ViewModel의 메서드(addItem, toggleItem)를 호출할 수 있게 합니다.
4. 앱 실행 흐름
- 앱이 실행되면 ProviderScope가 생성되고, todoListViewModelProvider를 통해 상태를 관리할 준비를 합니다.
- 사용자가 할 일을 입력하고 추가 버튼을 누르면:
- todoNotifier.addItem이 호출되어 상태가 업데이트됩니다.
- Riverpod은 상태가 업데이트된 것을 감지하고 UI를 자동으로 다시 그립니다.
- 사용자가 체크박스를 누르면:
- 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 데이터를 쉽게 가져와 사용할 수 있도록 도와주는 위젯. | 쇼룸 운영자: 창고에서 가져온 상품(데이터)을 화면에 배치하고 사용자와의 상호작용을 처리. |