[Flutter] 3. 플러터의 렌더링

건전한건전지·2022년 9월 6일
0

Flutter Architecture

목록 보기
4/4

이 글을 쓰는 이유..

플러터 개발자로 면접을 보다보면 거의 10번 중 8번은 이런 질문을 하는 것 같다.

플러터의 렌더링 과정이나 트리의 구성을 설명해주세요.

대충은 알고있다. 트리는 3개가 있고 레이아웃은 렌더 트리에서 어쩌구.. 어디선가 주워듣고 단편적으로 찾아본 내용으로 최대한 말은 해본다.
그러면 돌아오는 대답은 '어느정도 알고 계시네요'다.
어느정도 안다는 말은 플러터 개발자 길을 선택한 나에겐 적지않은 충격이었다.
적어도 자신이 다루는 것에 대해서는 잘 알고있어야 한다고 생각하기 때문이다.
코드 짜는데 신나서 문서를 등한시 했으니..

그래서 각잡고 플러터의 아키텍처에 관해 공부해보려한다.


지난 포스트에서는 플러터의 위젯에 대해 알아보았다.
이번에는 플러터가 어떻게 네이티브에 준하는 퍼포먼스를 내는지 알아보자.


플러터의 렌더링 모델

플러터의 렌더링 과정을 알아보기 전에 안드로이드의 렌더링 과정을 간단히 알아보자.
먼저, 안드로이드 프레임워크의 자바 코드를 호출한다. 그러면 안드로이드 시스템 라이브러리는 C/C++로 작성된 그래픽 엔진인 Skia로 렌더링될 수 있는 객체를 제공하고, 그래픽 엔진은 CPU 또는 GPU을 호출해 드로잉 작업을 수행한다.

한가지 더, 플러터 등장 이전의 크로스 플랫폼 프레임워크의 동작을 살펴보자.
일반적인 크로스 플랫폼 프레임워크는 안드로이드와 IOS UI 라이브러리에 추상 계층을 만듬으로써 동작한다. 이러한 앱 코드는 종종 JavaScript와 같은 인터프리터 언어로 작성되고, UI 표현을 위해 Java 베이스인 안드로이드 또는 Object-C 베이스인 IOS의 시스템 라이브러리와 상호작용한다. 이러한 동작은 UI와 앱 로직에 많은 상호작용이 발생할 때 상당한 오버헤드가 발생한다.

그러면 플러터는 무엇이 다를까?
플러터는 자체 고유의 위젯 세트를 사용해 위에서 설명한 추상화를 최소화했다. 플러터의 시각적인 부분을 담당하는 다트 코드는 Skia 사용을 위한 네이티브 코드로 컴파일된다. 또한, 플러터는 엔진에 Skia의 복사본을 포함하고있어, 디바이스의 안드로이드 버전이 낮아도 앱의 성능을 최신의 성능을 내게끔 해준다. 이러한 동작은 IOS, Windows, macOS 등에서도 똑같이 동작한다.

정리하면, 기존의 크로스 플랫폼 앱들은 네이티브 코드와 상호작용해 드로잉을 수행했다면, 플러터는 자신의 UI 코드를 사용해 드로잉을 수행한다고 보면 되겠다. 네이티브 코드를 거치거나 상호작용하지 않기에 성능상 충분한 이득이 있는 것은 당연한 것으로 보인다.


렌더링 파이프라인


플러터의 렌더링 파이프라인을 표현한 모형이다.
두 쓰레드가 플러터 렌더링에 관여한다.

  1. UI Thread
    UI 쓰레드는 1 부터 5까지 단계를 수행한다. 이 때, 어플리케이션과 플러터 코드를 포함한 다트코드가 다트 VM에서 살행된다. 그러고나서 레어이 트리 및 드로잉 설명을 생성하기 위한 위젯 트리, 엘리멘트 트리, 렌더 오브젝트 트리를 생성한다. 레이어 트리는 드로잉 설명에 저장된다.

  2. GPU Thread
    GPU 쓰레드는 6 단계를 수행한다. 플러터 엔진의 Skia와 관련된 그래픽 코드를 수행하며, 레이어 트리를 얻기위해 GPU와 상호작용하고, 레이어 트리를 구성하고 래스터화하고, 트리를 스크린에 표현한다.


위 다이어그램은 플러터의 파이프라인을 간단히 나타낸 것이다.

build 단계를 자세히 알아보자

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

계층 구조의 위 코드를 보자.
플러터가 이 코드를 렌더링할 때, 현재 앱 상태를 기반으로 위젯 하위트리를 리턴하는 build() 매소드를 실행한다. 이 과정동안 build() 매소드는 상태를 기반으로 새로운 위젯을 도입한다.
위 코드에서 Container는 colorchild 속성을 가지고 있다. Container의 소스 코드를 살펴보면, color가 null이 아니면 색상을 표현하는 ColoredBox가 삽입되는 것을 볼 수 있다.

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

