Flutter 튜토리얼 12편: 상태 관리 라이브러리 비교
요약
핵심 요지
- 문제 정의: Flutter에는 다양한 상태 관리 라이브러리가 있어서 어떤 것을 선택해야 할지 혼란스럽다.
- 핵심 주장: 팀 규모, 앱 복잡도, 유지보수 요구사항에 따라 적합한 라이브러리가 다르다.
- 주요 근거:
Provider1는 입문용으로 적합하고,Riverpod2은 신규 프로젝트의 표준이 되고 있으며,BLoC3은 대규모 엔터프라이즈에 적합하다. - 실무 기준: 소규모 앱은 Provider나 setState, 중대규모 앱은 Riverpod, 엔터프라이즈는 BLoC을 권장한다.
- 한계: 완벽한 라이브러리는 없으며, 팀의 경험과 프로젝트 요구사항에 따라 선택해야 한다.
문서가 설명하는 범위
- Flutter의 기본 제공 상태 관리 방식
- 주요 상태 관리 라이브러리 비교 (Provider, Riverpod, BLoC, GetX)
- 라이브러리별 코드 예시
- 프로젝트 상황에 따른 선택 기준
읽는 시간: 18분 | 난이도: 중급
참고 자료
- List of state management approaches - Flutter 공식 상태 관리 옵션
- State management packages on pub.dev - pub.dev 상태 관리 패키지
문제 상황
Flutter 생태계에는 수십 개의 상태 관리 라이브러리가 존재합니다.
pub.dev에서 “state-management” 토픽으로 검색하면 100개 이상의 패키지가 나옵니다.
선택의 어려움
상태 관리 라이브러리 선택 시 고민├── Provider - Google 공식 권장, 간단함├── Riverpod - Provider 개선, 컴파일 타임 안전성├── BLoC - 이벤트 기반, 엔터프라이즈 표준├── GetX - 올인원, 간편함├── Redux - React 생태계에서 유명├── MobX - 리액티브 프로그래밍└── ... 수십 개 더어떤 라이브러리가 “최고”인지 묻는 것은 의미가 없습니다. 중요한 것은 “내 프로젝트와 팀에 맞는” 라이브러리를 선택하는 것입니다.
해결 방법
라이브러리를 선택하기 전에 각각의 특징과 적합한 상황을 이해해야 합니다. Flutter에서 가장 많이 사용되는 4가지 접근 방식을 비교합니다.
챕터 1: Flutter 기본 제공 방식 이해하기
Why
NOTE외부 라이브러리 없이도 Flutter가 제공하는 기본 도구로 상태를 관리할 수 있습니다.
기본 방식을 이해해야 라이브러리들이 해결하려는 문제가 무엇인지 알 수 있습니다.// Flutter 기본 제공 상태 관리// 1. setState - 위젯 내부 상태// 2. InheritedWidget - 위젯 트리 공유// 3. ValueNotifier - 단일 값 변경 감지
What
NOTEFlutter는 세 가지 기본 상태 관리 도구를 제공합니다.
도구 용도 복잡도 setState단일 위젯 내부 상태 낮음 InheritedWidget위젯 트리에 데이터 공유 중간 ValueNotifier단일 값 변경 감지 낮음
How
TIPsetState - 가장 기본적인 방식
class Counter extends StatefulWidget {@overrideState<Counter> createState() => _CounterState();}class _CounterState extends State<Counter> {int _count = 0;@overrideWidget build(BuildContext context) {return Column(children: [Text('Count: $_count'),ElevatedButton(onPressed: () {setState(() {_count++;});},child: Text('Increment'),),],);}}ValueNotifier와 ValueListenableBuilder
class CounterWithNotifier extends StatefulWidget {@overrideState<CounterWithNotifier> createState() => _CounterWithNotifierState();}class _CounterWithNotifierState extends State<CounterWithNotifier> {final ValueNotifier<int> _counter = ValueNotifier(0);@overridevoid dispose() {_counter.dispose();super.dispose();}@overrideWidget build(BuildContext context) {return Column(children: [ValueListenableBuilder<int>(valueListenable: _counter,builder: (context, value, child) {return Text('Count: $value');},),ElevatedButton(onPressed: () => _counter.value++,child: Text('Increment'),),],);}}InheritedWidget - 위젯 트리 공유
class CounterProvider extends InheritedWidget {final int count;final VoidCallback increment;const CounterProvider({required this.count,required this.increment,required Widget child,}) : super(child: child);static CounterProvider of(BuildContext context) {return context.dependOnInheritedWidgetOfExactType<CounterProvider>()!;}@overridebool updateShouldNotify(CounterProvider oldWidget) {return count != oldWidget.count;}}
Watch out
WARNING기본 제공 도구만으로는 복잡한 앱을 관리하기 어렵습니다.
// InheritedWidget의 한계// 1. 상태 변경 로직을 별도로 구현해야 함// 2. 위젯 재빌드 최적화가 어려움// 3. 보일러플레이트 코드가 많음// 이런 한계를 해결하기 위해 상태 관리 라이브러리가 등장
결론: 기본 도구는 간단한 상태에 적합하지만, 복잡한 앱에서는 라이브러리가 필요합니다.
챕터 2: Provider 이해하기
Why
NOTE
InheritedWidget을 직접 사용하기에는 보일러플레이트가 많습니다.
Provider는 InheritedWidget을 감싸서 사용하기 쉽게 만든 패키지입니다.// InheritedWidget 직접 사용: 50줄 이상// Provider 사용: 10줄 이하
What
NOTEProvider는 Flutter 팀이 공식 권장하는 상태 관리 패키지입니다.
간단한 API로 위젯 트리에 데이터를 제공하고 변경을 감지합니다.
특징 설명 장점 배우기 쉬움, 공식 권장, 안정적 단점 런타임 타입 체크, 복잡한 의존성 처리 어려움 적합한 상황 입문자, 소규모~중규모 앱
How
TIP설치
pubspec.yaml dependencies:provider: ^6.1.0ChangeNotifier 정의
import 'package:flutter/foundation.dart';class CartModel extends ChangeNotifier {final List<String> _items = [];List<String> get items => List.unmodifiable(_items);int get totalItems => _items.length;void add(String item) {_items.add(item);notifyListeners(); // 리스너들에게 변경 알림}void remove(String item) {_items.remove(item);notifyListeners();}}Provider 제공
void main() {runApp(ChangeNotifierProvider(create: (context) => CartModel(),child: MyApp(),),);}상태 사용 - Consumer
class CartPage extends StatelessWidget {@overrideWidget build(BuildContext context) {return Consumer<CartModel>(builder: (context, cart, child) {return Column(children: [Text('Total items: ${cart.totalItems}'),ListView.builder(shrinkWrap: true,itemCount: cart.items.length,itemBuilder: (context, index) {return ListTile(title: Text(cart.items[index]));},),],);},);}}상태 변경 - Provider.of
class AddButton extends StatelessWidget {@overrideWidget build(BuildContext context) {return ElevatedButton(onPressed: () {// listen: false는 변경을 구독하지 않음context.read<CartModel>().add('New Item');},child: Text('Add Item'),);}}
Watch out
WARNINGProvider는 런타임에 타입을 체크하므로 컴파일 타임에 오류를 잡지 못합니다.
// Provider가 없으면 런타임에 오류 발생class SomePage extends StatelessWidget {@overrideWidget build(BuildContext context) {// CartModel Provider가 상위에 없으면?final cart = context.read<CartModel>(); // 런타임 오류!return Text('Items: ${cart.totalItems}');}}BuildContext에 의존하므로 위젯 밖에서 상태에 접근하기 어렵습니다.
결론: Provider는 입문자에게 적합하고, 소규모 앱에서 충분히 잘 동작합니다.
챕터 3: Riverpod 이해하기
Why
NOTEProvider의 창시자가 Provider의 한계를 해결하기 위해 만든 패키지입니다.
BuildContext 없이도 상태에 접근할 수 있고, 컴파일 타임 안전성을 제공합니다.// Provider: 런타임 오류context.read<CartModel>(); // Provider 없으면 런타임 에러// Riverpod: 컴파일 타임 체크ref.watch(cartProvider); // Provider 없으면 컴파일 에러
What
NOTERiverpod은 현재 Flutter 신규 프로젝트의 표준으로 자리잡고 있습니다.
컴파일 타임 안전성, 자동 캐싱, 코드 생성 지원이 핵심 특징입니다.
특징 설명 장점 컴파일 타임 안전성, 테스트 용이, 자동 캐싱 단점 Provider보다 학습 곡선 있음, 기존 코드 마이그레이션 필요 적합한 상황 신규 프로젝트, 중대규모 앱, 테스트 중시
How
TIP설치
pubspec.yaml dependencies:flutter_riverpod: ^2.5.0riverpod_annotation: ^2.3.0dev_dependencies:riverpod_generator: ^2.4.0build_runner: ^2.4.0Provider 정의 (코드 생성 방식)
import 'package:riverpod_annotation/riverpod_annotation.dart';part 'cart_provider.g.dart';@riverpodclass Cart extends _$Cart {@overrideList<String> build() {return []; // 초기 상태}void add(String item) {state = [...state, item];}void remove(String item) {state = state.where((e) => e != item).toList();}}앱 설정
void main() {runApp(ProviderScope( // Riverpod의 최상위 위젯child: MyApp(),),);}상태 사용
class CartPage extends ConsumerWidget {@overrideWidget build(BuildContext context, WidgetRef ref) {final cartItems = ref.watch(cartProvider); // 상태 구독return Column(children: [Text('Total items: ${cartItems.length}'),ListView.builder(shrinkWrap: true,itemCount: cartItems.length,itemBuilder: (context, index) {return ListTile(title: Text(cartItems[index]));},),],);}}상태 변경
class AddButton extends ConsumerWidget {@overrideWidget build(BuildContext context, WidgetRef ref) {return ElevatedButton(onPressed: () {ref.read(cartProvider.notifier).add('New Item');},child: Text('Add Item'),);}}
Watch out
WARNINGRiverpod은 Provider와 완전히 다른 패키지입니다.
기존 Provider 프로젝트를 마이그레이션하려면 상당한 작업이 필요합니다.// Provider → Riverpod 마이그레이션// 1. 모든 Provider를 Riverpod Provider로 변환// 2. Consumer → ConsumerWidget 또는 Consumer로 변경// 3. context.read → ref.read로 변경// 점진적 마이그레이션 불가능 - 한 번에 전환 필요코드 생성 방식을 사용하면
build_runner를 실행해야 합니다.Terminal window dart run build_runner watch
결론: Riverpod은 신규 프로젝트에 권장되며, 컴파일 타임 안전성이 큰 장점입니다.
챕터 4: BLoC 이해하기
Why
NOTE대규모 팀이나 엔터프라이즈 앱에서는 엄격한 아키텍처가 필요합니다.
BLoC은 이벤트 기반 아키텍처로 명확한 구조와 감사 추적(audit trail)을 제공합니다.// BLoC의 핵심 원칙// UI → Event → BLoC → State → UI// 모든 상태 변경이 이벤트로 기록됨
What
NOTEBLoC(Business Logic Component)은 UI와 비즈니스 로직을 완전히 분리합니다.
이벤트 기반 아키텍처로 모든 상태 변경을 추적할 수 있습니다.
특징 설명 장점 엄격한 구조, 테스트 용이, 이벤트 추적 가능 단점 보일러플레이트 많음, 학습 곡선 가파름 적합한 상황 대규모 팀, 엔터프라이즈 앱, 규제 산업
How
TIP설치
pubspec.yaml dependencies:flutter_bloc: ^8.1.0이벤트 정의
cart_event.dart abstract class CartEvent {}class AddItem extends CartEvent {final String item;AddItem(this.item);}class RemoveItem extends CartEvent {final String item;RemoveItem(this.item);}상태 정의
cart_state.dart class CartState {final List<String> items;const CartState({this.items = const []});CartState copyWith({List<String>? items}) {return CartState(items: items ?? this.items);}}BLoC 정의
cart_bloc.dart import 'package:flutter_bloc/flutter_bloc.dart';class CartBloc extends Bloc<CartEvent, CartState> {CartBloc() : super(const CartState()) {on<AddItem>(_onAddItem);on<RemoveItem>(_onRemoveItem);}void _onAddItem(AddItem event, Emitter<CartState> emit) {final updatedItems = List<String>.from(state.items)..add(event.item);emit(state.copyWith(items: updatedItems));}void _onRemoveItem(RemoveItem event, Emitter<CartState> emit) {final updatedItems = List<String>.from(state.items)..remove(event.item);emit(state.copyWith(items: updatedItems));}}BLoC 제공
void main() {runApp(BlocProvider(create: (context) => CartBloc(),child: MyApp(),),);}상태 사용
class CartPage extends StatelessWidget {@overrideWidget build(BuildContext context) {return BlocBuilder<CartBloc, CartState>(builder: (context, state) {return Column(children: [Text('Total items: ${state.items.length}'),ListView.builder(shrinkWrap: true,itemCount: state.items.length,itemBuilder: (context, index) {return ListTile(title: Text(state.items[index]));},),],);},);}}이벤트 발생
class AddButton extends StatelessWidget {@overrideWidget build(BuildContext context) {return ElevatedButton(onPressed: () {context.read<CartBloc>().add(AddItem('New Item'));},child: Text('Add Item'),);}}
Watch out
WARNINGBLoC은 간단한 기능에도 많은 파일과 코드가 필요합니다.
// 카운터 하나를 위해 필요한 파일들// 1. counter_event.dart - 이벤트 정의// 2. counter_state.dart - 상태 정의// 3. counter_bloc.dart - 로직 정의// 4. counter_page.dart - UI// 간단한 앱에는 과도한 구조Cubit을 사용하면 이벤트 없이 간소화할 수 있습니다.
class CounterCubit extends Cubit<int> {CounterCubit() : super(0);void increment() => emit(state + 1);void decrement() => emit(state - 1);}
결론: BLoC은 대규모 팀과 엔터프라이즈 앱에서 강력한 구조를 제공합니다.
챕터 5: GetX 이해하기
Why
NOTEGetX는 상태 관리, 라우팅, 의존성 주입을 하나의 패키지에 제공합니다.
빠른 개발과 적은 보일러플레이트를 목표로 합니다.// GetX는 "올인원" 솔루션// 상태 관리 + 라우팅 + 의존성 주입 + 유틸리티
What
NOTEGetX는 간편한 API로 빠른 개발을 가능하게 하지만, 주의가 필요합니다.
최근 유지보수 지연과 Flutter SDK 호환성 문제가 보고되고 있습니다.
특징 설명 장점 적은 보일러플레이트, 올인원 솔루션, 배우기 쉬움 단점 유지보수 지연, 전역 상태 디버깅 어려움, 메모리 누수 위험 적합한 상황 프로토타입, 소규모 개인 프로젝트
How
TIP설치
pubspec.yaml dependencies:get: ^4.6.6Controller 정의
import 'package:get/get.dart';class CartController extends GetxController {final items = <String>[].obs; // .obs로 반응형 변수 생성int get totalItems => items.length;void add(String item) {items.add(item);}void remove(String item) {items.remove(item);}}Controller 등록
void main() {Get.put(CartController()); // 의존성 주입runApp(MyApp());}상태 사용
class CartPage extends StatelessWidget {final CartController cart = Get.find(); // Controller 찾기@overrideWidget build(BuildContext context) {return Column(children: [Obx(() => Text('Total items: ${cart.totalItems}')), // 반응형 빌더Obx(() => ListView.builder(shrinkWrap: true,itemCount: cart.items.length,itemBuilder: (context, index) {return ListTile(title: Text(cart.items[index]));},)),],);}}상태 변경
class AddButton extends StatelessWidget {@overrideWidget build(BuildContext context) {return ElevatedButton(onPressed: () {Get.find<CartController>().add('New Item');},child: Text('Add Item'),);}}
Watch out
WARNINGGetX는 전문가들 사이에서 신규 프로젝트에 권장되지 않습니다.
// GetX의 문제점// 1. 유지보수자 1명 의존 - "버스 팩터" 위험// 2. Flutter SDK 업데이트 지연// 3. 전역 싱글톤 아키텍처 - 디버깅 어려움// 4. Controller 해제 불확실 - 메모리 누수 위험// 기존 GetX 프로젝트는 마이그레이션 계획 권장빠른 프로토타이핑에는 유용하지만, 프로덕션 앱에서는 다른 솔루션을 권장합니다.
결론: GetX는 빠른 개발에 유용하지만, 장기적인 프로젝트에서는 신중히 고려해야 합니다.
챕터 6: 라이브러리 선택 기준
Why
NOTE“최고의” 라이브러리는 없습니다.
프로젝트 상황, 팀 경험, 유지보수 요구사항에 따라 적합한 선택이 달라집니다.
What
NOTE선택 기준을 정리하면 다음과 같습니다.
상황 권장 라이브러리 이유 입문/학습 Provider 공식 권장, 낮은 학습 곡선 신규 프로젝트 Riverpod 컴파일 타임 안전성, 현대적 대규모/엔터프라이즈 BLoC 엄격한 구조, 이벤트 추적 프로토타입 GetX/setState 빠른 개발, 적은 설정 레거시 유지보수 기존 라이브러리 유지 마이그레이션 비용 고려
How
TIP보일러플레이트 비교
// 같은 카운터 기능 구현 시 코드량 비교// setState: ~20줄// Provider: ~30줄// Riverpod: ~25줄 (코드 생성 후)// BLoC: ~50줄+ (이벤트, 상태, BLoC 분리)// GetX: ~15줄결정 플로우차트
flowchart TD A[프로젝트 시작] --> B{팀 규모} B -->|1-3명| C{앱 복잡도} B -->|4명 이상| D[BLoC 또는 Riverpod] C -->|간단함| E[Provider 또는 setState] C -->|복잡함| F[Riverpod] D --> G{규제 산업?} G -->|예| H[BLoC] G -->|아니오| I[Riverpod]팀 경험 고려
// 팀에 React 경험자가 많으면// → Redux 패턴이 익숙할 수 있음// 팀에 Angular 경험자가 많으면// → RxDart와 BLoC이 익숙할 수 있음// Flutter 초보 팀이면// → Provider로 시작, 필요시 Riverpod으로 전환
Watch out
WARNING라이브러리를 혼합해서 사용하면 코드 일관성이 떨어집니다.
// 나쁜 예: 여러 라이브러리 혼용// 화면 A: Provider// 화면 B: GetX// 화면 C: BLoC// → 유지보수 어려움, 팀원 혼란// 좋은 예: 하나의 라이브러리로 통일// 전체 앱: Riverpod// → 일관된 패턴, 예측 가능한 코드한 번 선택하면 변경 비용이 크므로 신중하게 결정하세요.
결론: 팀 규모, 앱 복잡도, 유지보수 요구사항을 고려해 라이브러리를 선택합니다.
한계
상태 관리 라이브러리 선택에는 정답이 없습니다.
- 트레이드오프: 모든 라이브러리는 장단점이 있으며, 하나가 모든 상황에 완벽하지 않습니다.
- 마이그레이션 비용: 라이브러리 변경은 많은 시간과 노력이 필요합니다.
- 생태계 변화: Flutter 생태계는 빠르게 변하므로 현재 권장이 미래에도 유효하지 않을 수 있습니다.
- 팀 합의: 기술적 우수성보다 팀의 합의와 일관성이 더 중요할 수 있습니다.
Footnotes
공유
이 글이 도움이 되었다면 다른 사람과 공유해주세요!