Flutter 렌더링 관련 두가지 주요한 명제가 있다.
Everything's a Widget.
A widget is an immutable description of part of a user interface
Flutter 는 성능상의 이유로 widget 을 immutable 하게 관리한다. 만약 mutable 하다면, 위젯의 상태가 변했을 때 업데이트를 위해선 모든 위젯 내부의 상태를 비교해야 한다. 이를 immutable 하게 관리하고, 위젯의 상태가 변하면 새로운 위젯을 생성하는 방식을 사용하면, 업데이트를 위해서 단순히 같은 인스턴스인지만 확인하면 된다.
Flutter 는 어떻게 UI 의 상태를 관리할까?
이를 이해하려면 Flutter 가 관리하는 3가지 트리에 대하여 알아야 한다.
Tree 의 종류에는 Widget, Element, RenderObject 가 존재한다.
Widget 은 Element 를 위한 Configuration 을 기술한다. 개발자가 UI가 어떻게 그려지기 원하는지를 코드로 설명하는 과정이 이에 해당한다.
Element 는 mutable 하며, UI의 updating, changing 을 담당한다. Element 는 widgets 의 lifecycle 을 관리하는 역할을 한다.
RenderObject 는 실제 layout, paint 에 사용되며, Flutter 는 UI 를 그릴 때 widget 트리를 보는 것이 아닌, RenderObject 트리를 보고 있다. 이 또한 mutable 하다.
UI 가 그려지는데에 크게 총 3가지 과정을 Configure, Life Cycle, Paint 로 구분할 수 있다. Configure 단계에선 properties 를 저장하고, public API 를 제공한다. Life Cycle 단계에선 UI hierarchy 를 유지하고, parent/child 관계를 관리한다. Paint 단계에선 스스로를 위한 sizing & painting, children 을 위한 layout, 마지막으로 claim input event 등의 기능을 수행한다.
이를 각각 Widget, Element, RenderObject 가 담당한다고 볼 수 있다.
결국, widget 이 immutable 이더라도, 이와 mapping 되는 element 나 render object 가 mutable 하기 때문에 상태 변화에 대응할 수 있다.
왜 3가지 요소를 분리하는 걸까?
역할이 다르더라도, 이는 충분히 한개의 요소에서 관리될 수도 있다. 그런데 왜 각각의 역할을 분리하는 걸까? 이는 효율적이고 유연한 UI 업데이트를 위함이다.
화면의 상태가 변하더라도 중복적인 요소는 존재한다. 예를 들어, Text Widget 의 라벨이 동적으로 변경되는 경우가 존재할 것이다. Flutter 는 이전 위젯과 현재 위젯의 runtime type 과 key 를 비교하여 만약 차이가 없다면 이에 대응하는 element 와 render object 는 가볍게 update 시키는 방식으로 동작한다.
즉, widget 만 새롭게 교체되고, 그 외에 중복되는 요소는 일부 업데이트와 함께 그대로 사용하여 불필요하연 중복 작업을 줄일 수 있는 것이다. 실제로 DevTool 을 통해 확인해 보면, 같은 render object 를 사용하는 것을 확인할 수 있다.
다음 3장의 연속적인 이미지는 위에서 설명한 Text 의 label 을 변경하는 과정을 나타낸다.
Build: from Widget to Element
다음 코드가 widget 에서 element 로 build 되는 과정을 살펴보자.
Container(
color: Colors.blue,
child: Row(
children: [
Image.network('https://www.example.com/1.png'),
const Text('A'),
],
),
);
Flutter 가 위 fragment 를 render 하기 위해선 build 함수를 수행한다. Container 또한 widget 이며 build 를 포함하고, 이를 수행한다고 생각하면 될 것 같다.
bulid 함수의 return 값은 마찬가지로 widget 으로, 현재 앱 상태에 따라 UI를 렌더링하는 위젯의 하위 트리이다. 위 코드 예시에선 Container 에 color 와 child properties 에 값이 전달되었는데, 그 결과 Container -> ColoredBox -> Child 순서의 트리가 생성되게 된다.
이는, Container 내부에서 다시한번 여러 위젯을 조합하기 때문인데, 내부 코드를 보면 이해하기 쉽다.
if (color != null)
current = ColoredBox(color: color!, child: current);
같은 방식으로, Image 와 Text 위젯 또한 child 위젯을 각각 RawImage, RichText 위젯에 한번 더 감싼다. 최종적인 Widget Tree 는 다음과 같다.
Flutter DevTool Inspector 에서 확인할 수 있는 Object Tree 가 바로 이 Widget Tree 이다.
build phase 에서, Flutter 는 위젯에 일치하는 element 를 생성한다. 이는 각각의 위젯에 1대1로 mapping 된다. element 는 tree 의 계층 구조에 지정된 위치에 있는 특정 인스턴스를 나타낸다. 여기에는 2가지 타입이 존재한다.
- ComponentElement 로, 다른 elements 의 host 역할을 한다.
- RenderObjectElement 로, 레이아웃과 페인트 단계에 참여한다.
RenderObjectElements 는 실제 렌더링에 사용되는 RenderObject 와 위젯 사이의 중개자 역할을 한다.
참고로, StatlessWidget, StatefulWidget 은 모두 ComponentElement 에 매핑된다.
모든 위젯의 element 는 BuildContext 를 통해 참조할 수 있다. BuildContext 는 트리내의 위젯 위치에 대한 정보를 핸들링 하는 추상 클래스이며, 이를 구현한 것이 element 라고 볼 수 있다. BuildContext 는 build 함수를 통해 전파되며, Theme.of(context) 와 같이 트리 내의 가장 가까운 조상의 위젯을 참조하기 위해서 사용될 수 있다.
앞서 말했듯이 widget 는 immutable 하기 때문에, 노드 간의 부모/자식 관계를 포함한 위젯트리에 어떠한 변경사항이라도 발생하면 새로운 셋의 위젯이 리턴된다. 그러나 그것이 위젯과 관련한 모든 정보가 매번 rebuild 된다는 것을 의미하진 않는다. widget tree 에 mapping 되는 element tree 는 프레임 간에 지속되며, Flutter 가 모든 위젯을 disposable 하게 사용하면서도, 내재적으로는 캐싱이 이루어 지는 것을 가능하게 한다. element tree 는 위젯에 변경사항이 발생했을 때만 업데이트 및 새롭게 생성된다.
Layout and rendering
UI 프레임워크는 화면에 렌더링 되기 이전에, 각 element 의 계층 구조를 효율적으로 배치하고, 사이즈와 위치를 결정할 수 있어야 한다. Render Tree 에 포함되는 모든 노드는 RenderObject 이며, 이는 layout 과 painting 을 위해 추상화된 모델이다. 각 RenderObject 는 부모에 대하여는 잘 알고있지만, 자식에 대하여는 어떻게 방문하는지와 contraints 에 대한 정보만 가지고 있다.
build 단계에서 Flutter 는 각각의 RenderObjecetElement 에 대하여 RenderObject 를 상속 받는 object 들을 생성하거나 업데이트한다. RenderObject 를 상속받는 RenderParagraph 는 text 를, RenderImage 는 이미지를, RenderTransfrorm 은 부모와 자식노드간의 변형을 렌더링한다.
대부분의 Flutter 위젯은 RenderBox 를 상속받는 object 에 의하여 렌더링 된다. RenderBox 는 2차원 좌표 공간에 고정된 사이즈의 RenderObject 를 나타낸다. RenderBox 는 기본적인 box constraint model 을 기초를 제공하며, 넓이와 높이의 최소, 최대값을 지정할 수 있도록 도와준다.
레이아웃을 수행하기 위해서, Flutter 는 Render Tree 를 DFS 방식으로 순환한다. 이 때, constraints 은 부모에서 자식으로 전달되며, child 는 사이즈를 결정할 때 해당 constraints 를 따른다. 자식 노드는 응답으로 결정된 자신의 size 를 부모에게 돌려준다.
최종적으로, 모든 object 의 size 가 부모 constraints 에 의하여 결정되고, paint() method 를 호출하여 페인팅 될 준비가 끝나게 된다.
Render 트리의 root 에는 RenderView 가 존재하며, 이는 render tree 의 total output 을 나타낸다. 플랫폼이 새로운 frame 이 렌더링 되길 요구하면 compositeFrame() 메소드를 호출하는데, 이는 SceneBuilder 를 생성하여 scene 업데이트를 트리거한다. Scene 이 오되면, RenderView 는 composited 된 scene 을 Window.render() 메소드를 통해 전달하며, GPU 에게 control 을 넘겨주어 이를 렌더링 하도록 한다.
UI thread 에서 Layout, Paint, Composition 과정을 진행하며, 이를 통해 완성된 compoisted scene 을 GPU 에게 처리하도록 맡긴다. GPU 가 전달받은 scene 을 pixel 로 변환하고 실제 화면에 그려주는 것을 Rasterize 라고 부른다.
참고 자료
https://docs.flutter.dev/resources/architectural-overview#rendering-and-layout
Flutter architectural overview
A high-level overview of the architecture of Flutter, including the core principles and concepts that form its design.
docs.flutter.dev
'Flutter' 카테고리의 다른 글
Flutter User Input & Animation (0) | 2023.05.19 |
---|---|
Flutter Widgets (0) | 2023.05.18 |
[Dart] Event Loop (0) | 2023.05.15 |
[Dart] Isolates & Runtime + how Flutter work with it (0) | 2023.05.14 |