[Flutter 디코딩 하기 시리즈] 3. BuildContext?!

ricky_0_k·2023년 4월 4일
0
post-thumbnail

서론

저번에 생명주기로 골머리를 겪었는데 이번엔 더 심층적인 것이 나왔다.
안드에서도 골머리를 앓았던 Context.. 인데 Flutter 에서는 5단어가 더 생긴 BuildContext 이다..
이번에도 동영상 독파 후 시작한다.

소개

https://www.youtube.com/watch?v=rIaaH87z1-g&list=PLjxrf2q8roU1fRV40Ec8200rX6OuQkmnl

동영상에서는 먼저 위젯에 대해 복습을 간단하게 시작한다.
Flutter 의 위젯은 UI 가 갖춰야 하는 청사진 을 이야기한다.

위젯의 관계

위젯들은 서로 독립적이지 않고 관계를 맺는다고 이야기한다. 실제로 그렇다.

  • 빨간색 영역처럼 다른 위젯 옆에 있을 수 있다.
  • 초록색 영역처럼 위젯 내에 종속될 수도 있다.

위의 말이 이해가 잘 안될수도 있을 것 같아 코드도 준비해 보았다.


Widget build(BuildContext context) {
  return Scaffold(
    body: Column(
      children: [
        Row(
          children: [
            ColoredBox(
              color: Color(0xffff0000),
              child: Text("기둘기"),
            ),
            Text("니둘기"),
          ],
        ),
        ColoredBox(
          color: Color(0xffff0000),
          child: Padding(
            padding: EdgeInsets.all(8),
            child: Text("비둘기"),
          ),
        ),
      ],
    ),
  );
}

위 도식화된 그림에 따라 정리를 해본다면 이렇게 표현이 가능할 것이다. (한번 빌드한 내용도 가져와보았는데 예쁘지는 않다)

어쨌거나 이로서 위젯이 서로 관계를 가진다는 건 알았는데 이게 어떻게 가능할까?

위젯이 이 정보를 가지고 있을까?

위젯 간 관계를 나타내려면 최소 트리 내의 특정한 인스턴스 위치 정보 는 가지고 있어야 할 것이다.
아니 적어도 상위 위젯 이 무엇인지 정도는 알아야 할 것이다.
위의 정보들을 과연 위젯이 가지고 있을까?

그러기엔 위젯은 어느 위치에서든 자유롭게 사용이 가능해야하므로, 그 정보 들을 가지고 있기엔 무겁다.
그 정보를 가지고 있을 때 위젯의 위치가 바뀐다고 가정하면, 위젯 내 값들을 모조리 바꿔주어야 할테니
화면 그리기도 하는 데 위치까지 신경쓰기엔, 위젯이 전지전능한 기능이 되어 무거워질 것이다.

그리고 결정적으로 위젯은 변경할 수 없다.
그런데 위젯 위치 정보는 쉽게 바뀔 수 있는 정보인데 그걸 가지고 있는다?
서로 상충되는 내용이다.

실제 저자도 위젯 내에 해당 값들을 가지고 있지 않다고 언급했다.
그러므로 위젯 자체에서는 트리 내의 특정한 위치 정보를 보관하지 않을 것이다.

그러므로 정답은 ❌ 이다. 그러면 이런 역할을 누가 해주는 것일까?

Element

이후 저자는 Widget 코드를 분석하면서 createElement() 를 이야기한다.

createElement()

인스턴스를 구체화해주는 함수이다.
이 함수로 인해 만들어진 Element 는 위젯 트리에서 해당 위젯이 어디있는지 알려주는 데 활용된다.

실제로 createElement() 를 통해 widget, parent(상위 Element), renderObject(위젯 트리에서 현재 위치에 있는 렌더링 개체) 등을 설정 또는 가져올 수 있다.
(실제 Element 클래스 명세를 보면 _widget, _parent 등의 변수들을 볼 수 있다)

결론적으로 위치 관계를 다루는 역할을 Element 가 해준다는 걸 알 수 있다.

