얼렁뚱땅 개발 블로그

[Flutter] Flutter의 렌더링 방식 본문

전공/Flutter

[Flutter] Flutter의 렌더링 방식

김경원0519 2024. 9. 4. 01:27
반응형

목차

1. 렌더링 모델

1-1. Android 렌더링 동작

렌더링을 할 때 Android 프레임워크의 자바 코드를 호출하며, Android 시스템 라이브러리는 Canvas 객체에 그림을 그리는 컴포넌트를 제공합니다. 

Canvas 객체는 C/C++로 작성된 그래픽 엔진인 Skia를 렌더링 할 수 있으며, 이 엔진은 CPU 또는 GPU를 사용하여 렌더링 됩니다.

1-2. 크로스플랫폼 렌더링 동작

Android 및 iOS UI 라이브러리 위에 추상화 계층을 생성합니다. 이러한 방식은 Java 기반의 Android와 Objective-C 기반의 iOS 시스템 라이브러리와 상호작용하여 UI를 표시합니다. 

이 방식은 UI와 앱 로직 간의 상호작용이 많을 때 상당한 오버헤드를 추가합니다.

1-3. Flutter 렌더링 동작

Flutter는 크로스플랫폼 렌더링 방식의 추상화를 최소화하고 시스템 UI를 사용하지 않고 자체 위젯 세트를 사용합니다. Flutter의 화면을 그리는 Dart 코드는 네이티브 코드로 컴파일되며, Skia(차후 Impeller)를 사용해 렌더링 됩니다. 

또한 Flutter 엔진의 일부로 Skia의 복사본을 포함하고 있어, Skia 동작이 Android 버전에 영향을 받지 않습니다. (다른 네이티브 플랫폼에서도 동일한 방식으로 동작됩니다.)


2. 렌더링 파이프라인

Flutter의 렌더링 파이프라인은 다음 사진과 같습니다.

1. 사용자 입력

키보드, 터치스크린 등과 같은 입력 제스처에 대한 응답

2. 애니메이션

타이머 tick에 의해 트리거 된 사용자 인터페이스 변경

3. 빌드

화면에 표시되는 위젯을 생성하는 앱 코드

4. 레이아웃

화면에서 elements 위치 지정 및 크기 조정

5. 페인트

요소를 시각적 표현으로 변환

6. 구성

도면 순서대로 시각적 elements 오버레이

7. 레스터화

결과물을 CPU 렌더링 명령어로 변환


3. Build (from Widget to Element)

아래의 코드를 예시로 들어 Build 방식을 설명하겠습니다.

Container(
  color: Colors.blue,
  child: Row(
    children: [
      Image.network('https://www.example.com/1.png'),
      const Text('A'),
    ],
  ),
);

 

Flutter가 해당 코드를 렌더링 할 때 build() 함수를 호출합니다. build() 함수는 현재 앱 상태 기반으로 UI를 렌더링 하는 위젯의 하위 트리를 반환하는 메서드입니다. 

이 과정에서 build() 함수는 상태에 따라 새 위젯을 추가합니다. 이 예시로 위의 코드에서 Container에서 color와 child 속성을 가지고 있다. 이때 Container의 코드를 보면 color가 null이 아닌 경우 ColoredBox를 추가합니다.

if (color != null)
  current = ColoredBox(color: color!, child: current);

Container와 동일하게 Image, Text 위젯은 build 과정 동안 자식 위젯으로 RawImage와 RichText를 추가합니다.

위젯 트리는 작성된 코드를 표현하는 것보다 더 깊어지게 됩니다.

빌드 수행이 완료된 위젯 트리

DevTools의 Flutter Inspector와 같은 디버그 도구를 통해 트리를 검사할 때 원래 코드보다 훨씬 더 깊은 구조를 볼 수 있던 이유입니다.

Build 단계에서 Flutter는 코드로 표현된 위젯을 모든 위젯을 element를 사용하여 Element Tree로 변환합니다. Element Tree의 주어진 위치에 있는 위젯의 특정 인스턴스를 나타냅니다. 

  • ComponentElement: 다른 elements를 포함하는 호스트 역할
  • RenderObjectElement: 레이아웃 또는 페인트 단계의 element

Widget의 대한 Element는 위젯의 위치를 나타내는 BuildContext를 통해 참조할 수 있습니다. Context는 Theme.of(context)와 같은 함수 호출에서 사용되며, build() 메서드의 매개변수로 전달됩니다.

Widget은 불변(immutable)이기 때문에, Widget Tree 내에서 부모와 자식 위젯 간의 관계를 포함하여 Widget Tree에 변경이 생기면 새로운 Widget Tree를 반환합니다. (Widget 변경의 예, Text('A')를 Text('B')로 변경)

하지만 새로운 Widget Tree를 반환한다고 모든 Widget이 새로 그려지는 것은 아닙니다. Element Tree는 프레임마다 지속되며, 변경된 부분만 업데이트하여 성능을 최적화합니다. Widget Tree는 일회용처럼 다루지만, Element Tree를 통해 효율적으로 캐싱하고 관리하여 불필요한 재렌더링을 방지합니다.


