Flutter/개발 일지
[Flutter 개발일지] Riverpod 리팩토링: StateNotifier 통합 적용
@leem
2025. 3. 5. 15:21
Flutter에서 Riverpod를 활용해 노트 작성, 수정, 삭제 등의 기능을 구현할 때,
여러 가지 방식이 혼용되면 유지보수가 어려워질 수 있다.
이번에 노트 관련 ViewModel을 통일하고, StateNotifier 기반으로 관리하는 방식으로 리팩토링했다.
이 글에서는 기존 코드의 문제점, 변경 사항, 흐름을 정리해본다.
기존 코드의 문제점
1. 비일관적인 상태 관리
- 노트 목록 → StateNotifierProvider
- 노트 상세 → FutureProvider.autoDispose.family
- 노트 작성 → StateNotifierProvider 없이 직접 호출
🔥 문제점:
- 같은 기능을 다르게 관리하니 유지보수가 어려움
- FutureProvider는 한 번만 호출되고 상태 변경을 감지하지 않음 → UI 업데이트 누락
2. 상태 변경 후 UI 갱신이 불안정
- fetchNotes()를 호출해도 invalidate()만 수행하고, 다시 데이터를 가져오지 않는 경우 발생
- Future.microtask() 같은 비동기 패턴이 섞여 있어 흐름이 복잡
3. 노트 작성 시 상태 관리 부재
- 작성 도중 로딩, 성공, 실패 상태를 명확하게 구분할 필요가 있음
- AsyncValue<void>를 사용하면 로딩 중 버튼 비활성화, 에러 발생 시 스낵바 표시 가능
리팩토링 방향
- 모든 노트 관련 기능을 StateNotifierProvider 기반으로 통일
- 노트 작성도 상태(로딩, 성공, 실패)를 관리하도록 AsyncValue<void> 적용
- 노트 수정/삭제 후 noteListViewModelProvider를 즉시 갱신하도록 처리
- 불필요한 Future.microtask 패턴 제거
변경된 코드 흐름
1. 노트 목록 ViewModel (NoteListViewModel)
- StateNotifierProvider를 사용하여 노트 리스트를 관리
- fetchNotes()를 통해 유저 ID 기준으로 노트 데이터를 불러옴
- 최신순 정렬 기능 추가
final noteListViewModelProvider = StateNotifierProvider<NoteListViewModel, List<Note>>(
(ref) => NoteListViewModel(ref.read(noteRepositoryProvider)),
);
class NoteListViewModel extends StateNotifier<List<Note>> {
final NoteRepository _repository;
NoteListViewModel(this._repository) : super([]);
Future<void> fetchNotes(int userId) async {
if (userId == 0) {
state = [];
return;
}
try {
final response = await _repository.findAllByUser(userId: userId);
final notes = response['response']
?.map<Note>((json) => Note.fromJson(json))
.toList() ?? [];
state = notes;
} catch (e) {
state = [];
}
}
}
2. 노트 상세 ViewModel (NoteDetailViewModel)
- 기존의 FutureProvider.autoDispose.family를 StateNotifier 기반으로 변경
- 북마크 변경 시 UI가 즉시 반영되도록 state = state.copyWith(...) 패턴 사용
final noteDetailViewModelProvider = StateNotifierProvider.family<NoteDetailViewModel, Note?, int>(
(ref, noteId) => NoteDetailViewModel(ref.read(noteRepositoryProvider), noteId),
);
class NoteDetailViewModel extends StateNotifier<Note?> {
final NoteRepository _repository;
final int noteId;
NoteDetailViewModel(this._repository, this.noteId) : super(null) {
fetchNote();
}
Future<void> fetchNote() async {
try {
final response = await _repository.findById(id: noteId);
if (response.isNotEmpty) {
state = Note.fromJson(response);
}
} catch (e) {
state = null;
}
}
Future<void> toggleBookmark() async {
if (state == null || state!.noteId == null) return;
final updatedPinStatus = !state!.notePin;
try {
await _repository.updateNotePin(state!.noteId!, updatedPinStatus);
state = state!.copyWith(notePin: updatedPinStatus);
} catch (e) {}
}
}
3. 노트 작성 ViewModel (NoteViewModel)
- 작성 요청을 StateNotifier 기반으로 변경
- 상태를 AsyncValue<void>로 관리하여 로딩 중 상태, 성공 여부, 에러 처리 가능
- submitNote() 이후 UI에서 상태를 반영 가능
final noteViewModelProvider = StateNotifierProvider<NoteViewModel, AsyncValue<void>>(
(ref) => NoteViewModel(ref.read(noteRepositoryProvider)),
);
class NoteViewModel extends StateNotifier<AsyncValue<void>> {
final NoteRepository _repository;
NoteViewModel(this._repository) : super(const AsyncData(null));
Future<void> submitNote(Note note) async {
state = const AsyncLoading();
try {
final result = await _repository.save(note.toJson());
state = const AsyncData(null);
} catch (e, stack) {
state = AsyncError(e, stack);
}
}
}
4. 노트 작성 화면 (NoteWritePage)
- 작성 후 noteListViewModelProvider를 invalidate하여 리스트 갱신
- 상태를 ref.watch(noteViewModelProvider)로 감지하여 버튼 상태 변경
class NoteWritePage extends ConsumerStatefulWidget {
const NoteWritePage({super.key});
@override
_NoteWritePageState createState() => _NoteWritePageState();
}
class _NoteWritePageState extends ConsumerState<NoteWritePage> {
final TextEditingController _titleController = TextEditingController();
final TextEditingController _contentController = TextEditingController();
Future<void> _handleNoteCompletion(BuildContext context) async {
final title = _titleController.text.trim();
final content = _contentController.text.trim();
if (title.isEmpty || content.isEmpty) {
CommonSnackbar.warning(context, '제목과 내용을 입력해주세요.');
return;
}
final note = Note(title: title, content: content, userId: ref.read(sessionProvider).id ?? 0);
try {
await ref.read(noteViewModelProvider.notifier).submitNote(note);
ref.invalidate(noteListViewModelProvider); // 노트 리스트 갱신
Navigator.pushAndRemoveUntil(
context,
MaterialPageRoute(builder: (context) => MainScreen(initialIndex: 3)),
(route) => false,
);
} catch (e) {
CommonSnackbar.error(context, '노트 저장 실패: $e');
}
}
@override
Widget build(BuildContext context) {
final isLoading = ref.watch(noteViewModelProvider).isLoading;
return Scaffold(
appBar: AppBar(
title: Text('노트 작성'),
actions: [
IconButton(
icon: isLoading ? CircularProgressIndicator() : Icon(Icons.check),
onPressed: isLoading ? null : () => _handleNoteCompletion(context),
)
],
),
body: NoteWriteBody(titleController: _titleController, contentController: _contentController),
);
}
}
항목 | 리팩토링 전 | 리팩토링 후 |
상태 관리 방식 | StateNotifier, FutureProvider 혼용 | StateNotifierProvider로 통일 |
노트 목록 관리 | StateNotifierProvider | 동일 (fetchNotes() 최적화) |
노트 상세 조회 | FutureProvider.autoDispose.family | StateNotifierProvider.family로 변경 |
노트 작성 | 상태 관리 없이 Repository 직접 호출 | StateNotifierProvider 사용 (AsyncValue<void>) |
UI 갱신 방식 | invalidate()만 수행 → 업데이트 누락 가능 | fetchNotes()를 즉시 호출하여 갱신 |
불필요한 패턴 | Future.microtask() 같은 복잡한 흐름 존재 | 제거 후 깔끔한 로직으로 개선 |
에러 핸들링 | 개별적으로 try-catch 처리 | AsyncValue<void>로 통합 |