그런데 BuildContext 는 Element 와 뭔 상관?

그런데 BuildContext 와 Element 는 뭔 상관일까?
Element 의 명세를 자세히 보면 이렇게 되어 있다.

결국은 오늘의 주제 BuildContext 가 그 역할을 하는 것이다.

Element 는 다른 Element 들에게 유용한 노드가 되며, 이들이 나타내는 위젯에 대한 각각의 참조를 제공한다.

정리하면 이렇고, 그러면 build()에서 BuildContext 파라미터를 제공하는 것도 어느정도 이해가 된다.
해당 BuildContext 를 통해 상위 Element 를 확인하거나, 현재 위젯의 정보를 가져오거나하여 개발자는 유용한 작업을 할 수 있다.
(ex. MediaQuery.of(context))

이렇게 또 영상은 끝난다.
동영상에서 본 내용을 바탕으로 추가로 본 내용들을 정리해놓으려 한다.

Element Deep Dive

위젯은 불변하다. 그리고 build 를 하면 위젯을 새로 그려낸다.

그러므로 StatefulWidget 을 통해 위젯을 변경해주거나 (setState),
화면을 자주 왔다갔다하면 그만큼 위젯을 버리고 새로 그리는 작업이 많아질 것이다.

상태는 깨끗하게 관리되겠지만 그리는 데 많은 비용을 소모할 것이다.
flutter 는 이런 문제를 어떻게 해결하는 걸까?

Flutter Tree

먼저 Flutter 에서 자주 언급되는 Tree 를 이야기해보려 한다.

우리는 flutter 공식 문서나 위젯 설명 내용을 보면서 Widget Tree 를 들어봤을 것이고,
Widget Tree 뒤에 그림자가 스며든 그림과 Element Tree 에 대한 이야기도 많이 보았을 것이다.
그 외에 Render Tree 라는 것도 있다.

이 3개 Tree 에 대해 간단히 이야기해보려 한다.

Widget Tree

말 그대로 우리가 구현한 위젯 구조를 이야기한 것이다. (ex. 위의 Column 코드)

runApp() 에서 파라미터로 들어가는 위젯이 루트 위젯이 되고
거기서 호출되는 위젯에 따라 아래로 계속 타고 내려갈 것이다.

flutter inspector 를 보면 아래와 같은 구조를 볼 수 있을 것이다. 이것도 Widget Tree 이다.

Element Tree

각 Widget 이 생성되면서 동시에 Element 도 생성된다. (일전에 createElement() 에서 언급)
Element 에서 이야기한대로 다른 위젯과의 부모 또는 자식 관계를 나타내는 Tree 이다.
Widget 생성과 함께 만들어지는 만큼 Element 는 Widget 과 1:1 연결된다.

만약 Widget Tree 가 파괴 후 재창조 수준으로 다시 만들어지는 경우
Element Tree 는 Widget Tree 의 새로 생긴 위젯들과 1:1 연결을 다시하고
기존 위젯과 변경된 내용이 있는지 체크 ( = canUpdate() )한다.

변경된 내용이 있다면 해당 부분만 다시 그리라고(=렌더링하라고) 한다.

Render Tree

실제 그려진 화면에 대한 Tree 이다. Render Tree 는 Element Tree 랑만 연결되어 있다.

Widget Tree 랑 연결 되지 않았기 때문에, 객체만 만들어질 뿐 화면을 다시 구성하는 데에는 큰 영향을 주지 않는다고 한다.

Tree 이야기 도중 성능과 관련 있어보이는 내용을 블록 표시 해놓았다.

  • 위젯도 재사용을 활용한다. -> 재사용을 통해 위젯을 새로 그리는 걸 줄인다.
  • 그리고 렌더링 작업은 Widget 과 연결되지 않았다. -> 잦은 위젯 변경으로 인해 렌더링이 과하게 행해지는 것도 없다.

사실상 Element 를 flutter 의 핵심으로 보아도 될 것 같았다.

Element 생명주기