똑같이, ImageText 위젯은 build 과정동안 자식 위젯으로 RawImageRichText를 삽입한다.
따라서, 위젯 계층은 아래 모형과 같이 작성한 코드로 표현되는 것보다 더 깊어지게 된다.

또한, build 과정에서 플러터는 표현된 모든 위젯에 대해 각각의 엘리멘트를 가지도록 element tree에 변환한다.
각 엘리멘트는 트리 계층에서의 주어진 위치의 위젯의 인스턴스이다. 이 때, 엘리멘트는 두 가지 타입이 있다.

  • ComponentElement 다른 엘리먼트의 호스트이다.
  • RenderObjectElement 레이아웃 또는 페인트에 관련된 엘리먼트이다.

그림으로 보면 다음과 같다.

엘리먼트는 트리에서 위젯의 위치에 관여하는 BuildContext를 통해 참조될 수 있다. 그리고 이 BuildContextbuild() 매소드의 파라미터로 제공된다.

위 그림에서 Text의 글자가 바뀐다면 트리는 어떻게 바뀔까??
플러터의 기본 원칙으로 위젯은 불변(immutable)이다. 즉, 부모/자식의 관계를 포함한 모든 위젯은 불변이기때문에, 위젯 트리의 모든 변화(Text('A') => Text('B')로 변하는 경우 등)는 새로운 위젯 세트가 리턴되어야 한다.
그러면 변화가 일어날 때 마다 새로 트리를 만들면 리소스 낭비 아닌가?
플러터는 그렇지 않다. 엘리먼트 트리는 각각 프레임에 영속적이어서 하나의 엘리먼트 트리만을 사용하는 것처럼 보이며, 엘리먼트 트리에서 재설정이 필요한 부분만을 골라서 리빌드 한다.


레이아웃 및 렌더링

위 단락에서 우리가 작성한 위젯 코드가 위젯 트리를 만들고 해당 트리를 사용해 엘리먼트 트리를 만든다는 것을 알게되었다. 그러면 플러터는 화면에서 위젯이 어디에 위치해야하는지 어떻게 아는 걸까? 한번 알아보자.

플러터는 레이아웃과 페인팅을 정의하기 위한 추상 모델로 RenderObject를 사용하며, 이는 후에 기술할 렌더 트리의 모든 노드의 베이스 클래스다. 이 RenderObject는 부모에 대해서는 알지만, 자식에 대해서는 제약조건이나 확인하는 방법 말고는 아는 것이 거의 없다. build 과정 동안, 플러터는 엘리먼트 트리의 RenderObjectElement에 대해 RenderObject를 구현하는 오브젝트를 만들거나 업데이트한다.

대부분의 플러터 위젯은 2D 데카르트 좌표계, 고정 크기의 RenderBox를 구현함으로써 렌더링된다. RenderBox는 최대, 최소 높이와 너비를 렌더링하는데 사용한다. 플러터는 레이아웃을 수행하기 위해 렌더 트리의 처음부터 아래로 깊이 우선 탐색으로 내려가며 크기 제약을 자식에게 전해준다. 자식들은 부모가 준 제약조건에 따라야하며 받은 제약조건 내에서 자식은 크기를 부모에게 전달한다. 이 과정을 거치고 나면 모든 오브젝트들의 크기가 결정되고 paint 매소드를 실행할 준비가 된다.
그림으로 보면 다음과 같다.

렌더 트리의 총 출력을 표현하는 RenderView가 트리 루트에 있으며, 플랫폼이 새 프레임을 요구할 때, RenderView 객체의 compositeFrame() 매소드가 호출된다. 이 매소드는 SceneBuilder를 만들고 이 Builder는 scene을 업데이트한다. scene 업데이트가 완료되면 RenderView 객체는 조합된 scene을 Window.render() 매소드로 보내어 GPU에게 렌더링을 지시한다.

이로써 렌더링 과정이 끝나게 된다.


마무리

그동안 미뤄왔던 플러터의 렌더링 과정을 간략하게 알아보았다. 플러터의 아키텍처나 렌더링에 관한 내용은 유튜브로도 많이 게시되어 있고, 다른 개발자분들이 정리한 내용도 상당히 많다. 필자도 이번 포스트를 작성하면서 여러 블로그를 찾아봤는데 상당히 자세히 정리해놓은 블로그도 많았다. 다소 어려운 내용(아닐수도..)이었지만 포스트를 쓰면서 많이 정리가 된 것 같다.


<참고>

원문 : https://docs.flutter.dev/resources/architectural-overview#widgets

profile
Flutter 공부를 위한 블로그입니다.

0개의 댓글