Flutter에서 상태 관리 패턴은 어떤 것들이 있나요?
질문
Flutter에서 사용할 수 있는 다양한 상태 관리 패턴과 그 장단점을 설명해주세요.
답변
Flutter 앱 개발에서 상태 관리는 매우 중요한 개념입니다. 상태 관리는 앱의 데이터를 어떻게 저장하고, 업데이트하며, UI에 반영할지를 결정합니다. Flutter는 다양한 상태 관리 패턴과 라이브러리를 지원하며, 각각의 접근 방식은 고유한 장단점을 가지고 있습니다.
1. setState
가장 기본적인 Flutter의 상태 관리 방법입니다.
구현 예시
class CounterScreen extends StatefulWidget {
@override
_CounterScreenState createState() => _CounterScreenState();
}
class _CounterScreenState extends State<CounterScreen> {
int _counter = 0;
void _incrementCounter() {
setState(() {
_counter++;
});
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('카운터 앱')),
body: Center(
child: Text(
'$_counter',
style: TextStyle(fontSize: 24),
),
),
floatingActionButton: FloatingActionButton(
onPressed: _incrementCounter,
child: Icon(Icons.add),
),
);
}
}
장점
- 간단하고 직관적
- Flutter에 내장되어 있어 추가 패키지 필요 없음
- 작은 앱이나 프로토타입에 적합
단점
- 위젯 트리가 커질수록 상태 관리가 복잡해짐
- 상태를 공유하려면 상위 위젯으로 끌어올려야 함
- 깊은 위젯 트리에서 상태 전달이 번거로움
2. InheritedWidget 및 Provider
InheritedWidget은 Flutter의 내장 기능으로, Provider 패키지는 이를 기반으로 더 사용하기 쉽게 만든 라이브러리입니다.
Provider 구현 예시
// 상태 클래스
class CounterModel extends ChangeNotifier {
int _count = 0;
int get count => _count;
void increment() {
_count++;
notifyListeners();
}
}
// 메인 앱
void main() {
runApp(
ChangeNotifierProvider(
create: (context) => CounterModel(),
child: MyApp(),
),
);
}
// 상태를 사용하는 위젯
class CounterDisplay extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Text(
// 상태 읽기
'${context.watch<CounterModel>().count}',
style: TextStyle(fontSize: 24),
);
}
}
class CounterIncrement extends StatelessWidget {
@override
Widget build(BuildContext context) {
return FloatingActionButton(
// 상태 업데이트
onPressed: () => context.read<CounterModel>().increment(),
child: Icon(Icons.add),
);
}
}
장점
- 코드 구조화가 용이
- 상태를 위젯 트리 전체에 쉽게 공유
- 상대적으로 낮은 학습 곡선
- 공식 Flutter 팀이 권장
단점
- 복잡한 상태 관리에는 다소 제한적
- 대규모 앱에서는 코드 구성이 복잡해질 수 있음
3. Riverpod
Provider의 개선 버전으로, Provider의 일부 제한사항을 해결한 라이브러리입니다.
구현 예시
// 프로바이더 정의
final counterProvider = StateNotifierProvider<CounterNotifier, int>((ref) {
return CounterNotifier();
});
// 상태 클래스
class CounterNotifier extends StateNotifier<int> {
CounterNotifier() : super(0);
void increment() => state++;
}
// 위젯에서 사용
class CounterWidget extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
final count = ref.watch(counterProvider);
return Scaffold(
appBar: AppBar(title: Text('Riverpod 예제')),
body: Center(
child: Text('$count', style: TextStyle(fontSize: 24)),
),
floatingActionButton: FloatingActionButton(
onPressed: () => ref.read(counterProvider.notifier).increment(),
child: Icon(Icons.add),
),
);
}
}
장점
- Provider보다 타입 안전성 향상
- 컴파일 타임에 의존성 검사
- 프로바이더 오버라이드 기능
- 테스트가 용이
단점
- Provider에 비해 다소 복잡한 API
- 새로운 개념 학습 필요
4. BLoC (Business Logic Component)
Streams와 RxDart를 사용한 상태 관리 패턴입니다.
구현 예시
// BLoC 클래스
class CounterBloc {
final _counterStateController = StreamController<int>();
StreamSink<int> get _inCounter => _counterStateController.sink;
Stream<int> get counter => _counterStateController.stream;
final _counterEventController = StreamController<CounterEvent>();
Sink<CounterEvent> get counterEventSink => _counterEventController.sink;
int _counter = 0;
CounterBloc() {
_counterEventController.stream.listen(_mapEventToState);
}
void _mapEventToState(CounterEvent event) {
if (event is IncrementEvent) {
_counter++;
} else if (event is DecrementEvent) {
_counter--;
}
_inCounter.add(_counter);
}
void dispose() {
_counterStateController.close();
_counterEventController.close();
}
}
// 이벤트 클래스들
abstract class CounterEvent {}
class IncrementEvent extends CounterEvent {}
class DecrementEvent extends CounterEvent {}
// 사용 예시
class CounterPage extends StatefulWidget {
@override
_CounterPageState createState() => _CounterPageState();
}
class _CounterPageState extends State<CounterPage> {
final CounterBloc _bloc = CounterBloc();
@override
void dispose() {
_bloc.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('BLoC 패턴 예제')),
body: Center(
child: StreamBuilder(
stream: _bloc.counter,
initialData: 0,
builder: (context, snapshot) {
return Text(
'${snapshot.data}',
style: TextStyle(fontSize: 24),
);
},
),
),
floatingActionButton: Column(
mainAxisAlignment: MainAxisAlignment.end,
children: [
FloatingActionButton(
child: Icon(Icons.add),
onPressed: () {
_bloc.counterEventSink.add(IncrementEvent());
},
),
SizedBox(height: 10),
FloatingActionButton(
child: Icon(Icons.remove),
onPressed: () {
_bloc.counterEventSink.add(DecrementEvent());
},
),
],
),
);
}
}
장점
- 비즈니스 로직과 UI 분리
- 테스트가 용이
- 상태 변화를 스트림으로 관리
- 복잡한 상태 관리에 적합
단점
- 학습 곡선이 높음
- 간단한 앱에는 과도한 boilerplate 코드
- 스트림 관리의 복잡성
5. flutter_bloc 패키지
BLoC 패턴을 더 쉽게 구현할 수 있도록 돕는 라이브러리입니다.
구현 예시
// 이벤트, 상태 정의
abstract class CounterEvent {}
class IncrementEvent extends CounterEvent {}
class DecrementEvent extends CounterEvent {}
// BLoC 구현
class CounterBloc extends Bloc<CounterEvent, int> {
CounterBloc() : super(0) {
on<IncrementEvent>((event, emit) => emit(state + 1));
on<DecrementEvent>((event, emit) => emit(state - 1));
}
}
// 위젯에서 사용
class CounterPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return BlocProvider(
create: (context) => CounterBloc(),
child: Scaffold(
appBar: AppBar(title: Text('flutter_bloc 예제')),
body: Center(
child: BlocBuilder<CounterBloc, int>(
builder: (context, count) {
return Text(
'$count',
style: TextStyle(fontSize: 24),
);
},
),
),
floatingActionButton: Column(
mainAxisAlignment: MainAxisAlignment.end,
children: [
FloatingActionButton(
child: Icon(Icons.add),
onPressed: () {
context.read<CounterBloc>().add(IncrementEvent());
},
),
SizedBox(height: 10),
FloatingActionButton(
child: Icon(Icons.remove),
onPressed: () {
context.read<CounterBloc>().add(DecrementEvent());
},
),
],
),
),
);
}
}
장점
- BLoC 패턴의 구현을 단순화
- 상태 변화 추적 용이
- 널리 사용되는 패턴
- 대규모 앱에 적합
단점
- 작은 앱에는 과도할 수 있음
- 여전히 학습 곡선 존재
6. GetX
상태 관리, 라우팅, 종속성 주입을 포함한 경량 패키지입니다.
구현 예시
// 컨트롤러 정의
class CounterController extends GetxController {
var count = 0.obs;
void increment() => count++;
}
// 바인딩 클래스
class CounterBinding extends Bindings {
@override
void dependencies() {
Get.lazyPut(() => CounterController());
}
}
// 위젯에서 사용
class CounterPage extends StatelessWidget {
final CounterController controller = Get.find();
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('GetX 예제')),
body: Center(
child: Obx(() => Text(
'${controller.count}',
style: TextStyle(fontSize: 24),
)),
),
floatingActionButton: FloatingActionButton(
child: Icon(Icons.add),
onPressed: controller.increment,
),
);
}
}
// 라우팅 설정
void main() {
runApp(
GetMaterialApp(
initialRoute: '/home',
getPages: [
GetPage(
name: '/home',
page: () => CounterPage(),
binding: CounterBinding(),
),
],
),
);
}
장점
- 간결한 코드
- 상태 관리, 라우팅, DI 통합
- 적은 boilerplate 코드
- 빠른 개발 가능
단점
- 아키텍처가 덜 명확할 수 있음
- 큰 프로젝트에서는 구조화가 어려울 수 있음
- Flutter의 일반적 패턴에서 벗어남
7. Redux
단방향 데이터 흐름을 강조하는 상태 관리 패턴입니다.
구현 예시
// 액션 정의
enum Actions { Increment, Decrement }
// 상태 클래스
class AppState {
final int counter;
AppState({this.counter = 0});
AppState copyWith({int? counter}) {
return AppState(
counter: counter ?? this.counter,
);
}
}
// 리듀서
AppState reducer(AppState state, dynamic action) {
if (action == Actions.Increment) {
return state.copyWith(counter: state.counter + 1);
} else if (action == Actions.Decrement) {
return state.copyWith(counter: state.counter - 1);
}
return state;
}
// 스토어 설정
final store = Store<AppState>(
reducer,
initialState: AppState(),
);
// 위젯에서 사용
class CounterPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return StoreProvider<AppState>(
store: store,
child: Scaffold(
appBar: AppBar(title: Text('Redux 예제')),
body: Center(
child: StoreConnector<AppState, String>(
converter: (store) => store.state.counter.toString(),
builder: (context, count) {
return Text(
count,
style: TextStyle(fontSize: 24),
);
},
),
),
floatingActionButton: Column(
mainAxisAlignment: MainAxisAlignment.end,
children: [
StoreConnector<AppState, VoidCallback>(
converter: (store) {
return () => store.dispatch(Actions.Increment);
},
builder: (context, callback) {
return FloatingActionButton(
child: Icon(Icons.add),
onPressed: callback,
);
},
),
SizedBox(height: 10),
StoreConnector<AppState, VoidCallback>(
converter: (store) {
return () => store.dispatch(Actions.Decrement);
},
builder: (context, callback) {
return FloatingActionButton(
child: Icon(Icons.remove),
onPressed: callback,
);
},
),
],
),
),
);
}
}
장점
- 예측 가능한 상태 관리
- 시간 여행 디버깅 가능
- 잘 정의된 상태 흐름
- 미들웨어를 통한 확장성
단점
- 많은 boilerplate 코드
- 간단한 앱에는 과도한 복잡성
- 가파른 학습 곡선
8. MobX
관찰 가능한 상태 패턴에 기반한 상태 관리 솔루션입니다.
구현 예시
// store 클래스
part 'counter_store.g.dart';
class CounterStore = _CounterStore with _$CounterStore;
abstract class _CounterStore with Store {
@observable
int count = 0;
@action
void increment() {
count++;
}
@action
void decrement() {
count--;
}
}
// 위젯에서 사용
class CounterPage extends StatelessWidget {
final CounterStore store = CounterStore();
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('MobX 예제')),
body: Center(
child: Observer(
builder: (_) => Text(
'${store.count}',
style: TextStyle(fontSize: 24),
),
),
),
floatingActionButton: Column(
mainAxisAlignment: MainAxisAlignment.end,
children: [
FloatingActionButton(
child: Icon(Icons.add),
onPressed: store.increment,
),
SizedBox(height: 10),
FloatingActionButton(
child: Icon(Icons.remove),
onPressed: store.decrement,
),
],
),
);
}
}
장점
- 반응형 프로그래밍 접근 방식
- 코드 생성을 통한 간결함
- 명확한 상태 추적
- 강력한 디버깅 도구
단점
- 코드 생성 과정 필요
- 처음 설정이 복잡할 수 있음
- React 경험이 있는 개발자에게 더 친숙
9. 상태 관리 패턴 선택 기준
어떤 상태 관리 패턴을 선택할지는 다음 기준을 고려해야 합니다:
앱의 복잡성: 작은 앱은 setState나 Provider가 적합하고, 큰 앱은 BLoC, Riverpod, Redux 등이 적합
팀의 경험: 팀이 이미 익숙한 패턴 또는 학습 곡선이 낮은 패턴
유지보수성: 코드가 명확하고 유지관리가 쉬운 패턴
성능 요구사항: 앱의 성능 요구 사항에 맞는 패턴
개발 속도: 빠른 개발이 필요한 경우 GetX나 Provider가 유리
테스트 용이성: 테스트하기 쉬운 구조가 필요하면 BLoC이나 Redux 고려
10. 다양한 상태 유형 관리
Flutter 앱에서는 일반적으로 다음과 같은 여러 유형의 상태를 관리해야 합니다:
10.1 UI 상태 (Ephemeral State)
- 단일 위젯 내에 존재하는 임시 상태
- 예: 폼 입력 값, 애니메이션 상태
- 권장 관리 방법: setState 또는 지역 변수
10.2 앱 상태 (App State)
- 앱 전체에서 공유되는 상태
- 예: 사용자 정보, 설정, 장바구니 내용
- 권장 관리 방법: Provider, Riverpod, BLoC 등
10.3 서버 상태 (Server State)
- API에서 가져온 데이터
- 권장 관리 방법: flutter_hooks, dio와 Repository 패턴, GetX, flutter_bloc, flutter_query
요약
Flutter에서 다양한 상태 관리 솔루션을 제공하며, 각각은 고유한 장단점이 있습니다:
- setState: 간단하지만 확장성 제한적
- Provider: 중소규모 앱에 적합한 Flutter 공식 권장 솔루션
- Riverpod: Provider의 개선 버전으로 타입 안전성 강화
- BLoC: 확장성이 뛰어나지만 학습 곡선이 높음
- flutter_bloc: BLoC 패턴을 단순화한 패키지
- GetX: 간결한 코드와 빠른 개발이 가능한 통합 솔루션
- Redux: 예측 가능하고 디버깅이 용이한 단방향 데이터 흐름
- MobX: 관찰 가능한 상태와 반응형 UI 업데이트에 중점
앱의 요구사항과 팀의 선호도에 따라 적절한 상태 관리 패턴을 선택하는 것이 중요합니다. 복잡한 앱의 경우 여러 패턴을 함께 사용하는 하이브리드 접근 방식도 고려할 수 있습니다.