플러터는 어떻게 위젯을 렌더링할까?

kimdohun0104
6 min readJun 26, 2020

--

얼마 전 유튜브에서 Google Developer China 2019의 세션 중 하나인 ‘How flutter renders widgets’이라는 영상을 보게 되었습니다. 플러터가 내부적으로 어떻게 위젯 트리를 관리하는지 알아볼 수 있었고, 상당히 흥미로웠습니다.

How Flutter renders widgets 발표 자료

모든 것은 위젯이다

플러터는 모두 위젯으로 이루어져 있습니다. 아래의 플러터 코드는 어떤 UI를 구성할까요? 한 번 머릿속으로 상상해보시길 바랍니다.

아마도 왼쪽과 같은 화면을 상상하셨을 것입니다. UI의 결과를 상상할 때 어떤 생각을 거치셨나요? 저는 이렇게 생각했습니다.

음…. Center의 자식으로 Text라는 위젯이 존재하기 때문에 아마 화면의 중앙에 ‘Hello’를 출력하지 않을까?

여기서 알 수 있듯이 플러터의 위젯은 부모, 자식 관계를 맺고 있습니다. Column, Row와 같은 위젯은 여러 개의 자식을 가질 수 있습니다. 또 자식을 포함하지 않는 위젯도 존재합니다.

위젯은 불변성을 가진다

A widget is an immutable description of part of a user interface.

플러터의 공식 문서에서 위와 같이 말합니다. 플러터의 모든 위젯은 불변성을 가집니다.

플러터에서 Widget을 구현한 코드를 살펴보면 immutable 어노테이션을 확인할 수 있습니다 그리고 모든 속성은 final 키워드를 통해 변경할 수 없도록 강제했습니다.

하지만 UI는 가변적입니다. 유저의 수많은 상호작용을 통해 데이터가 변경될 것이고, UI는 반드시 업데이트되어야 합니다. 플러터는 어떻게 UI를 업데이트할까요?

정답은 정말 간단합니다. 위젯은 불변하지만, 위젯 트리는 변경 가능합니다. 위젯 트리에서 일부를 들어내고, 다른 위젯 구성으로 교체할 수 있습니다. 하지만 저희는 위젯의 일부를 변경하기 위해 위젯 트리 전체를 다시 빌드하는 것은 원하지 않습니다.

그럼 플러터는 어떻게 위젯 트리를 관리하는지 알아봅시다.

플러터의 3가지 트리

플러터에선 하나의 트리를 통해 위젯을 관리하는 것처럼 보입니다. 하지만 실제로는 3가지의 트리로 구성되어있습니다.

Widget

위젯은 속성에 대한 정보를 포함합니다. 예로 들어서 fontSize, text, style 등이 위젯의 속성에 속합니다.

Element

Element는 부모, 자식 관계에 대한 정보를 포함합니다. 그 말은 위젯의 생성과 파괴에 대한 생명주기를 관리합니다.

여기서 가장 중요한 부분은 부모, 자식 관계입니다. 만약 Element가 없다면 상위 혹은 하위 위젯에 대한 정보를 얻을 수 없고, 그로 인해서 위젯 트리를 변경해 UI를 업데이트하는 것이 힘들어질 것입니다.

Render Object

Render Object 위젯의 크기, 레이아웃을 포함합니다. 어떻게 화면에 그릴지에 대한 로직을 가지고 있습니다.

3가지 트리는 어떻게 구성될까?

간단하게 Text 위젯만 존재하는 앱을 예시로 들어보겠습니다. 플러터는 내부적으로 다음과 같이 동작합니다.

  1. Widget에서는 LeafRenderObjectElement를 생성합니다.
  2. Element는 Widget에 RenderObject 생성을 요구하며, 위젯은 RenderObject를 생성하며 렌더링을 위한 모든 정보를 넘겨줍니다. 렌더링을 위해선 위젯의 속성이 필요하기 때문입니다.

아직은 모든 것이 복잡해 보입니다. Text 하나를 렌더링하기 위해서 너무 많은 작업을 실행하는 느낌입니다. 하지만 마법은 위젯 트리에 변경이 일어날 때 빛을 발합니다.

UI가 변경되는 앱

버튼을 클릭하기 전엔 ‘Before’, 클릭 후엔 ‘After’라는 Text를 출력하는 앱을 제작해보았습니다. 그리고 플러터 DevTool을 사용하여 위젯에 대한 정보를 트레킹해보았습니다.

많은 정보를 확인할 수 있지만, 표시한 부분에 집중을 해야 합니다. 위에 표시된 부분은 RenderObject의 고유 ID입니다. 하지만 Before와 After 모두 고유 ID가 동일한 것을 확인할 수 있습니다. 그 말은 같은 RenderObject라는 것입니다.

하지만 Size와 같은 내부 내용은 변경된 것을 확인할 수 있습니다. 다시 한번 내부적으로 어떤 일이 일어나는지 확인해봅시다.

버튼이 클릭 되었을 때 플러터는 어떻게 대응할까요? 저희는 이미 Text를 위한 Element와 RenderObject를 생성했습니다. 다시 생성되는 것을 원하지 않습니다. 그렇기 때문에 플러터는 내부적으로 canUpdate() 함수를 포함합니다.

static bool canUpdate(Widget oldWidget, Widget newWidget) {
return oldWidget.runtimeType== newWidget.runtimeType && oldWidget.key == newWidget.key;
}

key는 현재 잠시 미뤄놓고, 중요한 것은 rumtimeType을 비교하는 것입니다. 두 타입 모두 Text로 공통적이기 때문에 위젯은 새로운 Element와 RenderObject를 생성하지 않습니다.

위젯을 변경했지만 RenderObject는 아직도 이전 위젯 렌더링을 위한 정보를 가지고 있습니다. 아직 before를 출력하는 정보를 가지고 있는 것이죠. 그래서 새로운 RenderObject를 생성하지 않고, 정보를 업데이트하는 updateRenderObject()를 사용합니다.

이제 성공적으로 UI를 업데이트했습니다.

결론

플러터 팀은 1초에 60프레임을 목표로 큰 노력을 기울이고 있습니다. 그리고 그 목표는 성공적으로 이루어지고 있습니다. 그것이 가능했던 이유 중 하나는 위에서 다룬 것처럼 자원을 최대한 재활용하는 것으로 생각합니다

--

--