Flutter 튜토리얼 12편: 상태 관리 라이브러리 비교

요약#

핵심 요지#

  • 문제 정의: Flutter에는 다양한 상태 관리 라이브러리가 있어서 어떤 것을 선택해야 할지 혼란스럽다.
  • 핵심 주장: 팀 규모, 앱 복잡도, 유지보수 요구사항에 따라 적합한 라이브러리가 다르다.
  • 주요 근거: Provider1는 입문용으로 적합하고, Riverpod2은 신규 프로젝트의 표준이 되고 있으며, BLoC3은 대규모 엔터프라이즈에 적합하다.
  • 실무 기준: 소규모 앱은 Provider나 setState, 중대규모 앱은 Riverpod, 엔터프라이즈는 BLoC을 권장한다.
  • 한계: 완벽한 라이브러리는 없으며, 팀의 경험과 프로젝트 요구사항에 따라 선택해야 한다.

문서가 설명하는 범위#

  • Flutter의 기본 제공 상태 관리 방식
  • 주요 상태 관리 라이브러리 비교 (Provider, Riverpod, BLoC, GetX)
  • 라이브러리별 코드 예시
  • 프로젝트 상황에 따른 선택 기준

읽는 시간: 18분 | 난이도: 중급


참고 자료#


문제 상황#

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#

NOTE

Flutter는 세 가지 기본 상태 관리 도구를 제공합니다.

도구용도복잡도
setState단일 위젯 내부 상태낮음
InheritedWidget위젯 트리에 데이터 공유중간
ValueNotifier단일 값 변경 감지낮음

How#

TIP

setState - 가장 기본적인 방식

class Counter extends StatefulWidget {
@override
State<Counter> createState() => _CounterState();
}
class _CounterState extends State<Counter> {
int _count = 0;
@override
Widget build(BuildContext context) {
return Column(
children: [
Text('Count: $_count'),
ElevatedButton(
onPressed: () {
setState(() {
_count++;
});
},
child: Text('Increment'),
),
],
);
}
}

ValueNotifier와 ValueListenableBuilder

class CounterWithNotifier extends StatefulWidget {
@override
State<CounterWithNotifier> createState() => _CounterWithNotifierState();
}
class _CounterWithNotifierState extends State<CounterWithNotifier> {
final ValueNotifier<int> _counter = ValueNotifier(0);
@override
void dispose() {
_counter.dispose();
super.dispose();
}
@override
Widget 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>()!;
}
@override
bool 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#

NOTE

Provider는 Flutter 팀이 공식 권장하는 상태 관리 패키지입니다.
간단한 API로 위젯 트리에 데이터를 제공하고 변경을 감지합니다.

특징설명
장점배우기 쉬움, 공식 권장, 안정적
단점런타임 타입 체크, 복잡한 의존성 처리 어려움
적합한 상황입문자, 소규모~중규모 앱

How#

TIP

설치

pubspec.yaml
dependencies:
provider: ^6.1.0

