2024-01-05
1. Flutter’s rendering model
일반적으로 크로스 플랫폼 언어들은 추상화된 레이어를 통해 네이티브 언어들과 소통을 하게 되는데, 이러한 추상화 과정에서 많은 부하를 가져오게 된다.
반면에 Flutter는 자체 위젯 세트를 통해 시스템 UI 위젯 라이브러리를 우회하여 이러한 추상화를 최소함으로써 부하를 줄이는 전략을 사용했다.
Flutter의 시각적 요소를 그리는 Dart 코드는 렌더링을 위해 Skia(또는 향후 Impeller)를 사용하는 네이티브 코드로 컴파일된다.
Flutter는 또한 자체 Skia 복사본을 엔진의 일부로 포함하므로 개발자는 휴대폰이 새로운 Android 버전으로 업데이트되지 않은 경우에도 최신 성능 개선 사항으로 업데이트를 유지할 수 있도록 앱을 업그레이드할 수 있으며, 이는 다른 플랫폼에서 도 동일한 방식으로 동작한다.
(Flutter 3.10에서는 Impeller를 iOS의 기본 렌더링 엔진으로 설정했다.)
2. From user input to the GPU
아래는 플러터의 렌더링을 진행하는 파이프라인 시퀀스이다.
유저의 제스처에 반응하여 애니메이션의 트리거 되고 이후 요청에 따른 위젯들이 빌드된다. ( 1~3 )
빌드된 위젯 기준으로 레이아웃을 잡고 잡힌 레이아웃에 시각적 효과를 입힌 뒤 재구성하여 산출물을 만든다. ( 4~6 )
최종적인 산출물을 CPU 가 이해할 수 있도록 변환하여 화면에 그려지게 된다. (7)
3. Build: from Widget to Element
플러터는 build() 라는 함수를 통해 각각의 위젯이 상태에 따라 랜더링 UI를 그린다.
Container(
color: Colors.blue,
child: Row(
children: [
Image.network('https://www.example.com/1.png'),
const Text('A'),
],
),
);
아래의 이미지는 위의 코드를 계층구조로 나타낸 예제이다.
여기서 흥미로운 점은 계층 구조 Container 와 Row 사이에 Container의 속성 값인 color 가 ColoredBox 형태로 들어가 있는 것을 확인할 수 있다. 이는 Container 속성값 color는 아래와 같은 코드로 적용이 되기 때문이다. color라는 속성이 있을 경우 Container의 child를 다시 한번 ColoredBox에 담는 것을 확인할 수 있다.
if (color != null)
current = ColoredBox(color: color!, child: current);
또한 Flutter는 빌드 단계에서코드로 표현된 위젯을 모든 위젯에 대해 하나의 요소가 있는 해당 요소 트리로 변환하게 되는데, 각 요소는 트리 계층 구조의 지정된 위치에 있는 위젯의 특정 인스턴스를 나타낸다. 요소에는 두 가지 기본 유형이 있는데 아래와 같다.
- ComponentElement, a host for other elements. (각 요소들의 호스트)
- RenderObjectElement, an element that participates in the layout or paint phases. (레이아웃 | 페인티에 참여하는 원소)
위젯들은 Buildcontext를 참조할 수 있고, 노드 간의 상위/하위 관계를 포함하여 불변이기 때문에 위젯 트리가 변경되면(ex: 앞의 예에서 Text('A')를 Text('B')로 변경) 위젯 객체의 새로운 세트가 반환된다. 다만 이게 전체 구조를 다시 그린다는 의미는 아니다.
플러터에서 "element tree(요소 트리)"는 프레임 간에 지속적으로 유지되며, 이로 인해 플러터는 위젯 계층 구조가 완전히 처분 가능한 것처럼 행동할 수 있다. 동시에 기본 표현구조를 캐싱한다. 변경된 위젯만 탐색함으로써 플러터는 다시 설정이 필요한 요소 트리의 일부만 다시 구축할 수 있게 된다.
이 말의 핵심은 플러터가 화면을 렌더링할 때 위젯 트리의 일부가 변경되더라도 전체 트리를 다시 만들지 않고, 변경된 부분만 다시 구성하여 성능을 향상할 수 있다는 점이다. 이는 플러터가 이전에 렌더링 된 화면의 상태를 유지하면서 변경된 부분만을 갱신하여 렌더링 하는 메커니즘이다.
요소 트리의 지속성은 성능 측면에서 중요한 역할을 하게 되는데 이는 전체 위젯 트리를 매번 새로 구성하는 대신 변경된 부분만을 업데이트함으로써 렌더링 프로세스를 최적화하고, 앱의 반응성과 효율성을 향상시킬 수 있게 된다.
4. Layout and rendering
모든 노드들의 render tree 베이스 클래스는 Render Object 이다. 이는 layout 과 painting의 추상화를 정의해 준다. 각각의 Render Object 는 자신의 부모노드에 대한 정보를 알고 있지만 자식노드에 대해서는 접근 방법과 제약 조건 외에는 거의 알지 못한다.
빌드 단계에서 Flutter는 요소 트리의 각 RenderObjectElement에 대해 RenderObject에서 상속되는 객체를 생성하거나 업데이트한다. RenderObject는 기본 요소이며, RenderParagraph는 텍스트를 렌더링 하고, RenderImage는 이미지를 렌더링 하며, RenderTransform은 자식을 그리기 전에 변환을 적용한다.
대부분의 Flutter 위젯은 RenderObject를 나타내는 RenderBox 하위 클래스에서 상속된 객체에 의해 렌더링된다. RenderBox는 렌더링 할 각 위젯의 최소 및 최대 너비와 높이를 설정하는 상자 제약 모델의 기초를 제공한다.
레이아웃을 수행하기 위해 Flutter는 깊이 우선 순회로 render tree 를 탐색하고 크기 제약 조건을 상위에서 하위로 전달한다. 크기를 결정할 때 자식은 부모가 제공한 제약 조건을 따른다. 자식은 부모가 설정한 제약 조건 내에서 부모 개체에 크기를 전달한다.
이 단일 트리 탐색이 끝나면 모든 객체는 부모의 제약 조건 내에서 정의된 크기를 가지며 Paint() 메서드를 호출하여 화면에 그릴 준비를 한다.
box constraint model 은 O(n) 시간에 객체를 레이아웃하는 방법으로 매우 강력하다.
부모는 최대 및 최소 제약 조건을 동일한 값으로 설정하여 자식 개체의 크기를 지정할 수 있으며, 너비에 대한 지시만 하고 자식 노드에게 높이에 대한 유연성을 제공할 수 도 있다.
5. 출처
https://docs.flutter.dev/resources/architectural-overview#layout-and-rendering
메인 이미지 출처 : 사진: Unsplash의Pawel Czerwinski