[Flutter]Inherited Widget을 파헤치며

한상욱·2024년 8월 29일
0

Flutter

목록 보기
20/26
post-thumbnail

들어가며

Flutter에는 Inherited Widget이라는 것이 존재합니다. 오늘은 Inherited Widget에 대해서 알아보겠습니다.

Inherited Widget이 등장한 이유

Flutter의 위젯 트리에서 가장 하단의 위젯에서 부모 위젯의 상태가 필요한 경우 트리 중간에 있는 모든 위젯에 데이터를 전달하며 내려가게 됩니다.

상태가 필요한 제일 하단의 자식 위젯을 위해서 모든 위젯은 상태를 전달받게 되므로 이는 곧 코드의 리팩토링을 어렵게할 수 있습니다. 그렇다고 부모 위젯의 하위 위젯들을 모두 같은 build 메소드로 묶게 된다면, 전체 위젯이 리빌드되기 때문에 성능저하가 발생할 수도 있을 것입니다. 어떻게 이를 해결할 수 있을까요?

이를 해결하기 위해서 등장한 것이 Inherited Widget입니다.

MediaQuery를 파헤치며

Inherited Widget은 자식 트리에 존재하는 모든 위젯들이 Inherited Widget의 상태를 가져올 수 있다는 특징을 갖고 있습니다. 이를 위해서는 BuildContext에 대한 이해가 필요합니다.

BuildContext의 가장 기본적인 개념은 링크를 통해 확인할 수 있습니다.
[Flutter]BuildContext에 대해서

어찌되었든, BuildContext는 위젯 트리에서의 현재 위치를 나타내게 됩니다. 이 개념을 Inherited Widget이 사용하게 됩니다.

여러분들은 개발을 하다가 아래와 같이 사용되는 코드를 종종 보셨을 겁니다.

Navaigator.of(context) ...
Theme.of(context) ...

위 코드에서는 of 메소드에 context를 전달하는 방식으로 사용됩니다. MediaQuery의 of 메소드의 소스 코드를 한번 찾아가보죠.

  static MediaQueryData of(BuildContext context) {
    return _of(context);
  }

  static MediaQueryData _of(BuildContext context, [_MediaQueryAspect? aspect]) {
    assert(debugCheckHasMediaQuery(context));
    return InheritedModel.inheritFrom<MediaQuery>(context, aspect: aspect)!.data;
  }

음, 버전이 올라가며 변경되었는지 몰라도 소스코드가 살짝 다른 것 같지만, 좀 더 내부의 inheritedFrom이라는 메소드를 찾아가 봅시다.

  static T? inheritFrom<T extends InheritedModel<Object>>(BuildContext context, { Object? aspect }) {
    if (aspect == null) {
      return context.dependOnInheritedWidgetOfExactType<T>();
    }

    // Create a dependency on all of the type T ancestor models up until
    // a model is found for which isSupportedAspect(aspect) is true.
    final List<InheritedElement> models = <InheritedElement>[];
    _findModels<T>(context, aspect, models);
    if (models.isEmpty) {
      return null;
    }

    final InheritedElement lastModel = models.last;
    for (final InheritedElement model in models) {
      final T value = context.dependOnInheritedElement(model, aspect: aspect) as T;
      if (model == lastModel) {
        return value;
      }
    }

    assert(false);
    return null;
  }

이 코드에서

return context.dependOnInheritedWidgetOfExactType<T>();

를 주목해봅시다. 왜냐하면 aspect는 선택적으로 전달받기 때문에, aspect는 null이겠죠. 그렇기에 가장 상단인 dependOnInheritedWidgetOfExactType<T>를 반환합니다. T는 여기서 InheriteModel이 됩니다. 그리고 InheritedModel은

abstract class InheritedModel<T> extends InheritedWidget {
  /// Creates an inherited widget that supports dependencies qualified by
  /// "aspects", i.e. a descendant widget can indicate that it should
  /// only be rebuilt if a specific aspect of the model changes.
  const InheritedModel({ super.key, required super.child });

  
  InheritedModelElement<T> createElement() => InheritedModelElement<T>(this);

  /// Return true if the changes between this model and [oldWidget] match any
  /// of the [dependencies].
  
  bool updateShouldNotifyDependent(covariant InheritedModel<T> oldWidget, Set<T> dependencies);

InheritedWidget이네요. 좀 감이 잡히실까요? 아직은 좀 코드가 방대해서 한눈에 안들어오네요. 공식문서에서 소개하는 Inherited Widget 예시코드를 보며 어떻게 자식 위젯에게 상태를 공유하는지 봅시다.

Inherited Widget

class FrogColor extends InheritedWidget {
  const FrogColor({
    super.key,
    required this.color,
    required super.child,
  });

