오늘의 핵심 학습 키워드
개념설명적용 포인트예시
| Freezed | 불변(Immutable) 데이터 모델을 쉽게 정의하는 코드 생성기 | ToDoModel 클래스 생성 시 copyWith, fromJson, toJson 자동화 | ToDoModel 모델 생성 |
| Riverpod (AsyncNotifier) | 상태 비동기 관리 (Firebase 연동, CRUD 등) | HomeViewModel에서 Firebase 데이터 비동기 로드 및 업데이트 | ref.watch(homeViewProvider) |
| copyWith() | 데이터 일부만 변경할 때 사용 | 즐겨찾기/완료 토글 시 기존 상태 복제 | toDo.copyWith(isFavorite: !toDo.isFavorite) |
| HookConsumerWidget / useState / useTextEditingController | State를 따로 만들지 않고도 Hook으로 상태 제어 | AddToDoDialog의 텍스트 입력, focus 상태 등 관리 | final controller = useTextEditingController() |
| Firebase Firestore CRUD | Firestore 데이터 읽기/쓰기/삭제 | Repository에서 DB 접근 로직 분리 | ToDoRepository |
Freezed를 통한 모델 구조 개선
- 기존 수동 작성하던 모델(ToDoModel)을 @freezed로 대체.
- 자동 생성되는 copyWith, fromJson, toJson을 통해 코드량 감소.
- 명시적 타입 (Map<String, Object?>)으로 수정하여 타입 에러 해결.
📄 to_do_model.dart
@freezed class ToDoModel with _$ToDoModel { const factory ToDoModel({ required String id, required String title, String? description, @Default(false) bool isFavorite, @Default(false) bool isDone, }) = _ToDoModel; factory ToDoModel.fromJson(Map<String, Object?> json) => _$ToDoModelFromJson(json); }
Riverpod AsyncNotifier 구조 정립
- Firestore 데이터 비동기 호출을 위한 AsyncNotifier<List<ToDoModel>> 사용.
- 상태 변경 시마다 AsyncData(await getToDos())로 최신 데이터 반영.
- copyWith() 덕분에 데이터 토글 간결하게 구현 가능.
📄 home_view_model.dart
class HomeViewModel extends AsyncNotifier<List<ToDoModel>> { final ToDoRepository repo = ToDoRepository(); @override Future<List<ToDoModel>> build() async => await repo.getToDos(); Future<void> toggleFavorite({required ToDoModel toDo}) async { final updated = toDo.copyWith(isFavorite: !toDo.isFavorite); await repo.updateToDo(updated); state = AsyncData(await repo.getToDos()); } }
HookConsumerWidget으로 AddToDoDialog 리팩토링
- 기존 StatefulWidget → HookConsumerWidget으로 전환.
- useState, useTextEditingController, useFocusNode로 훨씬 간결한 상태 관리.
- setState() 제거 가능, 훅 기반 반응형 상태 전환 구현.
📄 add_to_do_dialog.dart
class AddToDoDialog extends HookConsumerWidget { const AddToDoDialog({super.key}); @override Widget build(BuildContext context, WidgetRef ref) { final titleController = useTextEditingController(); final descController = useTextEditingController(); final focusNode = useFocusNode(); final isFavorite = useState(false); final isDescription = useState(false); void save() { final title = titleController.text.trim(); if (title.isEmpty) return; final toDo = ToDoModel( id: '', title: title, description: descController.text.trim().isEmpty ? null : descController.text, isFavorite: isFavorite.value, ); Navigator.of(context).pop(toDo); } return Column( children: [ TextField(controller: titleController, focusNode: focusNode), if (isDescription.value) TextField(controller: descController, maxLines: null), Row( children: [ IconButton( icon: Icon(isFavorite.value ? Icons.star : Icons.star_border), onPressed: () => isFavorite.value = !isFavorite.value, ), IconButton( icon: Icon(Icons.check), onPressed: save, ), ], ) ], ); } }
ViewModel과 View의 연결
- HomePage에서 ref.watch(homeViewProvider)로 상태 구독.
- 각 ToDoView 위젯에 콜백을 전달해 개별 작업 수행 가능.
- 삭제 기능까지 추가 (onDelete → ref.read(homeViewProvider.notifier).deleteToDo(id: toDo.id)).
핵심 정리
항목배운 점
| 모델 관리 | Freezed로 불변 객체 생성 및 JSON 변환 자동화 |
| 상태 관리 | AsyncNotifier로 Firestore CRUD 제어 |
| UI 연결 | HookConsumerWidget으로 StatefulWidget 대체 |
| 코드 효율 | copyWith와 useState를 활용한 코드 간결화 |
| 실무 감각 | Repository 패턴 + ViewModel 조합으로 확장성 확보 |
마무리 요약
오늘은 단순한 Todo앱이 아니라,
실제 구조화된 MVVM + Hook + Freezed 조합으로
앱의 “아키텍처적 완성도”를 올린 하루였습니다.
특히:
- Riverpod 구조의 비동기 데이터 흐름 이해
- Hook을 활용한 State 간결화
- Freezed를 통한 데이터 모델 자동화
까지 완벽하게 익힌 날