Flutter에서 상태 관리는 어떻게 하나요?
질문
Flutter 앱에서 상태 관리를 위한 다양한 방법과 그 특징은 무엇인가요?
답변
Flutter에서 상태 관리는 애플리케이션의 복잡성과 규모가 커질수록 중요해지는 핵심 개념입니다. 상태 관리는 앱 내의 데이터 흐름과 UI 업데이트를 체계적으로 처리하는 방법을 의미합니다. Flutter는 다양한 상태 관리 방법을 제공하며, 각각 특정 사용 사례와 앱 규모에 적합합니다.
1. 로컬 상태 관리
StatefulWidget
가장 기본적인 상태 관리 방법은 StatefulWidget
을 사용하는 것입니다. 이는 위젯 내부에서 상태를 관리하는 방법으로, 작고 독립적인 상태에 적합합니다.
class CounterWidget extends StatefulWidget {
@override
_CounterWidgetState createState() => _CounterWidgetState();
}
class _CounterWidgetState extends State<CounterWidget> {
int _counter = 0;
void _incrementCounter() {
setState(() {
_counter++;
});
}
@override
Widget build(BuildContext context) {
return Column(
children: [
Text('카운터: $_counter'),
ElevatedButton(
onPressed: _incrementCounter,
child: Text('증가'),
),
],
);
}
}
장점:
- 간단하고 직관적
- 외부 종속성 없음
- 로컬 상태 관리에 적합
단점:
- 깊은 위젯 트리에서 상태 공유가 어려움
- 복잡한 상태 로직 처리에 적합하지 않음
- 위젯 간 상태 공유가 어려움
2. 상위 위젯을 통한 상태 관리
InheritedWidget
InheritedWidget
은 위젯 트리를 통해 데이터를 효율적으로 전달할 수 있는 방법입니다. Flutter의 많은 상태 관리 솔루션이 내부적으로 이를 사용합니다.
class CounterProvider extends InheritedWidget {
final int counter;
final Function increment;
CounterProvider({
required this.counter,
required this.increment,
required Widget child,
}) : super(child: child);
@override
bool updateShouldNotify(CounterProvider oldWidget) {
return counter != oldWidget.counter;
}
static CounterProvider of(BuildContext context) {
return context.dependOnInheritedWidgetOfExactType<CounterProvider>()!;
}
}
class CounterApp extends StatefulWidget {
@override
_CounterAppState createState() => _CounterAppState();
}
class _CounterAppState extends State<CounterApp> {
int _counter = 0;
void _incrementCounter() {
setState(() {
_counter++;
});
}
@override
Widget build(BuildContext context) {
return CounterProvider(
counter: _counter,
increment: _incrementCounter,
child: MyApp(),
);
}
}
class CounterDisplay extends StatelessWidget {
@override
Widget build(BuildContext context) {
final provider = CounterProvider.of(context);
return Text('카운터: ${provider.counter}');
}
}
class CounterIncrement extends StatelessWidget {
@override
Widget build(BuildContext context) {
final provider = CounterProvider.of(context);
return ElevatedButton(
onPressed: () => provider.increment(),
child: Text('증가'),
);
}
}
장점:
- 위젯 트리를 통한 효율적인 데이터 전달
- 불필요한 리빌드 방지
단점:
- 직접 사용하기에는 코드가 복잡함
- 복잡한 상태 로직에는 부족할 수 있음
Provider 패키지
Provider
는 InheritedWidget
을 더 사용하기 쉽게 만든 라이브러리로, Flutter 팀에서도 권장하는 방법 중 하나입니다.
// pubspec.yaml
// dependencies:
// provider: ^6.0.5
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
class Counter with ChangeNotifier {
int _count = 0;
int get count => _count;
void increment() {
_count++;
notifyListeners();
}
}
void main() {
runApp(
ChangeNotifierProvider(
create: (context) => Counter(),
child: MyApp(),
),
);
}
class CounterDisplay extends StatelessWidget {
@override
Widget build(BuildContext context) {
// listen: true로 Counter 객체의 변경 사항을 구독
final counter = Provider.of<Counter>(context);
return Text('카운터: ${counter.count}');
}
}
class CounterIncrement extends StatelessWidget {
@override
Widget build(BuildContext context) {
// listen: false로 UI 업데이트 없이 Counter 객체에 접근
final counter = Provider.of<Counter>(context, listen: false);
return ElevatedButton(
onPressed: () => counter.increment(),
child: Text('증가'),
);
}
}
장점:
- 간결한 API
- 종속성 주입 패턴 지원
- 다양한 Provider 유형 제공 (
ChangeNotifierProvider
,FutureProvider
,StreamProvider
등) - 테스트 용이성
단점:
- 중대형 앱에서는 코드 구성이 복잡해질 수 있음
- 확장성 제한
3. 상태 관리 라이브러리
Riverpod
Riverpod는 Provider의 개선된 버전으로, 컴파일 타임 안전성과 테스트 용이성을 제공합니다.
// pubspec.yaml
// dependencies:
// flutter_riverpod: ^2.3.6
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
// 상태 제공자 정의
final counterProvider = StateNotifierProvider<CounterNotifier, int>((ref) {
return CounterNotifier();
});
class CounterNotifier extends StateNotifier<int> {
CounterNotifier() : super(0);
void increment() => state++;
}
void main() {
runApp(
// 전체 앱을 ProviderScope로 감싸기
ProviderScope(
child: MyApp(),
),
);
}
class CounterWidget extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
// 상태 읽기
final count = ref.watch(counterProvider);
return Column(
children: [
Text('카운터: $count'),
ElevatedButton(
onPressed: () => ref.read(counterProvider.notifier).increment(),
child: Text('증가'),
),
],
);
}
}
장점:
- Provider보다 향상된 타입 안전성
- 전역 접근 없이도 제공자에 접근 가능
- 다양한 제공자 유형
- 테스트 용이성
단점:
- 학습 곡선이 있음
- Provider보다 복잡한 API
Bloc / Flutter Bloc
Bloc(Business Logic Component) 패턴은 이벤트 기반 상태 관리를 위한 패턴으로, 복잡한 애플리케이션에 적합합니다.
// pubspec.yaml
// dependencies:
// flutter_bloc: ^8.1.3
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
// 이벤트 정의
abstract class CounterEvent {}
class IncrementEvent extends CounterEvent {}
// Bloc 정의
class CounterBloc extends Bloc<CounterEvent, int> {
CounterBloc() : super(0) {
on<IncrementEvent>((event, emit) {
emit(state + 1);
});
}
}
void main() {
runApp(
BlocProvider(
create: (context) => CounterBloc(),
child: MyApp(),
),
);
}
class CounterPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
body: BlocBuilder<CounterBloc, int>(
builder: (context, count) {
return Column(
children: [
Text('카운터: $count'),
ElevatedButton(
onPressed: () => context.read<CounterBloc>().add(IncrementEvent()),
child: Text('증가'),
),
],
);
},
),
);
}
}
장점:
- 명확한 아키텍처
- 비즈니스 로직과 UI 완전 분리
- 대규모 앱에 적합
- 테스트 용이성
- 강력한 디버깅 도구
단점:
- 많은 보일러플레이트 코드
- 간단한 앱에는 과도할 수 있음
- 가파른 학습 곡선
GetX
GetX는 상태 관리, 라우팅, 종속성 관리를 위한 경량 라이브러리입니다.
// pubspec.yaml
// dependencies:
// get: ^4.6.5
import 'package:flutter/material.dart';
import 'package:get/get.dart';
class CounterController extends GetxController {
var count = 0.obs;
void increment() => count++;
}
void main() {
runApp(GetMaterialApp(home: Home()));
}
class Home extends StatelessWidget {
final CounterController controller = Get.put(CounterController());
@override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Obx(() => Text('카운터: ${controller.count}')),
ElevatedButton(
onPressed: () => controller.increment(),
child: Text('증가'),
),
],
),
),
);
}
}
장점:
- 간결한 문법
- 상태 관리, 라우팅, 종속성 주입 통합
- 빠른 개발 속도
- 낮은 학습 곡선
단점:
- 아키텍처 가이드라인 부족
- 큰 프로젝트에서 구조화 어려움
- 테스트 어려움
MobX
MobX는 반응형 프로그래밍 라이브러리로, 관찰 가능한 상태와 반응을 통해 상태를 관리합니다.
// pubspec.yaml
// dependencies:
// flutter_mobx: ^2.0.6+5
// mobx: ^2.2.0
// dev_dependencies:
// build_runner: ^2.4.6
// mobx_codegen: ^2.3.0
import 'package:flutter/material.dart';
import 'package:flutter_mobx/flutter_mobx.dart';
import 'package:mobx/mobx.dart';
// counter_store.dart
part 'counter_store.g.dart';
class CounterStore = _CounterStore with _$CounterStore;
abstract class _CounterStore with Store {
@observable
int count = 0;
@action
void increment() {
count++;
}
}
// main.dart
void main() {
runApp(MyApp());
}
class CounterWidget extends StatelessWidget {
final CounterStore store = CounterStore();
@override
Widget build(BuildContext context) {
return Column(
children: [
Observer(
builder: (_) => Text('카운터: ${store.count}'),
),
ElevatedButton(
onPressed: () => store.increment(),
child: Text('증가'),
),
],
);
}
}
장점:
- 반응형 프로그래밍 모델
- 자동 UI 업데이트
- 계산된 값(computed) 지원
- 코드 생성으로 보일러플레이트 감소
단점:
- 코드 생성 설정 필요
- 복잡한 상태 구조에서 이해하기 어려울 수 있음
Redux
Redux는 단일 방향 데이터 흐름과 예측 가능한 상태 컨테이너를 제공하는 라이브러리입니다.
// pubspec.yaml
// dependencies:
// flutter_redux: ^0.10.0
// redux: ^5.0.0
import 'package:flutter/material.dart';
import 'package:flutter_redux/flutter_redux.dart';
import 'package:redux/redux.dart';
// 액션
enum Actions { Increment }
// 리듀서
int counterReducer(int state, dynamic action) {
if (action == Actions.Increment) {
return state + 1;
}
return state;
}
void main() {
final store = Store<int>(counterReducer, initialState: 0);
runApp(MyApp(store: store));
}
class MyApp extends StatelessWidget {
final Store<int> store;
MyApp({required this.store});
@override
Widget build(BuildContext context) {
return StoreProvider<int>(
store: store,
child: MaterialApp(
home: CounterPage(),
),
);
}
}
class CounterPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
StoreConnector<int, String>(
converter: (store) => store.state.toString(),
builder: (context, count) {
return Text('카운터: $count');
},
),
StoreConnector<int, VoidCallback>(
converter: (store) {
return () => store.dispatch(Actions.Increment);
},
builder: (context, callback) {
return ElevatedButton(
onPressed: callback,
child: Text('증가'),
);
},
),
],
),
),
);
}
}
장점:
- 예측 가능한 상태 관리
- 단일 방향 데이터 흐름
- 시간 여행 디버깅
- 미들웨어 지원
단점:
- 많은 보일러플레이트 코드
- 간단한 앱에는 과도할 수 있음
- 가파른 학습 곡선
4. 기타 접근법
ValueNotifier와 ValueListenableBuilder
Flutter의 내장 기능을 사용한 간단한 상태 관리 방법입니다.
class CounterWidget extends StatelessWidget {
final ValueNotifier<int> counter = ValueNotifier<int>(0);
@override
Widget build(BuildContext context) {
return Column(
children: [
ValueListenableBuilder<int>(
valueListenable: counter,
builder: (context, value, child) {
return Text('카운터: $value');
},
),
ElevatedButton(
onPressed: () => counter.value++,
child: Text('증가'),
),
],
);
}
}
장점:
- 외부 종속성 없음
- 간단한 상태에 적합
- 낮은 오버헤드
단점:
- 복잡한 상태 관리에 부적합
- 전역 상태 관리가 어려움
Streams와 StreamBuilder
Dart의 Stream을 활용한 반응형 상태 관리 방법입니다.
class CounterBloc {
final _counterController = StreamController<int>();
int _counter = 0;
Stream<int> get counterStream => _counterController.stream;
void increment() {
_counter++;
_counterController.sink.add(_counter);
}
void dispose() {
_counterController.close();
}
}
class CounterWidget extends StatefulWidget {
@override
_CounterWidgetState createState() => _CounterWidgetState();
}
class _CounterWidgetState extends State<CounterWidget> {
final CounterBloc _bloc = CounterBloc();
@override
void dispose() {
_bloc.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Column(
children: [
StreamBuilder<int>(
stream: _bloc.counterStream,
initialData: 0,
builder: (context, snapshot) {
return Text('카운터: ${snapshot.data}');
},
),
ElevatedButton(
onPressed: () => _bloc.increment(),
child: Text('증가'),
),
],
);
}
}
장점:
- 반응형 프로그래밍 모델
- 비동기 이벤트 처리에 강함
- 외부 패키지 불필요
단점:
- StreamController 관리 필요
- 복잡한 상태에서 코드가 복잡해질 수 있음
상태 관리 선택 가이드
상태 관리 방법을 선택할 때 고려해야 할 사항:
앱 복잡성
- 간단한 앱:
StatefulWidget
,Provider
,GetX
- 중간 복잡도:
Provider
,Riverpod
,MobX
- 복잡한 앱:
Bloc
,Redux
,Riverpod
- 간단한 앱:
팀 경험
- 학습 곡선이 낮은 것:
Provider
,GetX
- 체계적인 아키텍처 필요:
Bloc
,Redux
,Riverpod
- 학습 곡선이 낮은 것:
상태 종류
- 로컬 상태:
StatefulWidget
,ValueNotifier
- 공유 상태:
Provider
,Riverpod
,Bloc
- 전역 상태:
Provider
,Riverpod
,Redux
,GetX
- 로컬 상태:
비동기 작업
- 비동기 작업이 많은 경우:
Bloc
,Riverpod
,Streams
- 비동기 작업이 많은 경우:
테스트 용이성
- 테스트 중심 개발:
Bloc
,Redux
,Riverpod
- 테스트 중심 개발:
멀티 상태 관리 접근법
실제 대규모 앱에서는 한 가지 상태 관리 방법만 사용하는 것이 아니라, 여러 접근법을 함께 사용하는 경우가 많습니다:
- 로컬 UI 상태는
StatefulWidget
으로 - 화면 간 공유되는 상태는
Provider
나Riverpod
으로 - 복잡한 비즈니스 로직은
Bloc
패턴으로
요약
Flutter의 상태 관리는 앱의 요구사항과 규모에 따라 다양한 방법을 선택할 수 있습니다:
로컬 상태 관리:
StatefulWidget
ValueNotifier
및ValueListenableBuilder
공유 상태 관리:
InheritedWidget
Provider
라이브러리 기반 상태 관리:
Riverpod
Bloc
/Flutter Bloc
GetX
MobX
Redux
기타 접근법:
- Stream 및 StreamBuilder
- RxDart
적절한 상태 관리 방법을 선택하는 것은 앱의 복잡성, 팀의 경험, 테스트 요구사항 등에 따라 달라질 수 있습니다. 작은 앱에서는 단순한 방법을 사용하고, 앱이 성장함에 따라 더 구조화된 접근법으로 전환하는 것이 일반적입니다.