InheritedWidget 심화 — BuildContext와 의존성 전파 원리
InheritedWidget 심화 — BuildContext와 의존성 전파 원리
InheritedWidget은 Flutter의 상태 관리 기반 기술입니다. Provider, Theme, MediaQuery 모두 InheritedWidget 위에 구현되어 있습니다. 동작 원리를 이해하면 상태 관리 라이브러리도 더 깊이 이해할 수 있습니다.
BuildContext란?
BuildContext는 사실 Element 의 추상 인터페이스입니다. 위젯 트리에서 현재 위젯의 위치 정보를 담고 있습니다.
@override
Widget build(BuildContext context) {
// context = 이 위젯의 Element
// context를 통해 트리 상위의 데이터에 접근 가능
// Theme 접근 → 내부적으로 InheritedWidget 조회
final theme = Theme.of(context);
// MediaQuery 접근
final size = MediaQuery.sizeOf(context);
return Text('화면 너비: ${size.width}');
}
면접 포인트: "BuildContext가 뭔가요?"라는 질문에 "위젯 트리에서 현재 위치를 나타내는 Element의 핸들"이라고 답할 수 있어야 합니다.
InheritedWidget 동작 원리
class CounterData extends InheritedWidget {
final int count;
final VoidCallback increment;
const CounterData({
super.key,
required this.count,
required this.increment,
required super.child,
});
// of 패턴: 하위 위젯에서 접근하는 정적 메서드
static CounterData of(BuildContext context) {
final result = context.dependOnInheritedWidgetOfExactType<CounterData>();
assert(result != null, 'CounterData를 찾을 수 없습니다');
return result!;
}
// 선택적: 의존성 없이 데이터만 읽기
static CounterData? maybeOf(BuildContext context) {
return context.dependOnInheritedWidgetOfExactType<CounterData>();
}
@override
bool updateShouldNotify(CounterData oldWidget) {
// true를 반환하면 의존하는 위젯들이 리빌드됨
return count != oldWidget.count;
}
}
전체 사용 예제
// 상태를 관리하는 StatefulWidget
class CounterProvider extends StatefulWidget {
final Widget child;
const CounterProvider({super.key, required this.child});
@override
State<CounterProvider> createState() => _CounterProviderState();
}
class _CounterProviderState extends State<CounterProvider> {
int _count = 0;
void _increment() {
setState(() => _count++);
}
@override
Widget build(BuildContext context) {
return CounterData(
count: _count,
increment: _increment,
child: widget.child,
);
}
}
// 하위 위젯에서 사용
class CounterDisplay extends StatelessWidget {
const CounterDisplay({super.key});
@override
Widget build(BuildContext context) {
final data = CounterData.of(context);
return Text('${data.count}');
}
}
class CounterButton extends StatelessWidget {
const CounterButton({super.key});
@override
Widget build(BuildContext context) {
final data = CounterData.of(context);
return ElevatedButton(
onPressed: data.increment,
child: const Text('증가'),
);
}
}
dependOn vs getElement
// dependOnInheritedWidgetOfExactType
// → 의존성 등록 + 데이터 반환
// → InheritedWidget이 변경되면 이 위젯도 리빌드
final data = context.dependOnInheritedWidgetOfExactType<CounterData>();
// getInheritedWidgetOfExactType (또는 getElementForInheritedWidgetOfExactType)
// → 데이터만 반환, 의존성 등록 안 함
// → InheritedWidget이 변경되어도 리빌드 안 됨
// → initState 등에서 사용 가능
이 차이가 Provider의 watch vs read의 원리입니다.
의존성 전파 흐름
CounterData (InheritedWidget)
├─ WidgetA (의존O → 리빌드됨)
│ └─ WidgetB (의존X → 리빌드 안 됨)
│ └─ WidgetC (의존O → 리빌드됨)
└─ WidgetD (의존X → 리빌드 안 됨)
dependOnInheritedWidgetOfExactType을 호출한 위젯만 리빌드됩니다. 중간 위젯은 리빌드되지 않습니다. 이것이 Prop Drilling 없이 데이터를 전파할 수 있는 이유입니다.
updateShouldNotify의 중요성
@override
bool updateShouldNotify(CounterData oldWidget) {
// false를 반환하면 의존 위젯들이 리빌드되지 않음
return count != oldWidget.count;
}
// 여러 필드가 있을 때
@override
bool updateShouldNotify(AppData oldWidget) {
return count != oldWidget.count ||
name != oldWidget.name ||
theme != oldWidget.theme;
}
InheritedModel — 세밀한 의존성 제어
InheritedWidget은 데이터가 변하면 모든 의존 위젯이 리빌드됩니다. InheritedModel을 사용하면 특정 부분(aspect)만 감시할 수 있습니다.
class AppModel extends InheritedModel<String> {
final int count;
final String theme;
const AppModel({
super.key,
required this.count,
required this.theme,
required super.child,
});
static AppModel of(BuildContext context, {String? aspect}) {
return InheritedModel.inheritFrom<AppModel>(context, aspect: aspect)!;
}
@override
bool updateShouldNotify(AppModel oldWidget) {
return count != oldWidget.count || theme != oldWidget.theme;
}
@override
bool updateShouldNotifyDependent(
AppModel oldWidget,
Set<String> dependencies,
) {
if (dependencies.contains('count') && count != oldWidget.count) {
return true;
}
if (dependencies.contains('theme') && theme != oldWidget.theme) {
return true;
}
return false;
}
}
// count만 감시 (theme 변경 시 리빌드 안 됨)
final model = AppModel.of(context, aspect: 'count');
of 패턴 관례
// Theme.of(context) → ThemeData
// MediaQuery.of(context) → MediaQueryData
// Navigator.of(context) → NavigatorState
// Scaffold.of(context) → ScaffoldState
// 내 InheritedWidget도 같은 패턴으로
static MyData of(BuildContext context) {
return context.dependOnInheritedWidgetOfExactType<MyData>()!;
}
// null 안전 버전
static MyData? maybeOf(BuildContext context) {
return context.dependOnInheritedWidgetOfExactType<MyData>();
}
정리
- BuildContext는 Element의 추상 인터페이스로, 트리에서 현재 위치를 나타냅니다
- InheritedWidget은
dependOn으로 의존성을 등록한 위젯만 선택적으로 리빌드합니다 updateShouldNotify에서 실제 변경 여부를 비교하여 불필요한 리빌드를 방지합니다- Provider의
watch/read는dependOn/getElement의 래퍼입니다 - InheritedModel로 특정 속성만 감시하는 세밀한 최적화가 가능합니다
댓글 로딩 중...