  final Color color;

  static FrogColor? maybeOf(BuildContext context) {
    return context.dependOnInheritedWidgetOfExactType<FrogColor>();
  }

  static FrogColor of(BuildContext context) {
    final FrogColor? result = maybeOf(context);
    assert(result != null, 'No FrogColor found in context');
    return result!;
  }

  
  bool updateShouldNotify(FrogColor oldWidget) => color != oldWidget.color;
}

이렇게, Inherited Widget을 상속받는 위젯들은 공통적으로 of 메소드와 updateShouldNotify 메소드를 갖고 있습니다. of 메소드는 maybeOf메소드를 통해서 상속된 위젯의 가장 가까운 인스턴스를 불러옵니다.

그리고 동시에 의존성을 호출했던 위젯을 등록하기도 합니다. 이후, 해당 Inherited Widget의 값이 변경될 때의 재빌드를 위해서 말이죠.

그리고 이러한 재빌드를 수행하는 것이 updateShouldNotify 메소드입니다. 이를 통해 재빌드되야 하는 모든 위젯에 이러한 변경을 알리는 것이죠.

initState와 of 메소드

이러한 과정을 이해하게 되면, 어떠한 점을 발견할 수 있는데, initState 또는 생성자에서 of 메소드를 사용하게 되면 어떻게 될지 보며 이해해봅시다.

기본적으로 of 메소드를 통해서 Inherited Widget의 상태를 접근하게 되면, 상태의 변경을 감지할 때마다 재빌드를 해야 하는데, initState에서 이러한 상태가 변경이 될리가 없잖아요?

그렇기에 아래와 같은 에러를 볼 수 있습니다.

그래서 프레임워크가 위와같은 경고를 주는 것입니다. 프레임워크는 initState에서 작성된 of 메소드를 확인한 후, 위젯의 생명주기를 이미 파악하고 있기 때문에 initState가 다시 실행된다는 것은 말이 안된다고 판단하기 때문이죠.

그렇기에 of 메소드를 안전하게 사용하는 방법은 didChangeDependencies 또는 build 메소드 내에서 호출하는 것입니다.

didChangeDepencies와 build에서 of 메소드 호출의 차이

여기서, 잠깐만 두 메소드내에서 실행되는 of 메소드의 차이점을 좀 알아봅시다. build 메소드는 우리가 수없이 많이 코드를 작성하기에 굉장히 익숙하죠. 그렇다면 didChangeDependencies 메소드는 무엇인지 알아봅시다.

우선, 메소드 이름에서 보이는 Dependencies, 의존성이 무엇인지 알아봅시다. 이 의존성은 무엇을 의미할까요? 위에서 언급된 dependOnInheritedWidgetOfExactType이 생성한 의존성을 의미합니다.

만약 아래와 같은 코드를 작성하였을 때를 생각해봅시다.

  
  void didChangeDependencies() {
    final data = MediaQuery.of(context);
    size = expensiveCalculate(data);
    super.didChangeDependencies();
  } 

부모 노드가 중요하지 않고 오직 코드가 실행되는 것에 중점을 둔 상태라고 했을 때, MediaQuery의 상태가 변경되는 시점이 발생한다면,
didChangeDependencies 메소드는 build 메소드보다 먼저 실행된 후 build 메소드가 실행됩니다. 단순히, build 메소드는 이 값을 참조하면 되겠죠.

물론, build 메소드 내부에서 해당 연산을 수행해도 코드는 굴러갑니다.

  
  void build(BuildContext context) {
    final data = MediaQuery.of(context);
    size = expensiveCalculate(data);
    return SomeWidget(
		...
  } 

성능적인 차이는 존재하지 않겠지만, 개발을 하다보면 수없이 많은 위젯들이 빌드되고 복잡한 애니메이션에서는 이러한 차이가 앱 성능 전체에 영향을 줄 수도 있을 것입니다.

결론

Inherited Widget은 복잡한 위젯 트리에서 상태의 변경을 하위 위젯에게 알리면서 효율적으로 상태를 전파합니다. 하지만, 공식적으로는 Provider 사용을 권장하기 때문에 이러한 것이 있구나 알고가셨으면 좋겠습니다!

profile
자기주도적, 지속 성장하는 모바일앱 개발자가 되기 위해

0개의 댓글