Flutter 상태관리: BLoC vs GetX 비교

1. 상태관리가 왜 필요한가?
Flutter는 선언형 UI 프레임워크입니다.
UI는 상태(State)의 함수로 표현되며, 상태가 바뀌면 위젯이 다시 빌드됩니다.
UI = f(State)
앱이 단순할 때는 setState()로 충분합니다. 하지만 다음과 같은 상황이 오면 한계에 부딪힙니다.
- 여러 위젯이 같은 데이터를 공유해야 할 때
- 비즈니스 로직이 UI 코드와 뒤섞일 때
- 비동기 처리(API 호출, 스트림 등)가 복잡해질 때
- 앱 규모가 커져 유지보수가 어려워질 때
이 문제를 해결하는 방법이 상태관리 패턴이며,
Flutter 생태계에서 가장 널리 쓰이는 두 가지 선택지가 BLoC과 GetX입니다.
2. BLoC 개요
BLoC(Business Logic Component) 은 Google이 2018년 Google I/O에서 소개한 패턴입니다.flutter_bloc 패키지로 구현하며, 이벤트 → BLoC → 상태 의 단방향 데이터 흐름을 강제합니다.
핵심 개념
| 개념 | 역할 |
|---|---|
Event |
사용자 액션 또는 외부 트리거 (입력) |
State |
UI가 렌더링할 데이터 (출력) |
Bloc |
이벤트를 받아 상태를 내보내는 비즈니스 로직 |
BlocBuilder |
상태 변화에 반응해 UI를 재빌드하는 위젯 |
BlocProvider |
BLoC 인스턴스를 위젯 트리에 제공하는 DI |
특징 요약
- 엄격한 단방향 흐름: 이벤트 → BLoC → 상태
- Flutter 공식 권장 패턴 중 하나
Stream기반으로 리액티브 프로그래밍 원칙에 충실Cubit을 이용한 경량 버전도 제공 (이벤트 없이 메서드 직접 호출)
3. GetX 개요
GetX는 커뮤니티 주도의 올인원 Flutter 패키지입니다.
상태관리 외에 라우팅, 의존성 주입, 유틸리티까지 하나의 패키지에 담겨 있습니다.
핵심 개념
| 개념 | 역할 |
|---|---|
GetxController |
상태와 비즈니스 로직을 보유하는 컨트롤러 |
.obs |
관찰 가능한(Observable) 변수 선언 |
Obx / GetX |
반응형 상태 변화에 맞춰 UI를 재빌드 |
Get.put() / Get.find() |
의존성 주입 및 검색 |
Get.to() / Get.back() |
BuildContext 없는 내비게이션 |
특징 요약
- 최소한의 보일러플레이트 — 빠른 개발 가능
- 상태관리 + 라우팅 + DI의 올인원 솔루션
BuildContext없이 어디서든 내비게이션/다이얼로그 호출 가능- 반응형(
Rx) + 단순 상태관리(GetBuilder) 두 가지 방식 지원
4. 코드 비교: 카운터 앱 예제
가장 단순한 예제인 카운터 앱으로 두 방식의 코드를 비교합니다.
BLoC 방식
BlocProvider와 BlocBuilder를 조합해서 상태를 UI에 반영합니다.
1. 이벤트 정의
// counter_event.dart
abstract class CounterEvent {}
class IncrementEvent extends CounterEvent {}
class DecrementEvent extends CounterEvent {}
2. 상태 정의
// counter_state.dart
class CounterState {
final int count;
const CounterState(this.count);
}
3. BLoC 구현
// counter_bloc.dart
import 'package:flutter_bloc/flutter_bloc.dart';
class CounterBloc extends Bloc<CounterEvent, CounterState> {
CounterBloc() : super(const CounterState(0)) {
on<IncrementEvent>((event, emit) {
emit(CounterState(state.count + 1));
});
on<DecrementEvent>((event, emit) {
emit(CounterState(state.count - 1));
});
}
}
4. UI
// counter_page.dart
BlocProvider(
create: (_) => CounterBloc(),
child: BlocBuilder<CounterBloc, CounterState>(
builder: (context, state) {
return Column(
children: [
Text('${state.count}'),
ElevatedButton(
onPressed: () => context.read<CounterBloc>().add(IncrementEvent()),
child: const Text('+'),
),
],
);
},
),
)
GetX 방식
Get.put으로 컨트롤러를 등록하고, Obx로 상태 변화를 감지합니다.
1. 컨트롤러 구현
// counter_controller.dart
import 'package:get/get.dart';
class CounterController extends GetxController {
var count = 0.obs; // Observable 변수
void increment() => count++;
void decrement() => count--;
}
2. UI
// counter_page.dart
final controller = Get.put(CounterController());
Obx(() => Column(
children: [
Text('${controller.count}'),
ElevatedButton(
onPressed: controller.increment,
child: const Text('+'),
),
],
))
코드량 비교: BLoC은 4개 파일, GetX는 2개 파일. 보일러플레이트 차이가 명확합니다.
5. 아키텍처 & 구조
BLoC의 아키텍처
View (Widget)
│
│ add(Event)
▼
Bloc
│
│ emit(State)
▼
View (BlocBuilder 리빌드)
BLoC은 이벤트와 상태를 명시적으로 분리합니다.
모든 사용자 액션은 Event로 표현되고, UI는 State만 렌더링합니다.
이 구조 덕분에:
- 비즈니스 로직이 어디에 있는지 항상 명확합니다
- 상태 변화 추적이 용이해 디버깅이 쉽습니다
- 팀 규모가 커져도 일관된 코드 스타일을 유지할 수 있습니다
GetX의 아키텍처
View (Obx Widget)
│
│ controller.method()
▼
GetxController
│
│ .obs 변수 변경
▼
View (자동 리빌드)
GetX는 반응형 변수가 변경되면 이를 구독하는 Obx 위젯만 선택적으로 리빌드합니다.
구조적 제약이 적어 빠르지만, 팀에서 컨벤션을 명시적으로 정하지 않으면 로직이 분산될 수 있습니다.
6. 생산성
- 프로토타이핑 / 소규모 프로젝트: GetX가 정말 빠릅니다
- 중대형 프로젝트 / 팀 개발: BLoC의 엄격한 구조가 장기적으로 유리합니다
7. 성능
두 라이브러리 모두 필요한 위젯만 리빌드하는 최적화를 지원하며, 일반적인 앱에서 체감 성능 차이는 거의 없습니다.
| 항목 | BLoC | GetX |
|---|---|---|
| 리빌드 범위 | BlocBuilder의 buildWhen으로 제어 |
Obx가 사용한 .obs 변수만 구독 |
| 메모리 관리 | BlocProvider 스코프로 자동 해제 |
Get.delete() 또는 onClose() 명시 필요 |
| 패키지 크기 | 작음 | 큰 편 (라우팅, DI 등 포함) |
주의: GetX는
Get.put()으로 등록된 컨트롤러가 자동으로 해제되지 않는 경우가 있습니다. 메모리 누수 방지를 위해onClose()오버라이딩을 습관화해야 합니다.
8. 테스트 용이성
BLoC 테스트
bloc_test 패키지를 이용해 이벤트-상태 흐름을 단위 테스트할 수 있습니다.
구조가 명확하기 때문에 테스트 작성이 직관적입니다.
blocTest<CounterBloc, CounterState>(
'IncrementEvent 발생 시 count가 1 증가해야 함',
build: () => CounterBloc(),
act: (bloc) => bloc.add(IncrementEvent()),
expect: () => [const CounterState(1)],
);
GetX 테스트
GetX는 테스트 환경에서 Get 싱글턴이 전역 상태를 공유하는 특성 때문에 테스트 간 격리가 까다로울 수 있습니다.
각 테스트 전후로 Get.reset()을 호출해야 합니다.
setUp(() => Get.put(CounterController()));
tearDown(() => Get.reset());
test('increment 호출 시 count가 1 증가해야 함', () {
final controller = Get.find<CounterController>();
controller.increment();
expect(controller.count.value, 1);
});
테스트 용이성: BLoC > GetX
9. 부가 기능 비교
GetX는 상태관리 외에도 다양한 기능을 제공합니다.
| 기능 | BLoC | GetX |
|---|---|---|
| 상태관리 | ✅ | ✅ |
| 라우팅 | ❌ (별도 패키지 필요) | ✅ 내장 |
| 의존성 주입 | ✅ (get_it 등과 조합) | ✅ 내장 |
| 국제화(i18n) | ❌ | ✅ 내장 |
| 스낵바/다이얼로그 | ❌ | ✅ (Get.snackbar() 등) |
BuildContext 없는 내비게이션 |
❌ | ✅ |
10. 언제 무엇을 써야 하나?
BLoC을 선택해야 할 때
- 중대형 팀 프로젝트 — 코드 스타일을 강제할 수 있어 일관성 확보
- 엄격한 아키텍처가 필요한 경우 — Clean Architecture와의 궁합이 좋음
- 테스트 커버리지가 중요한 경우 — 비즈니스 로직 단위 테스트가 용이
- Flutter 공식 패턴을 따르고 싶을 때
- 복잡한 상태 전환 로직이 많은 경우
GetX를 선택해야 할 때
- 1인 개발 또는 소규모 팀
- 빠른 프로토타이핑 — MVP나 데모 앱
- 라우팅/DI/상태관리를 단일 패키지로 해결하고 싶을 때
- Flutter 입문자 — 낮은 진입장벽으로 전체 흐름 파악에 유리
BuildContext없이 어디서든 내비게이션이 필요한 경우
11. 결론
| BLoC | GetX | |
|---|---|---|
| 보일러플레이트 | 많음 | 적음 |
| 아키텍처 엄격성 | 높음 | 낮음 |
| 학습 곡선 | 가파름 | 완만함 |
| 테스트 | 용이 | 까다로움 |
| 기능 범위 | 상태관리 전문 | 올인원 |
| 대규모 팀 | 적합 | 주의 필요 |
"BLoC은 규율, GetX는 자유"
BLoC은 처음에는 번거롭지만, 팀이 커지고 앱이 복잡해질수록 그 가치를 발휘합니다.
GetX는 혼자 빠르게 만들어야 할 때 강력한 무기가 됩니다.