플러터는 OS에 관계 없이 일관된 UI를 보여주기 위해 만들어진 크로스 플랫폼 프레임워크입니다.
이번에는 플러터 앱의 UI가 화면에 어떻게 랜더링되는지에 대해서 알아보겠습니다.
해당 포스트의 내용은 개인적인 공부를 위한 것으로, 틀린 내용이 있을 수 있습니다
플랫폼별 렌더링 과정
Native - Android
View
- 기본적으로 View라는 트리 구조로 UI를 구성함
- 각 View는 UI가 화면에 어떻게 보일지를 정의하는 역할
SurfaceFlinger
- View로 정의된 UI를 GPU에 전달하는 역할
- 어디에 뭐를 넣어야 하는지 지시하는 역할로 보면 쉽다!
GPU + Hardward Composer
- GPU는 SurfaceFlinger의 명령을 받아 버튼 등을 실제 픽셀로 칠한다.
- Hardward Composer는 GPU가 그림을 효율적으로 그릴 수 있도록 돕는 도우미 역할
Choreographer
- 보통 화면은 1초에 60장 그려지면 부드럽게 보인다.
- 앱이 위 리듬에 맞춰 화면을 그려 부드럽게 보일 수 있게 조율하는 역할
Native - iOS
UIView
- iOS 앱은 UIView로 이루어진 트리 구조
Core Animation
- 화면을 더 부드럽고 빠르게 보여주기 위해 사용한다.
- 미리 애니메이션 동작(위치, 크기, 색상 변화 등)을 계산해놓음.
- 따라서 애니메이션이 시작되면 CPU가 계속 연산할 필요 없이 GPU가 바로 실행할 수 있음!
CALayer
- UIView 뒤에는 CALayer라는 꾸미기 레이어가 붙어있다.
- 그림자, 모서리, 배경색 등 시각적 효과 담당
- (UIView → 구조) , (CALayer → 꾸미기 & 효과) 라고 보면 쉽다!
- 계산된 결과는 Metal이나 OpenGL 같은 그래픽 엔진을 통해 GPU에 전달된다.
WindowServer
- 여러 앱이 하나의 화면에 뜨는 경우가 생길 수 있음! (알림창 등)
- 여러 화면 요소를 한 장의 화면으로 합쳐서 보여주는 역할
JavaScript
- React.js 기반으로 화면을 어떻게 보여줄지 적습니다.
- React의 문법으로 모바일 화면을 만들 수 있게 확장한 것!
Bridge
- 당연히 JS는 모바일 화면을 직접 그릴 수 없음
- Bridge를 건너서 네이티브 언어로 번역 (안드로이드야 버튼 하나 만들어 줘...)
Virtual DOM
- React.js와 같이 Virtual DOM이 변경을 감지한 후 업데이트
- (웹과의 차이) 변경 감지의 흐름: Virtual DOM → Native Bridge → Native View
Native UI 그리기
- JS에서 시작되어 번역된 내용은 최종적으로 UIView(iOS), View(Andriod)가 된다.
- 이걸 GPU가 그려서 우리 눈에 보이게 됨.
JavaScript 엔진
플러터의 렌더링에서는 세 개의 핵심 트리 구조가 존재합니다.

이 세 트리는 서로 연결되어,
설계도(Widget Tree) → 상태관리(Element Tree) → 실제 그리기(Render Tree)의 과정을 담당합니다.
- Flutter의 UI는 전부 다 위젯으로 구성됩니다.
- UI의 설계도 역할을 한다고 보면 쉽습니다..
Element
- Widget은 설계도의 역할을 하기 때문에, 실제 화면에 배치될 때는 Element로 변환됩니다.
- 각 Element는 Widget의 인스턴스를 관리하면서 변동사항을 추적합니다.
RenderObject
- Element를 통해 만들어진 구조를 실제로 그리는 역할을 수행합니다.
- 크기 계산(Layout)과 화면에 그리기(Painting)을 수행
Skia / Impeller
- Flutter는 기본 View 시스템을 이용하지 않고 자체 엔진을 사용
- Skia: 2D 그래픽을 빠르게 처리, 안정적
- Impeller: 부드러운 애니메이션, GPU효율 ⬆️
Flutter의 렌더링 과정
파이프라인

- User input
- 터치, 스와이프와 같은 사용자의 입력
- UI 업데이트의 트리거가 됩니다.
- Animation
- 애니메이션 컨트롤러가 시간에 따라 상태를 갱신합니다.
- 매 프레임마다 애니메이션 값(위치 등)이 바뀌면 다음 단계들이 실행됩니다.
- Build
- build() 가 호출되면 새로운 Widget Tree가 만들어집니다.
- 기본적으로 Widget은 immutable입니다.
- Layout
- 부모가 자식에세 constraints를 내려줌 → 자식이 constraint 내에서 자신의 크기를 선택 → 부모에게 자신의 크기를 전달
- 위 규칙을 = constraints go down, sizes go up 이라고 합니다.
- 이 과정에서 RenderObject가 performLayout()을 실행해 위치/크기를 결정 → 레이아웃 및 페인팅 책임자 = RenderObject

- Paint
- 레이아웃된 결과를 바탕으로 paint()가 호출되어 그리기 명령을 작성합니다. (아직 픽셀은 아님!)
- Composition
- 여러 UI 요소들을 Layer 단위로 관리
- 어떤 Layer를 composite해서 GPU한테 넘길 건지 결정
- RepaintBoundary로 레이어를 분리하면 특정 부분만 다시 그리게 할 수 있음.*
- Rasterize
- Layer Tree(= 합성된 Layer들, 네 번째 트리)가 GPU로 넘어가서 Skia/Impeller 같은 엔진이 실제 픽셀 명령(삼각형 등)으로 변환하여 화면에 출력.
특징
자체 렌더링 엔진(Skia)
- Flutter는 Android나 iOS의 기존 UI 위젯 시스템을 전혀 사용하지 않음.
- Skia를 통해 하나하나 직접 그린다!
- 플랫폼 일관성 - OS에 구애받지 않고 동일한 UI 구현 가능
레이어 트리
- 위젯 → Element → RenderObject → Layer Tree로 이어지는 구조
- build()호출시 Widget Tree 다시 생성 → 기존 Element Tree와 비교하여 필요한 Element만 업데이트 하는 방식
네이티브 브리지 없음
- React Native처럼 JS → Native UI로 가는 Bridge가 필요 없음.
- Flutter는 Dart 코드 → 바로 엔진으로 연결됨. → Bridge 병목 X
저번 블로그에서 호기롭게 Flutter의 UI 렌더링에 대해서 알아보겠다고 했는데, 생각보다 방대하고 복잡한 개념 + 플랫폼별 렌더링 과정에 분량을 잡아먹혀 자세히 알아보긴 무리였습니다...🙂↕️
다음에는 조금 더 가볍고 재미있는 주제로 찾아오겠습니다.
플러터의 렌더링 구조는 다른 프레임워크와 확인히 다르네요! 트리 구조를 기반으로, 필요한 부분만 업데이트한다면 상당히 효율적으로 렌더링을 진행할 수 있겠어요. 자체 렌더링 엔진을 활용해서 플랫폼 종속성도 없고, 병목현상도 없다니! 최근 앱 개발의 트렌드가 플러터로 이동하고 있는지 알 수 있는 내용이었습니다!