상태 관리 입문 — setState의 한계와 Provider 소개
상태 관리 입문 — setState의 한계와 Provider 소개
Flutter 면접에서 가장 많이 나오는 주제 중 하나가 상태 관리입니다. setState부터 시작해서 왜 상태 관리 라이브러리가 필요한지, Provider는 어떤 문제를 해결하는지 정리해보겠습니다.
상태(State)란?
앱에서 변할 수 있는 모든 데이터를 상태라고 합니다.
| 종류 | 예시 | 관리 위치 |
|---|---|---|
| Ephemeral State (로컬) | 현재 탭 인덱스, 애니메이션 진행률 | 해당 위젯 내부 |
| App State (전역) | 로그인 정보, 장바구니, 설정 | 앱 전체에서 공유 |
면접 포인트: "Ephemeral state와 App state의 차이를 설명해주세요"라는 질문이 자주 나옵니다.
setState의 한계
문제 1: 상태 끌어올리기 (Lifting State Up)
자식 위젯에서 상태를 변경하려면, 상태를 공통 조상으로 끌어올려야 합니다.
// 상태를 최상위로 끌어올려야 하는 구조
class ParentWidget extends StatefulWidget {
@override
State<ParentWidget> createState() => _ParentWidgetState();
}
class _ParentWidgetState extends State<ParentWidget> {
int _count = 0;
void _increment() {
setState(() => _count++);
}
@override
Widget build(BuildContext context) {
return Column(
children: [
// 상태를 표시하는 위젯
DisplayWidget(count: _count),
// 상태를 변경하는 위젯 — 콜백을 전달해야 함
ButtonWidget(onPressed: _increment),
],
);
}
}
문제 2: Prop Drilling
깊은 위젯 트리에서 상태를 전달하려면 중간 위젯들이 모두 해당 데이터를 전달해야 합니다.
App
└─ HomePage (user 전달)
└─ Content (user 전달, 사용 안 함)
└─ Sidebar (user 전달, 사용 안 함)
└─ ProfileCard (user 실제 사용)
중간 위젯들은 user 데이터를 사용하지 않지만 전달만 해야 합니다. 위젯 트리가 깊어질수록 이 문제가 심각해집니다.
문제 3: 불필요한 리빌드
// setState를 호출하면 해당 위젯의 build() 전체가 다시 실행됨
// 변경된 부분만 리빌드하고 싶어도 전체가 리빌드됨
setState(() {
_count++; // count만 바꿨는데 전체 build가 다시 실행
});
InheritedWidget — Flutter의 기본 해결책
Flutter에 내장된 상태 전파 메커니즘입니다. Provider의 기반 기술이기도 합니다.
class CounterInherited extends InheritedWidget {
final int count;
final VoidCallback increment;
const CounterInherited({
super.key,
required this.count,
required this.increment,
required super.child,
});
// 하위 위젯에서 접근하는 헬퍼 메서드
static CounterInherited of(BuildContext context) {
return context.dependOnInheritedWidgetOfExactType<CounterInherited>()!;
}
@override
bool updateShouldNotify(CounterInherited oldWidget) {
return count != oldWidget.count;
}
}
InheritedWidget은 보일러플레이트가 많고 사용하기 번거롭습니다. 이를 쉽게 만든 것이 Provider입니다.
Provider 소개
Provider는 InheritedWidget을 감싼 래퍼로, 상태 관리를 간편하게 만들어줍니다.
# pubspec.yaml
dependencies:
provider: ^6.1.0
기본 사용법
// 1. 상태 클래스 정의
class CounterModel extends ChangeNotifier {
int _count = 0;
int get count => _count;
void increment() {
_count++;
notifyListeners(); // 변경 알림
}
void decrement() {
_count--;
notifyListeners();
}
}
// 2. Provider로 상태 제공
void main() {
runApp(
ChangeNotifierProvider(
create: (context) => CounterModel(),
child: const MyApp(),
),
);
}
// 3. 하위 위젯에서 상태 사용
class CounterScreen extends StatelessWidget {
const CounterScreen({super.key});
@override
Widget build(BuildContext context) {
// 상태 읽기 (변경 시 리빌드)
final count = context.watch<CounterModel>().count;
return Scaffold(
body: Center(
child: Text('$count', style: const TextStyle(fontSize: 48)),
),
floatingActionButton: FloatingActionButton(
// 상태 변경 (리빌드 없이 접근)
onPressed: () => context.read<CounterModel>().increment(),
child: const Icon(Icons.add),
),
);
}
}
watch vs read
| 메서드 | 용도 | 리빌드 |
|---|---|---|
context.watch<T>() | 상태 읽기 + 변경 감지 | 변경 시 리빌드 |
context.read<T>() | 상태 읽기 (1회) | 리빌드 안 함 |
context.select<T, R>() | 특정 값만 감시 | 해당 값 변경 시만 리빌드 |
// build 메서드 안에서 상태를 읽을 때 → watch
final count = context.watch<CounterModel>().count;
// 콜백(onPressed 등)에서 메서드를 호출할 때 → read
onPressed: () => context.read<CounterModel>().increment(),
// 특정 속성만 감시할 때 → select
final count = context.select<CounterModel, int>((m) => m.count);
면접 포인트: watch는 build 안에서, read는 콜백 안에서 사용합니다. 이 규칙을 지키지 않으면 불필요한 리빌드가 발생하거나 상태 변경을 감지하지 못합니다.
여러 Provider 제공하기
void main() {
runApp(
MultiProvider(
providers: [
ChangeNotifierProvider(create: (_) => AuthModel()),
ChangeNotifierProvider(create: (_) => CartModel()),
ChangeNotifierProvider(create: (_) => ThemeModel()),
],
child: const MyApp(),
),
);
}
상태 관리 라이브러리 비교
| 라이브러리 | 특징 | 난이도 |
|---|---|---|
| setState | 내장, 로컬 상태용 | 쉬움 |
| Provider | InheritedWidget 래퍼, 가장 기본적 | 쉬움 |
| Riverpod | Provider의 진화, 컴파일타임 안전 | 중간 |
| Bloc | 이벤트 기반, 엔터프라이즈 | 중간~어려움 |
| GetX | 간편하지만 규모가 커지면 관리 어려움 | 쉬움 |
정리
setState는 로컬 상태에만 적합합니다- Prop Drilling, 불필요한 리빌드가 setState의 주요 한계입니다
- Provider는 InheritedWidget을 쉽게 사용할 수 있게 한 래퍼입니다
watch는 build에서,read는 콜백에서 사용하세요- 프로젝트 규모와 팀 상황에 맞는 상태 관리 라이브러리를 선택하세요
댓글 로딩 중...