ChangeNotifier 정의

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 {
@override
Widget 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 {
@override
Widget build(BuildContext context) {
return ElevatedButton(
onPressed: () {
// listen: false는 변경을 구독하지 않음
context.read<CartModel>().add('New Item');
},
child: Text('Add Item'),
);
}
}

Watch out#

WARNING

Provider는 런타임에 타입을 체크하므로 컴파일 타임에 오류를 잡지 못합니다.

// Provider가 없으면 런타임에 오류 발생
class SomePage extends StatelessWidget {
@override
Widget build(BuildContext context) {
// CartModel Provider가 상위에 없으면?
final cart = context.read<CartModel>(); // 런타임 오류!
return Text('Items: ${cart.totalItems}');
}
}

BuildContext에 의존하므로 위젯 밖에서 상태에 접근하기 어렵습니다.

결론: Provider는 입문자에게 적합하고, 소규모 앱에서 충분히 잘 동작합니다.


챕터 3: Riverpod 이해하기#

Why#

NOTE

Provider의 창시자가 Provider의 한계를 해결하기 위해 만든 패키지입니다.
BuildContext 없이도 상태에 접근할 수 있고, 컴파일 타임 안전성을 제공합니다.

// Provider: 런타임 오류
context.read<CartModel>(); // Provider 없으면 런타임 에러
// Riverpod: 컴파일 타임 체크
ref.watch(cartProvider); // Provider 없으면 컴파일 에러

What#

NOTE

Riverpod은 현재 Flutter 신규 프로젝트의 표준으로 자리잡고 있습니다.
컴파일 타임 안전성, 자동 캐싱, 코드 생성 지원이 핵심 특징입니다.

특징설명
장점컴파일 타임 안전성, 테스트 용이, 자동 캐싱
단점Provider보다 학습 곡선 있음, 기존 코드 마이그레이션 필요
적합한 상황신규 프로젝트, 중대규모 앱, 테스트 중시

How#

TIP

설치

pubspec.yaml
dependencies:
flutter_riverpod: ^2.5.0
riverpod_annotation: ^2.3.0
dev_dependencies:
riverpod_generator: ^2.4.0
build_runner: ^2.4.0

Provider 정의 (코드 생성 방식)

import 'package:riverpod_annotation/riverpod_annotation.dart';
part 'cart_provider.g.dart';
@riverpod
class Cart extends _$Cart {
@override
List<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 {
@override
Widget 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 {
@override
Widget build(BuildContext context, WidgetRef ref) {
return ElevatedButton(
onPressed: () {
ref.read(cartProvider.notifier).add('New Item');
},
child: Text('Add Item'),
);
}
}

Watch out#

WARNING

Riverpod은 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#

NOTE

BLoC(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 {
@override
Widget 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 {
@override
Widget build(BuildContext context) {
return ElevatedButton(
onPressed: () {
context.read<CartBloc>().add(AddItem('New Item'));
},
child: Text('Add Item'),
);
}
}

Watch out#

WARNING

BLoC은 간단한 기능에도 많은 파일과 코드가 필요합니다.

// 카운터 하나를 위해 필요한 파일들
// 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#

NOTE

GetX는 상태 관리, 라우팅, 의존성 주입을 하나의 패키지에 제공합니다.
빠른 개발과 적은 보일러플레이트를 목표로 합니다.

// GetX는 "올인원" 솔루션
// 상태 관리 + 라우팅 + 의존성 주입 + 유틸리티

What#

NOTE

GetX는 간편한 API로 빠른 개발을 가능하게 하지만, 주의가 필요합니다.
최근 유지보수 지연과 Flutter SDK 호환성 문제가 보고되고 있습니다.

특징설명
장점적은 보일러플레이트, 올인원 솔루션, 배우기 쉬움
단점유지보수 지연, 전역 상태 디버깅 어려움, 메모리 누수 위험
적합한 상황프로토타입, 소규모 개인 프로젝트

How#

TIP

설치

pubspec.yaml
dependencies:
get: ^4.6.6

Controller 정의

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 찾기
@override
Widget 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 {
@override
Widget build(BuildContext context) {
return ElevatedButton(
onPressed: () {
Get.find<CartController>().add('New Item');
},
child: Text('Add Item'),
);
}
}

Watch out#

WARNING

GetX는 전문가들 사이에서 신규 프로젝트에 권장되지 않습니다.

// 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#

  1. Provider: Flutter 팀이 공식 권장하는 상태 관리 패키지로, InheritedWidget을 감싸서 사용하기 쉽게 만든 것이다.

  2. Riverpod: Provider의 창시자가 만든 차세대 상태 관리 패키지로, 컴파일 타임 안전성과 BuildContext 독립성이 특징이다.

  3. BLoC(Business Logic Component): 이벤트 기반 아키텍처로 UI와 비즈니스 로직을 분리하는 패턴이다.

공유

이 글이 도움이 되었다면 다른 사람과 공유해주세요!

Flutter 튜토리얼 12편: 상태 관리 라이브러리 비교
https://moodturnpost.net/posts/flutter/flutter-state-options/
작성자
Moodturn
게시일
2026-01-08
Moodturn

목차