📖 [Flutter 개발일지] 독서 기록 앱 만들기 - 노트 기능 구현
공통 컴포넌트와 Riverpod 상태 관리로 메모 작성/조회 기능 효율적으로 개발하기
1. 공통 컴포넌트 NoteBookInfo로 코드 재사용하기
공통 컴포넌트 목적
- 글쓰기 페이지와 글보기 페이지에서 책 정보를 표시하는 UI가 필요할 때, (리팩토링 전) 중복된 코드는 유지보수가 어려워지고 코드도 지저분해져서 간결화 필요.
- NoteBookInfo라는 공통 컴포넌트를 만들어서 한 번만 UI를 작성하고, 필요한 곳에서 재사용할 수 있도록 함. 코드가 깔끔해지고, 유지보수도 쉬워짐
NoteBookInfo
NoteBookInfo 위젯은 책 정보(제목, 저자, 이미지)를 표시하는 역할
책 추가/변경/삭제 버튼도 이 위젯에서 관리
상황 | 동작 |
Note 추가한 책이 없고 글쓰기 모드 | "책 추가" 버튼 표시 |
Note 추가한 책이 없고 글보기 모드 | 아무것도 표시하지 않음 |
Note 추가한 책이 있을 때 | 책 정보(제목, 저자, 이미지)와 아이콘 표시 |
2. 글쓰기에서 책 추가하기 (Riverpod 상태 관리)
상태 관리
상태 관리란 앱의 데이터(상태)를 어떻게 저장하고 변경할지 관리하는 방법
Flutter에서는 보통 setState()로 상태를 관리하는데, 이 방법은 작은 앱에선 괜찮지만, 페이지가 많아질수록 복잡해지고 문제가 생김
Riverpod 사용 이유
- setState() 문제점:
- 페이지가 다시 빌드되면 상태가 초기화돼서 데이터가 사라짐.
- 여러 위젯이 같은 상태를 공유하기 어려움.
- Riverpod의 장점:
- 앱 전역에서 상태를 관리할 수 있음 (한 번 저장하면 앱 전체에서 사용 가능)
- 필요한 부분만 다시 빌드해서 성능 좋음
- 테스트 쉬움
📌 글쓰기 흐름
- 책 추가 버튼 클릭 → /noteAddBook 페이지로 이동
- 책 선택 후 Navigator.pop()으로 선택한 책 정보를 반환
- 반환된 책 정보를 bookWriteProvider에 저장
- NoteBookInfo 위젯이 책 정보를 구독(watch)하여 자동으로 UI 업데이트
책 추가 코드 (글쓰기 페이지)
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:shelfy_team_project/providers/book_provider.dart';
import 'package:shelfy_team_project/ui/pages/note/note_page/widget/note_book_Info.dart';
import 'package:shelfy_team_project/ui/widgets/custom_appbar.dart';
// 글쓰기 페이지에서 상태 구독을 위해 ConsumerWidget 사용
class NoteWritePage extends ConsumerWidget {
const NoteWritePage({super.key});
// 📚 책을 선택하는 함수 (책 추가/변경 시 호출됨)
Future<void> selectBook(BuildContext context, WidgetRef ref) async {
// '/noteAddBook' 페이지로 이동 후, 선택된 책 정보를 받아옴
final selectedBook = await Navigator.pushNamed(context, '/noteAddBook');
// 선택된 책 데이터가 있으면 상태 저장
if (selectedBook is Map<String, String>) {
ref.read(bookWriteProvider.notifier).state = selectedBook;
}
}
// 📚 선택된 책 삭제 함수
void deleteBook(WidgetRef ref) {
ref.read(bookWriteProvider.notifier).state = null; // 상태 초기화
}
@override
Widget build(BuildContext context, WidgetRef ref) {
final book = ref.watch(bookWriteProvider); // 상태 구독
return Scaffold(
appBar: NoteCustomAppBar(context: context, title: '글쓰기', actionText: '완료'),
body: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
children: [
// 📚 NoteBookInfo 위젯을 통해 책 정보 표시
NoteBookInfo(
bookImage: book?['book_image'],
bookTitle: book?['book_title'],
bookAuthor: book?['book_author'],
isEditMode: true, // 글쓰기 모드에서는 변경/삭제 가능
onAddPressed: () => selectBook(context, ref),
onChangePressed: () => selectBook(context, ref),
onDeletePressed: () => deleteBook(ref),
),
],
),
),
);
}
}
3. 글보기에서 Riverpod으로 데이터 구독하기
글보기 페이지에서 상태 관리
글보기 페이지에서는 등록된 책 데이터를 불러와서 표시해야 함
여기서도 Riverpod을 사용해서 상태를 구독하고, 자동으로 UI를 업데이트
📌 글보기 흐름
- 글보기 페이지에서 bookViewProvider 구독
- 저장된 책 정보를 NoteBookInfo에 전달
- 책 정보가 없으면 아무것도 표시하지 않고, 있으면 책 정보 표시
글보기 코드
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:shelfy_team_project/providers/book_provider.dart';
import 'package:shelfy_team_project/ui/pages/note/note_page/widget/note_book_Info.dart';
import 'package:shelfy_team_project/ui/widgets/custom_appbar.dart';
class NoteViewPage extends ConsumerWidget {
const NoteViewPage({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final book = ref.watch(bookViewProvider); // 글보기 상태 구독
return Scaffold(
appBar: NoteCustomAppBar(context: context, title: '글보기', actionText: '수정'),
body: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
children: [
if (book != null)
// 📚 등록된 책 정보 표시
NoteBookInfo(
bookImage: book['book_image'],
bookTitle: book['book_title'],
bookAuthor: book['book_author'],
isEditMode: false, // 글보기 모드에서는 상세보기만 가능
onDetailPressed: () => print('책 상세 정보 보기'),
),
],
),
),
);
}
}
4. 공통 다이얼로그 만들기 (Dialog 재사용)
글쓰기 완료, 수정, 삭제 등에서 같은 형태의 다이얼로그가 필요한 상황,
공통 다이얼로그 컴포넌트를 만들어서 다양한 상황에서 재사용할 수 있게
공통 다이얼로그 코드
import 'package:flutter/material.dart';
// 📚 공통 다이얼로그 함수 (간단한 확인/취소 용)
void showConfirmationDialog({
required BuildContext context,
required String title,
String? subtitle,
required String confirmText,
required VoidCallback onConfirm,
IconData? snackBarIcon, // 스낵바 아이콘 (옵션)
Color confirmTextColor = Colors.blue,
String snackBarMessage = '작업 완료!', // 기본 메시지
Color snackBarColor = Colors.green,
}) {
showDialog(
context: context,
barrierDismissible: false, // 배경 클릭으로 닫기 방지
builder: (context) {
return AlertDialog(
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
title: Center(child: Text(title, style: Theme.of(context).textTheme.bodyMedium)),
content: subtitle != null
? Center(child: Text(subtitle, style: Theme.of(context).textTheme.bodySmall?.copyWith(color: Colors.grey)))
: null,
actionsAlignment: MainAxisAlignment.center, // 버튼 중앙 정렬
actions: [
TextButton(onPressed: () => Navigator.of(context).pop(), child: const Text('취소')),
ElevatedButton(
onPressed: () {
Navigator.of(context).pop(); // 다이얼로그 닫기
onConfirm(); // 확인 시 동작 실행
// 📚 스낵바로 결과 메시지 표시
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Row(
children: [
if (snackBarIcon != null) ...[
Icon(snackBarIcon, color: Colors.white),
const SizedBox(width: 8),
],
Text(snackBarMessage),
],
),
backgroundColor: snackBarColor,
duration: const Duration(seconds: 2),
),
);
},
style: ElevatedButton.styleFrom(
backgroundColor: Colors.transparent,
shadowColor: Colors.transparent,
),
child: Text(confirmText, style: TextStyle(color: confirmTextColor)),
),
],
);
},
);
}
정리
- 공통 컴포넌트(NoteBookInfo)를 활용해서 UI 중복을 제거하고 코드 재사용성을 높였습니다.
- Riverpod으로 상태를 전역에서 관리하면서 필요한 부분만 빌드해 성능을 최적화했습니다.
- 공통 다이얼로그 컴포넌트로 수정/삭제/글쓰기 완료 등의 다양한 상황에서 재사용성을 높였습니다.
- 전역 상태 관리 덕분에 페이지가 전환되더라도 상태를 유지할 수 있습니다.