Flutter에서 InheritedWidget이란 무엇인가요?
질문
Flutter에서 InheritedWidget이란 무엇이며 어떤 용도로 사용되나요?
답변
InheritedWidget은 Flutter에서 위젯 트리를 통해 데이터를 효율적으로 전달하는 위젯 유형입니다. 위젯 트리의 하위 항목들이 상위 항목의 데이터에 액세스할 수 있게 해주는 매커니즘을 제공합니다. Provider, Riverpod, BLoC 패턴과 같은 대부분의 Flutter 상태 관리 솔루션은 내부적으로 InheritedWidget을 기반으로 구축되어 있습니다.
InheritedWidget의 핵심 개념
위젯 트리를 통한 데이터 전파: 데이터를 자식 위젯에게 명시적으로 전달하지 않고도 위젯 트리를 통해 깊은 수준의 자식 위젯까지 데이터를 제공할 수 있습니다.
자동 리빌드 메커니즘: 하위 위젯이 InheritedWidget에 의존할 때, InheritedWidget이 업데이트되면 의존하는 모든 하위 위젯이 자동으로 다시 빌드됩니다.
의존성 최적화: 실제로 의존하는 하위 위젯만 다시 빌드되므로 성능이 최적화됩니다.
InheritedWidget의 기본 구현
간단한 InheritedWidget 구현은 다음과 같습니다:
class MyInheritedData extends InheritedWidget {
// 공유할 데이터
final int data;
// 생성자
const MyInheritedData({
Key? key,
required this.data,
required Widget child,
}) : super(key: key, child: child);
// 데이터에 접근하기 위한 편의 메서드
static MyInheritedData of(BuildContext context) {
final MyInheritedData? result =
context.dependOnInheritedWidgetOfExactType<MyInheritedData>();
assert(result != null, 'MyInheritedData를 찾을 수 없습니다.');
return result!;
}
// 위젯이 다시 빌드되어야 하는지 결정하는 메서드
@override
bool updateShouldNotify(MyInheritedData oldWidget) {
return data != oldWidget.data;
}
}
InheritedWidget 사용 예시
// InheritedWidget을 위젯 트리에 배치
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
home: MyInheritedData(
data: 42,
child: HomeScreen(),
),
);
}
}
// 하위 위젯에서 데이터 사용하기
class HomeScreen extends StatelessWidget {
@override
Widget build(BuildContext context) {
// InheritedWidget의 데이터에 접근
final myData = MyInheritedData.of(context).data;
return Scaffold(
appBar: AppBar(title: Text('InheritedWidget 예제')),
body: Center(
child: Text('데이터: $myData'),
),
);
}
}
데이터를 변경할 수 있는 InheritedWidget
InheritedWidget 자체는 불변(immutable)이므로 직접 데이터를 변경할 수 없습니다. 데이터를 변경하고 싶다면 일반적으로 StatefulWidget과 함께 사용합니다:
class InheritedDataProvider extends StatefulWidget {
final Widget child;
InheritedDataProvider({required this.child});
@override
_InheritedDataProviderState createState() => _InheritedDataProviderState();
// 접근을 위한 정적 메서드
static _InheritedDataProviderState of(BuildContext context) {
return context.findAncestorStateOfType<_InheritedDataProviderState>()!;
}
}
class _InheritedDataProviderState extends State<InheritedDataProvider> {
int data = 0;
void updateData(int newData) {
setState(() {
data = newData;
});
}
@override
Widget build(BuildContext context) {
return _InheritedData(
data: data,
child: widget.child,
);
}
}
class _InheritedData extends InheritedWidget {
final int data;
_InheritedData({
required this.data,
required Widget child,
}) : super(child: child);
@override
bool updateShouldNotify(_InheritedData oldWidget) {
return data != oldWidget.data;
}
static _InheritedData of(BuildContext context) {
return context.dependOnInheritedWidgetOfExactType<_InheritedData>()!;
}
}
// 사용 예시
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
home: InheritedDataProvider(
child: DataScreen(),
),
);
}
}
class DataScreen extends StatelessWidget {
@override
Widget build(BuildContext context) {
final data = _InheritedData.of(context).data;
return Scaffold(
appBar: AppBar(title: Text('변경 가능한 데이터')),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text('데이터: $data'),
ElevatedButton(
onPressed: () {
// 데이터 업데이트
InheritedDataProvider.of(context).updateData(data + 1);
},
child: Text('증가'),
),
],
),
),
);
}
}
InheritedWidget의 두 가지 액세스 방법
InheritedWidget의 데이터에 접근하는 방법에는 두 가지가 있습니다:
- dependOnInheritedWidgetOfExactType: 위젯이 InheritedWidget에 의존하게 만들고, InheritedWidget이 변경되면 자동으로 다시 빌드됩니다.
final data = context.dependOnInheritedWidgetOfExactType<MyInheritedData>()!.data;
// 또는 일반적으로 다음과 같은 편의 메서드를 통해
final data = MyInheritedData.of(context).data;
- getInheritedWidgetOfExactType: 의존성 없이 데이터에 접근합니다. InheritedWidget이 변경되더라도 위젯이 자동으로 다시 빌드되지 않습니다.
final data = context.getInheritedWidgetOfExactType<MyInheritedData>()?.data;
실제 활용 사례: 테마 관리
InheritedWidget은 앱 전체에서 테마 데이터를 공유하는 데 매우 유용합니다:
class ThemeProvider extends StatefulWidget {
final Widget child;
final ThemeData initialTheme;
ThemeProvider({
required this.child,
required this.initialTheme,
});
@override
_ThemeProviderState createState() => _ThemeProviderState();
static _ThemeProviderState of(BuildContext context) {
return context.findAncestorStateOfType<_ThemeProviderState>()!;
}
}
class _ThemeProviderState extends State<ThemeProvider> {
late ThemeData _themeData;
@override
void initState() {
super.initState();
_themeData = widget.initialTheme;
}
void setTheme(ThemeData theme) {
setState(() {
_themeData = theme;
});
}
void toggleTheme() {
setState(() {
_themeData = _themeData.brightness == Brightness.dark
? ThemeData.light()
: ThemeData.dark();
});
}
@override
Widget build(BuildContext context) {
return _InheritedTheme(
themeData: _themeData,
child: widget.child,
);
}
}
class _InheritedTheme extends InheritedWidget {
final ThemeData themeData;
_InheritedTheme({
required this.themeData,
required Widget child,
}) : super(child: child);
@override
bool updateShouldNotify(_InheritedTheme oldWidget) {
return themeData != oldWidget.themeData;
}
static _InheritedTheme of(BuildContext context) {
return context.dependOnInheritedWidgetOfExactType<_InheritedTheme>()!;
}
}
// 사용 예시
void main() {
runApp(
ThemeProvider(
initialTheme: ThemeData.light(),
child: MyApp(),
),
);
}
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
// 테마 데이터 접근
final theme = _InheritedTheme.of(context).themeData;
return MaterialApp(
theme: theme,
home: HomeScreen(),
);
}
}
class HomeScreen extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('테마 예제')),
body: Center(
child: ElevatedButton(
onPressed: () {
// 테마 토글
ThemeProvider.of(context).toggleTheme();
},
child: Text('테마 전환'),
),
),
);
}
}
Provider와의 관계
Flutter 커뮤니티에서 널리 사용되는 Provider 패키지는 InheritedWidget을 기반으로 구축되었으며, 사용하기 더 쉽게 만든 추상화 계층입니다:
// Provider 사용 예시 (간단함을 위해)
void main() {
runApp(
ChangeNotifierProvider(
create: (context) => ThemeModel(),
child: MyApp(),
),
);
}
class ThemeModel extends ChangeNotifier {
ThemeData _themeData = ThemeData.light();
ThemeData get themeData => _themeData;
void toggleTheme() {
_themeData = _themeData.brightness == Brightness.dark
? ThemeData.light()
: ThemeData.dark();
notifyListeners();
}
}
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
final themeModel = Provider.of<ThemeModel>(context);
return MaterialApp(
theme: themeModel.themeData,
home: HomeScreen(),
);
}
}
InheritedWidget의 장점
위젯 트리를 통한 효율적인 데이터 전달: 소품 드릴링(prop drilling)이라고 하는 중간 위젯을 통해 데이터를 전달할 필요가 없습니다.
자동 리빌드 최적화: 데이터가 변경될 때 의존하는 위젯만 다시 빌드됩니다.
관심사 분리: 데이터 관리 로직과 UI 로직을 분리할 수 있습니다.
테스트 용이성: 앱의 다른 부분과 분리하여 데이터 로직을 테스트할 수 있습니다.
InheritedWidget의 한계
변경 불가능성: InheritedWidget 자체는 불변이므로 직접 데이터를 변경할 수 없습니다(StatefulWidget과 결합하여 해결).
복잡성: 직접 구현하기에는 다소 복잡할 수 있습니다(Provider 같은 패키지를 사용하여 해결).
컨텍스트 의존성: 데이터에 접근하려면 BuildContext가 필요합니다.
실제 예: MediaQuery, Theme 등 Flutter의 내장 InheritedWidget
Flutter 프레임워크 자체는 여러 InheritedWidget을 제공합니다:
// 화면 크기 정보 접근
MediaQueryData mediaQuery = MediaQuery.of(context);
double screenWidth = mediaQuery.size.width;
// 테마 접근
ThemeData theme = Theme.of(context);
Color primaryColor = theme.primaryColor;
// 지역화 접근
Locale locale = Localizations.localeOf(context);
결론
InheritedWidget은 Flutter에서 위젯 트리를 통해 데이터를 효율적으로 공유하는 강력한 메커니즘입니다. 상태 관리의 기본 구성 요소로서, 많은 고급 상태 관리 솔루션의 기반이 됩니다. 직접 구현하는 것이 복잡할 수 있지만, 그 개념을 이해하면 Provider나 Riverpod와 같은 상위 수준 패키지를 더 효과적으로 사용할 수 있습니다.
Flutter 개발자로서 InheritedWidget의 작동 방식과 사용법을 이해하는 것은 효율적인 상태 관리 패턴을 구현하고 앱의 성능을 최적화하는 데 큰 도움이 됩니다.