2025. 3. 5. 10:46ㆍ같이 공부합시다 - Flutter/Dart & Flutter 기초부터 실전까지
📃 개요
⚠️ Flutter에서 setState()만으로 상태를 관리하면 코드가 복잡해지고, 여러 위젯에서 상태를 공유하기 어렵습니다.
📌 Flutter에서 상태(State)를 관리하는 것은 앱 개발의 핵심 요소입니다.
⚠️ 상태가 여러 화면에서 공유될 경우, 적절한 상태 관리 패턴이 필요합니다.
📌 Provider 패턴은 Flutter에서 공식적으로 권장하는 상태 관리 방법 중 하나로, 보다 효율적인 방식으로 전역 상태를 공유할 수 있습니다.
✅ Provider 패턴이란? → Flutter에서 상태를 효율적으로 관리하는 방법
✅ ChangeNotifier와 Consumer의 역할과 사용법
✅ 전역 상태 관리와 UI 업데이트 최적화 방법
✅ ChangeNotifier를 활용하면 setState() 없이 UI를 업데이트할 수 있습니다.
✅ Consumer를 이용하여 특정 위젯만 상태가 변경될 때 리빌드할 수 있습니다.
🔔 주제
🔸 Provider 패턴 개념 및 기본 구조 이해
🔸 ChangeNotifier를 사용한 상태 관리
🔸 Consumer를 활용하여 UI 최적화
🔸 MultiProvider를 활용한 여러 상태 관리
1️⃣ Provider 패턴 개념 및 기본 구조 이해
🔸 Provider는 Flutter에서 공식적으로 추천하는 상태 관리 라이브러리
🔸 ChangeNotifier를 활용하여 상태를 관리하고, UI에 반영할 수 있음
🔸 상태가 변경될 때 필요한 위젯만 업데이트할 수 있어 성능 최적화 가능
📌 Provider 기본 흐름
1. ChangeNotifier를 사용하여 상태를 정의
2. ChangeNotifierProvider로 상태를 앱에 등록
3. Consumer 또는 Provider.of<T>()를 사용하여 UI에서 상태를 가져옴
2️⃣ ChangeNotifier를 사용한 상태 관리
🔸 ChangeNotifier는 Flutter에서 상태를 관리할 수 있도록 도와주는 클래스
🔸 notifyListeners()를 호출하면 연결된 모든 위젯이 업데이트됨
📌 Provider 패키지
🔸 VSCode 터미널에서 등록
flutter pub add provider
📌 ChangeNotifier 구현 (Counter 예제)
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
void main() {
// runApp(const MyApp());
// Provider 등록
runApp(
ChangeNotifierProvider(
create: (context) => CounterProvider(),
child: const MyApp(),
),
);
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Provider 패턴',
theme: ThemeData(
useMaterial3: true,
colorScheme: ColorScheme.fromSeed(seedColor: Colors.blue)),
home: const CounterScreen(),
);
}
}
// 상태 클래스
class CounterProvider extends ChangeNotifier {
int _counter = 0;
int get counter => _counter;
void increment() {
_counter++;
notifyListeners(); // UI 업데이트
}
}
// UI 클래스
class CounterScreen extends StatelessWidget {
const CounterScreen({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text("Provider 예제")),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
"카운트: ${context.watch<CounterProvider>().counter}",
style: const TextStyle(fontSize: 30),
),
const SizedBox(height: 20),
ElevatedButton(
onPressed: () => context.read<CounterProvider>().increment(),
child: const Text("증가"),
),
],
),
),
);
}
}
🔔 코드 설명
✅ ChangeNotifierProvider를 사용하여 CounterProvider를 앱에 등록
✅ context.watch<CounterProvider>()를 사용하여 상태를 가져와 UI 업데이트
✅ notifyListeners()를 호출하면 상태가 변경되며 화면이 업데이트됨
3️⃣ Consumer를 활용하여 UI 최적화
🔸 Consumer를 사용하면 특정 위젯만 상태 변경 시 리빌드할 수 있음
🔸 context.watch<T>()는 전체 위젯을 리빌드하지만, Consumer는 해당 위젯만 리빌드
📌 Consumer를 사용한 상태 관리 최적화
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
void main() {
// runApp(const MyApp());
// Provider 등록
runApp(
ChangeNotifierProvider(
create: (context) => CounterProvider(),
child: const MyApp(),
),
);
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Provider 패턴',
theme: ThemeData(
useMaterial3: true,
colorScheme: ColorScheme.fromSeed(seedColor: Colors.blue)),
home: const CounterScreen(),
);
}
}
// 상태 클래스
class CounterProvider extends ChangeNotifier {
int _counter = 0;
int get counter => _counter;
void increment() {
_counter++;
notifyListeners(); // UI 업데이트
}
}
// UI 클래스
class CounterScreen extends StatelessWidget {
const CounterScreen({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text("Provider 예제")),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
// Text(
// "카운트: ${context.watch<CounterProvider>().counter}",
// style: const TextStyle(fontSize: 30),
// ),
Consumer<CounterProvider>(
builder: (context, provider, child) {
return Text(
"카운트: ${provider.counter}",
style: const TextStyle(fontSize: 30),
);
},
),
const SizedBox(height: 20),
ElevatedButton(
onPressed: () => context.read<CounterProvider>().increment(),
child: const Text("증가"),
),
],
),
),
);
}
}
🔔 코드 설명
✅ Consumer를 사용하면 필요한 위젯만 리빌드됨
✅ 성능 최적화를 위해 사용 가능
4️⃣ MultiProvider 를 활용한 여러 상태 관리
🔸 여러 개의 상태 클래스를 한 번에 등록할 수 있음
🔸 MultiProvider를 사용하여 다양한 상태를 동시에 관리 가능
📌 MultiProvider 적용 예제
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
void main() {
// Provider 등록
// runApp(
// ChangeNotifierProvider(
// create: (context) => CounterProvider(),
// child: const MyApp(),
// ),
// );
runApp(
MultiProvider(
providers: [
ChangeNotifierProvider(create: (context) => CounterProvider()),
ChangeNotifierProvider(create: (context) => AnotherProvider()),
],
child: const MyApp(),
),
);
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Provider 패턴',
theme: ThemeData(
useMaterial3: true,
colorScheme: ColorScheme.fromSeed(seedColor: Colors.blue)),
home: const CounterScreen(),
);
}
}
// 상태 클래스
class CounterProvider extends ChangeNotifier {
int _counter = 0;
int get counter => _counter;
void increment() {
_counter++;
notifyListeners(); // UI 업데이트
}
}
// 또다른 상태 클래스
class AnotherProvider extends ChangeNotifier {
int _counter = 0;
int get counter => _counter;
void decrement() {
_counter--;
notifyListeners();
}
}
// UI 클래스
class CounterScreen extends StatelessWidget {
const CounterScreen({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text("Provider 예제")),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Consumer<CounterProvider>(
builder: (context, provider, child) {
return Text(
"카운트: ${provider.counter}",
style: const TextStyle(fontSize: 30),
);
},
),
const SizedBox(height: 20),
ElevatedButton(
onPressed: () => context.read<CounterProvider>().increment(),
child: const Text("증가"),
),
const SizedBox(height: 20),
Consumer<AnotherProvider>(
builder: (context, provider, child) {
return Text(
"카운트 감소: ${provider.counter}",
style: const TextStyle(fontSize: 30),
);
},
),
const SizedBox(height: 20),
ElevatedButton(
onPressed: () => context.read<AnotherProvider>().decrement(),
child: const Text("감소"),
),
],
),
),
);
}
}
🔔 코드 설명
🔸 여러 개의 ChangeNotifierProvider를 MultiProvider에 등록
🔸 앱에서 여러 상태를 동시에 관리 가능
🔻 MultiProvider 테스트 결과
5️⃣ Provider 사용 시 주의해야 할 점
🔸 Provider를 너무 깊이 중첩하면 성능 저하 발생
🔸 notifyListeners()를 남발하면 불필요한 UI 리렌더링 발생
🔸 context.watch<T>()를 사용할 때, build() 메서드 안에서만 호출해야 함
📌 잘못된 예제 (불필요한 notifyListeners() 호출)
void increment() {
_count++;
notifyListeners(); // ❌ 너무 자주 호출하면 성능 저하
}
📌 올바른 예제 (최적화된 notifyListeners() 사용)
void increment() {
if (_count < 100) { // 특정 조건에서만 호출
_count++;
notifyListeners();
}
}
💡 Provider를 사용할 때는 성능을 고려하여 필요한 부분만 notifyListeners()를 호출해야 합니다.
📌 내용 요약
✅ Provider는 Flutter에서 공식적으로 추천하는 상태 관리 패턴이다.
✅ ChangeNotifier를 사용하면 상태 변경을 감지하고 UI를 업데이트할 수 있다.
✅ Consumer를 활용하면 특정 위젯만 리빌드할 수 있어 성능 최적화가 가능하다.
✅ MultiProvider를 사용하면 여러 개의 상태를 동시에 관리할 수 있다.
📌 보너스
✅ Provider를 활용한 Todo List 앱
📌 주요 기능
🔸 Provider를 사용하여 전역 상태 관리
🔸 할 일 목록을 추가/삭제 가능
🔸 setState() 없이 UI 자동 업데이트
✅ 1. 프로젝트 구조
📂 todo_app_provider
├── main.dart (앱의 진입점)
├── providers/todo_provider.dart (Provider 상태 관리)
├── screens/todo_list_screen.dart (할 일 목록 화면)
├── widgets/todo_item.dart (각 할 일 아이템 UI)
✅ 2. main.dart (Provider 등록)
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'providers/todo_provider.dart';
import 'screens/todo_list_screen.dart';
void main() {
runApp(
MultiProvider(
providers: [
ChangeNotifierProvider(create: (context) => TodoProvider()),
],
child: const MyApp(),
),
);
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
debugShowCheckedModeBanner: false,
title: '할 일 관리',
theme: ThemeData(
useMaterial3: true,
colorScheme: ColorScheme.fromSeed(seedColor: Colors.blue),
),
home: const TodoListScreen(),
);
}
}
📌 ChangeNotifierProvider를 사용하여 TodoProvider를 전역에서 사용 가능하도록 설정
✅ 3. providers/todo_provider.dart (상태 관리)
import 'package:flutter/material.dart';
class TodoProvider extends ChangeNotifier {
final List<String> _tasks = [];
List<String> get tasks => _tasks;
void addTask(String task) {
_tasks.add(task);
notifyListeners(); // 상태 변경 알림
}
void removeTask(int index) {
_tasks.removeAt(index);
notifyListeners(); // 상태 변경 알림
}
}
📌 할 일 목록을 Provider에서 관리하여 setState() 없이도 UI 업데이트 가능
✅ 4. screens/todo_list_screen.dart (UI + Provider 연동)
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import '../providers/todo_provider.dart';
import '../widgets/todo_item.dart';
class TodoListScreen extends StatefulWidget {
const TodoListScreen({super.key});
@override
TodoListScreenState createState() => TodoListScreenState();
}
class TodoListScreenState extends State<TodoListScreen> {
final TextEditingController _controller = TextEditingController();
void _addTask() {
if (_controller.text.isNotEmpty) {
Provider.of<TodoProvider>(context, listen: false).addTask(_controller.text);
_controller.clear();
}
}
@override
Widget build(BuildContext context) {
final todoProvider = Provider.of<TodoProvider>(context);
return Scaffold(
appBar: AppBar(title: const Text("할 일 목록")),
body: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
children: [
Row(
children: [
Expanded(
child: TextField(
controller: _controller,
decoration: const InputDecoration(
labelText: "새로운 할 일",
border: OutlineInputBorder(),
),
),
),
const SizedBox(width: 10),
ElevatedButton(
onPressed: _addTask,
child: const Text("추가"),
),
],
),
const SizedBox(height: 20),
Expanded(
child: todoProvider.tasks.isEmpty
? const Center(child: Text("할 일이 없습니다."))
: ListView.builder(
itemCount: todoProvider.tasks.length,
itemBuilder: (context, index) {
return TodoItem(index: index);
},
),
),
],
),
),
);
}
}
📌 Provider에서 데이터를 가져와 UI를 자동 업데이트
✅ 5. widgets/todo_item.dart (할 일 아이템 UI)
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import '../providers/todo_provider.dart';
class TodoItem extends StatelessWidget {
final int index;
const TodoItem({super.key, required this.index});
@override
Widget build(BuildContext context) {
final todoProvider = Provider.of<TodoProvider>(context);
return Card(
child: ListTile(
title: Text(todoProvider.tasks[index]),
trailing: IconButton(
icon: const Icon(Icons.delete, color: Colors.red),
onPressed: () => todoProvider.removeTask(index),
),
),
);
}
}
📌 각 할 일 아이템을 별도 위젯으로 분리하여 코드 가독성 향상
✅ 6. 실행 흐름
1️⃣ 앱 실행 → TodoProvider가 Provider로 전역 등록됨
2️⃣ 사용자가 입력 필드에 할 일 입력 후 "추가" 버튼 클릭
3️⃣ Provider의 addTask() 실행 → _tasks 리스트에 데이터 추가
4️⃣ notifyListeners() 실행 → UI 자동 업데이트 (ListView.builder() 새로고침됨)
5️⃣ 사용자가 삭제 버튼 클릭 → removeTask(index) 실행 → notifyListeners() 호출 → UI 업데이트
✅ 7. 실행 결과
사용자의 동작 | 화면 UI |
앱 실행 | "할 일이 없습니다." 표시 |
"운동하기" 입력 후 추가 | "운동하기 🗑" 리스트에 추가됨 |
"독서하기" 입력 후 추가 | "운동하기 🗑" "독서하기 🗑" 추가됨 |
"운동하기" 삭제 클릭 | "독서하기 🗑" 만 남음 |
✅ 8. Provider 사용의 장점
✔ 전역 상태 관리 가능 → setState() 없이 UI 업데이트
✔ 코드 구조가 깔끔해짐 → Provider를 사용하여 UI와 로직을 분리
✔ 앱 규모가 커질 때 유지보수가 쉬움
✅ 9. 기능 확장 테스트
📌 데이터 저장 기능 추가 → shared_preferences를 활용하여 앱을 종료해도 목록 유지
🔸1. shared_preferences 패키지 추가
flutter pub add shared_preferences
🔸2. providers/todo_provider.dart (할 일 저장 기능 추가)
import 'package:flutter/material.dart';
import 'package:shared_preferences/shared_preferences.dart';
class TodoProvider extends ChangeNotifier {
final List<String> _tasks = [];
List<String> get tasks => _tasks;
TodoProvider() {
_loadTasks(); // 앱 실행 시 저장된 데이터 불러오기
}
void _loadTasks() async {
final prefs = await SharedPreferences.getInstance();
_tasks.clear();
_tasks.addAll(prefs.getStringList('tasks') ?? []);
notifyListeners(); // UI 업데이트
}
void _saveTasks() async {
final prefs = await SharedPreferences.getInstance();
prefs.setStringList('tasks', _tasks); // 데이터 저장
}
void addTask(String task) {
_tasks.add(task);
_saveTasks(); // 저장
notifyListeners(); // UI 업데이트
}
void removeTask(int index) {
_tasks.removeAt(index);
_saveTasks(); // 저장
notifyListeners(); // UI 업데이트
}
}
💡 기능 추가 사항
1. TodoProvider() → 생성자에서 SharedPreferences를 사용하여 저장된 할 일 불러오기
2. _loadTasks() → 앱 실행 시 데이터 불러옴
3. _saveTasks() → 할 일 추가/삭제 시 데이터 저장
🔸 3. 실행 흐름
1. 앱 실행 → _loadTasks() 실행 → SharedPreferences에서 데이터 불러오기
2. 사용자가 할 일을 추가하면 addTask() 실행 → SharedPreferences에 저장됨
3. 삭제 시 removeTask() 실행 → SharedPreferences에서 즉시 반영됨
4. 앱 종료 후 다시 실행해도 데이터 유지됨
깔끔하게 기능 추가 가능 😆👍
'같이 공부합시다 - Flutter > Dart & Flutter 기초부터 실전까지' 카테고리의 다른 글
📌 Day 14: Riverpod 상태 관리 패턴 (Provider와 비교하며 배우는 실전 적용법) (0) | 2025.03.07 |
---|---|
📌 Day 13: Provider 상태 관리 적용 테스트 (Counter 앱, 다크모드까지) (1) | 2025.03.06 |
📌 Day 11: 상태 관리 개념 및 setState() 이해 (1) | 2025.03.04 |
📌 Day 10: Flutter 다양한 버튼 정리 , Buttons ! (3) | 2025.02.28 |
📌 Day 9: Flutter 입력 폼 가이드! (TextField, Form, 유효성 검사까지) (4) | 2025.02.27 |