4. 레이아웃과 렌더링

4-1. RenderObject

UI 프레임워크의 중요한 부분은 Widget Tree를 효율적으로 배치하여 element가 화면에 렌더링 되기 전까지 크기와 위치를 결정하는 기능입니다.

RenderTree 내 모든 노드의 기본 클래스는 RenderObject입니다. RenderObject레이아웃과 페인팅을 위한 추상 모델을 정의하며, 특정 차원이나 데카르트 좌표계에 영향을 받지 않습니다. RenderObject는 부모를 알고 있지만 자식에 대한 정보는 접근 방법과 졔약 조건 정도만 가지고 있습니다. 이러한 추상화 덕분에 RenderObject가 다양한 사용 사례를 처리할 수 있도록 해줍니다.

Build 단계에서 ElementTree의 각 RenderObjectElement에 대해 RenderObject에 상속되는 객체를 생성하거나 업데이트합니다. 

RenderObject의 기본소요:

  • RenderPergage: 텍스트 렌더링
  • RenderImage: 이미지 렌더링
  • RenderTransform: 자식을 그리기 전 변환을 적용

대부분의 Flutter Widget은 2D 데카르트 공간에서 고정된 크기의 RenderObject를 나타내는 RenderBox 하위 클래스에서 상속되는 객체로 렌더링 됩니다. RenderBox는 렌더링 될 최소 및 최대 너비와 높이를 설정하는 Box 제약 모델의 기초를 제공합니다.

4-2. Constraint(제약 조건)과 Layout 처리 

레이아웃을 수행하기 위해, Flutter는 깊이 우선 탐색으로 RenderTree를 순회하며, 부모에서 자식으로 크기 제약을 전달합니다. 자식이 크기를 결정할 때 부모가 제공한 제약 조건을 준수해야 하며, 자신의 크기를 다시 부모로 전달합니다.

트리는 한 번 순회한 후 모든 객체가 부모의 제약 내 정의된 크기를 가지며 paint() 메서드를 호출하여 그릴 준비가 됩니다.

Box constraint model은 객체를 O(n) 시간 내에 배치하는 강력한 방법입니다.

  • 부모는 최대 및 최소 제약을 동일한 값으로 설정하여 자식 객체의 크기를 지정할 수 있습니다.
    예를 들어, 애플리케이션의 최상위 RenderObject는 자식을 화면 크기로 제한합니다. (렌더링 할 내용을 중앙에 배치하는 것 처럼 자식은 그 공간을 어떻게 사용할 지 선택할 수 있습니다.) 
  • 부모는 자식의 너비를 지정하지만 높이에 대한 유연성을 제공할 수 있습니다. (또는 높이를 지정하고 너비에 대한 유연성을 제공할 수 있습니다.) 
    예시로, 수평 제약에 맞춰야 하지만 텍스트 양에 따라 수직으로 변할 수 있는 Text가 있습니다

Box constraint model은 자식이 콘텐츠 렌더링 방법을 결정할 때 사용 가능한 크기를 알아야 하는 경우에도 작동합니다.

예를 들어, LayoutBuilder를 사용하면 자식이 전달받은 제약 조건에 따라 어떻게 사용할 지 결정할 수 있습니다.

Widget build(BuildContext context) {
  return LayoutBuilder(
    builder: (context, constraints) {
      if (constraints.maxWidth < 600) {
        return const OneColumnLayout();
      } else {
        return const TwoColumnLayout();
      }
    },
  );
}

4-3. RenderView

모든 RenderObject의 루트는 RenderView입니다. RenderView는 RenderTree의 전체 출력을 나타냅니다. 플랫폼이 새 프레임을 렌더링할 때, RenderTree의 루트에 있는 RenderView 객체의 일부인 compositeFrame() 메서드가 호출됩니다. 

compositeFrame() 메서드는 SceneBuilder가 생성되어 Scene 업데이트를 합니다. (Scene은 화면에 렌더링할 최종적인 그래픽 데이터를 의미합니다.)

Scene이 완료되면 RenderView는 합성된 Scene을 dart:ui의 Window.render() 메서드로 전달하고, Window.render() 메서드는 GPU에 제어를 전달하여 렌더링을 수행합니다.

5. 요약

  • WidgetTree
    • 위젯의 구성
    • 상태에 따라 자식 Widget 추가
    • WidgetTree에 변화가 생기면 새로운 WidgetTree를 반환
  • ElementTree
    • Widget과 1:1 대응
    • 변경된 부분만 업데이트하여 성능을 최적화
      • 새로운 WidgetTree를 반환되더라도 모든 Widget이 새롭게 그려지지 않음
    • Element 종류 
      • ComponentElement: 다른 elements를 포함하는 호스트 역할
      • RenderObjectElement: 레이아웃 또는 페인트 단계의 element
  • RenderTree
    • 화면에서 요소를 배치하고 그리는 작업을 수행
      • Layout과 Rendering
    • ElemenetTree의 RenderObjectElement와 대응

 

반응형

'전공 > Flutter' 카테고리의 다른 글

[Flutter] Flutter란 무엇인가요?  (0) 2024.03.02
Comments