이번 포스팅은 Compose의 렌더링 단계를 알아보도록 하겠다.
# 렌더링 단계
우선 위 그림은 안드로이드 공식문서에 있는 렌더링 단계의 그림이다.
렌더링 단계는 크게 총 3단계 컴포지션(Composeition), 레이아웃(Layout), 그리기(Drawing)로 나누어져 있다.
이제 하나씩 알아가 보자!
# 1. 컴포지션 (Composeition) 단계 - What 화면에 뭘 보여 줄지
컴포지션 단계에서는 UI 코드를 노드 트리 구조로 변환/만드는 단계이다.
해당 트리를 생성하면서 각 UI 컴포넌트의 계층 구조와 속성들을 파악하고 생성된 트리는 2단계인 레이아웃 단계에서 사용이 된다.
이제 노드 트리 구조를 어떤 식으로 만드는지 아래 예제를 보자!
Column {
Image(
painter = rememberVectorPainter(Icons.Filled.Fastfood),
contentDescription = "Favorite Icon"
)
Row {
Text(text = "내 이름은")
Text(text = "지게")
}
}
위처럼 UI 코드가 있다 이제 해당 코드를 노트 트리 구조로 생성하게 되면 아래 사진과 같아진다.
사진을 분석하자면 코드 최상위에 있는 Column을 기준으로 삼아 순서대로 노드가 만들어진다.
🔴 만약 여기서 Modifier를 사용하게 되면 노드 트리는 아래와 같이 만들어진다.
Image에 modifier.padding이 들어갔다고 가정했을 시 Image노드는 Modifier로 Wrapping 된다.
# 2. 레이아웃 (Layout) 단계 - where 각 요소를 어디에 배치할지
레이아웃 단계는 각 UI 요소의 크기와 위치를 결정하는 중요한 과정이다.
이 단계에서는 컴포지션 단계에서 생성된 UI 트리를 기반으로 각 컴포저블의 측정과 배치가 이루어진다.
레이아웃 단계에서는 아래 3단계에 따라 각 요소를 배치한다.
- 하위 요소 측정 : 노드가 하위 요소(있는 경우)를 측정한다.
- 자체 크기 결정 : 이러한 측정치를 기반으로 노드가 자체 크기를 결정한다.
- 하위 요소 배치 : 각 하위 노드는 노드의 자체 위치를 기준으로 배치된다.
이제 각 단계를 따라서 정확히 어떤 식으로 배치하는지 예제를 통해 알아보자!
최상위 노드인 Column 노드에서부터 위 3단계 알고리즘을 통해 크기를 측정할 것이다.
1. Column 노드는 하위 요소 측정 단계에서 하위 요소가 Image, Row 두 개가 있기 때문에 왼쪽 자식부터 방문한다.
2. 방문한 Image노드는 하위 요소 측정 단계에서 하위 요소가 없으니 2단계인 자체크기 결정 단계에서 본인 크기를 측정한다.
3. Column 노드의 하위 요소 측정이 다 끝나지 않아서 다시 오른쪽 자식인 Row 노드로 방문한다.
4. 방문한 Row 노드는 하위 요소가 Text 노드 2개가 있기 때문에 2개의 Text 노드를 방문한다.
5. 각 Text 노드의 하위 요소가 없으니 2단계인 자체크기 결정 단계에서 본인 크기를 측정한다.
6. 하위 측정값을 사용하여 Row 노드의 자체 크기를 측정하고 Text 노드들을 Row 내에서 가로로 배치한다.
- 너비: 두 Text 노드의 너비의 합
- 높이: Text 노드들 중 가장 큰 높이
7. Row 노드의 크기 측정과 배치가 완료되었으므로 이제 Column 노드로 돌아가서 Column 노드의 자체 크기를 측정하고 배치한다.
1. Image 노드의 크기
2. Row 노드의 크기
이 두 가지 측정값을 사용하여
- 너비: Image와 Row 중 더 큰 너비를 선택
- 높이: Image의 높이 + Row의 높이로 계산
하여 자신의 최종 크기를 측정하고 배치한다.
🔴 이처럼 Compose 런타임은 모든 노드를 측정하고 배치하는 데 UI 트리를 한 번만 통과하면 되므로 성능이 향상된다!
반대로 각 노드가 여러 번 방문되면(리컴포지션, Layout 제약조건 변경 시) 탐색 시간이 기하급수적으로 증가한다.
이제 모든 노드의 크기 측정과 배치가 완료되었으니 그리기(Drawing) 단계로 넘어가 보자!
# 3. 그리기 (Drawing) 단계 - How 각 요소를 화면에 어떻게 나타낼지
그리기 단계에서는 트리가 위에서 아래로 다시 탐색되고 각 노드가 차례로 화면에 그려진다.
일반적으로 기기 화면인 캔버스에 그려진다.
그리기 단계도 순차적으로 예제를 통해 알아가 보자!
2단계인 레이아웃 단계에서 측정과 배치가 완료가 되어있는 UI 트리를 가지고 예제를 진행하겠다.
1. Column 노드부터 시작
- Column은 만약 Modifier 즉 자신의 배경이나 테두리 같은 기본 스타일이 있다면 먼저 그린다.
2. 그 후 자식들을 그리기 위해 왼쪽 자식인 Image 노드부터 방문
- Image 노드는 자신의 위치(레이아웃 단계에서 결정된)에 이미지를 그린다.
- 이미지 리소스를 로드하고 지정된 크기로 렌더링 한다.
(실제로는 짱구 이미지의 세로 크기 + Row 세로 크기만큼 Column의 세로가 길어진다)
3. Row 노드로 넘어가 Modifier 즉 자신의 배경이나 테두리 같은 기본 스타일이 있다면 먼저 그린다.
- 이 예제에서는 없으니 생략
4. 자식 Text 노드들을 순서대로 그린다.
(실제로는 Text의 정확한 가로 사이즈에 맞게 그린다.)
이렇게 트리의 위에서 아래로(top-down) 순회하면서
- 부모가 먼저 자신의 배경을 그리고
- 그 위에 자식 요소들을 순서대로 그리는 방식으로
- Z-order(겹침 순서)가 자연스럽게 유지된다
레이아웃 단계와 마찬가지로 그리기도 한 번의 트리 순회로 모든 작업이 완료된다.
이렇게 컴포즈의 렌더링 단계를 알아봤는데 생각보다 복잡했지만 알고 보니 간단? 한 거 같기도 하다.
렌더링 단계를 알아봄으로써 컴포즈가 왜 렌더링이 xml보다 빠른지 이해했다!
참고
https://developer.android.com/develop/ui/compose/phases?hl=ko
https://velog.io/@lyh990517/Android-jetpack-compose%EC%9D%98-%EB%9E%9C%EB%8D%94%EB%A7%81-%EB%8B%A8%EA%B3%84%EB%A5%BC-%EC%95%8C%EC%95%84%EB%B3%B4%EC%9E%90