Element 의 생명주기에 대해 이야기한 공식문서 내용이 있어 가져와 보았다.

생성
createElement() 를 통해 생성된 Element 는, 부모 영역 아래 트리에 추가하기 위해 mount() 를 호출한다.

mount
주어진 부모의 주어진 슬롯에 있는 트리에 새로운 Element 를 추가하는 작업
새로 생성된 Element 가 tree 에 처음 추가될 때 호출되며, 이 함수를 통해 부모에 따라 달라지는 상태를 초기화한다.

연결된 렌더 객체를 Render Tree 에 연결하기위해,
필요에 따라 자식 위젯을 그려주거나 attachRenderObject 를 호출하기도 한다.

createElement() 이후 _firstBuild() 를 통해
state 초기 함수들 (ex. 이전에 Widget Lifecycle 로 언급했던 initState 등), build() 를 실행해준다.

이후 비로소 Element 는 활성으로 간주되어 화면에 나타날 수 있다.

변경

element 가 재사용이 불가능하다면 element 를 새로 만들어준다.
아래 canUpdate() 함수를 통해 먼저 재사용이 가능한지 체크한다.


abstract class Widget {
   …
  @factory
  Element createElement();static bool canUpdate(Widget oldWidget, Widget newWidget) {
    return oldWidget.runtimeType == newWidget.runtimeType && oldWidget.key == newWidget.key;
  }
}

runtimeType 과 key 를 비교해서 기존 위젯과의 변경점을 찾고, 변경점이 있다면 업데이트한다.
runtimeType 과 key 중 하나라도 변경되면 위젯은 새로 그려진다고 보면 된다.
새로 그려지는 flow 에서는, 이전 Element 를 mount 해제하고 새로운 Element 를 생성 후 mount 하여 새로운 위젯으로 변경할 수 있다.

소멸
Element 가 제거될 때 deactivateChild 가 호출되어 해당 Element의 랜더링 개체가 제거된다.
이후 이 Element 는 비활성화되고 unmount 된다.
unmount 된 Element 는 더 이상 사용할 수 없는 상태가 되어 화면에 나타나지 않고 트리에 통합되지도 않는다.

다만 애니메이션이 있을 경우, 애니메이션 종료 전까지는 비활성 상태로 있다가
애니메이션이 끝난 이후 비활성 상태 Element 들이 모두 unmount 된다.

복귀
특수한 경우 Element 가 다시 들어올수도 있다. (ex. 자신이나 부모 위젯에서 GlobalKey 를 사용하는 경우 등)
복귀하는 경우 비활성 Element 목록에서 제거된 뒤 활성상태가 되고 렌더링 개체를 Render Tree 에 다시 연결한다.

결론

Flutter 는 처음 접근하기 쉽다고 한다. 실제로 그렇다.
개발자가 Widget으로 화면을 구성하면 많은 부분을 Flutter가 해주고, Element들을 재사용해주기 때문에
Widget 만 가지고도 성능 저하 없이 화면 개발을 쉽게 할 수 있다.

하지만 깊게 들어가면 화면 표시가 느려지거나 버벅거림을 마주할 수도 있을 것이다.
또는 잘못된 context 를 사용하여 알 수 없는 에러를 마주하고 고치는 데 애를 먹을수도 있다.

이 경우에는 Element 관리를 잘하고 있는지, 잘못된 context 를 사용하지 않았는지 확인이 필요할 것이다.

이번 글을 쓰면서 거의 참고 링크 글을 읽고 그대로 작성한 부분도 있다.
깊게 들어가면 한 없이 어려운 내용인만큼 이번 정리 이후로도 참고 링크 내용을 주기적으로 봐야겠다는 생각이 들었다.

참고

https://papabee.tistory.com/77
https://docs.flutter.dev/resources/inside-flutter
https://docs.flutter.dev/resources/architectural-overview#build-from-widget-to-element
https://api.flutter.dev/flutter/widgets/Element/mount.html
https://ctoahn.tistory.com/30

profile
valuable 을 추구하려 노력하는 개발자

0개의 댓글