📌 Day 12: Provider 패턴 적용 (ChangeNotifier, Consumer)

2025. 3. 5. 10:46같이 공부합시다 - Flutter/Dart & Flutter 기초부터 실전까지

728x90
반응형

📃 개요

⚠️ 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. 앱 종료 후 다시 실행해도 데이터 유지됨
 
깔끔하게 기능 추가 가능 😆👍

728x90
반응형