컴포저블의 수명 주기
Compose에서 컴포저블은 UI의 상태와 변경 사항을 효율적으로 관리하기 위해 특정한 수명 주기를 따릅니다.
초기 Composition 시, Compose는 처음으로 컴포저블을 실행하고, UI를 구성하기 위해 호출된 컴포저블을 추적합니다.
앱 상태가 변경되면 Compose는 Recomposition을 예약하여 상태 변경 사항을 반영하고 UI를 업데이트합니다.
- Composition
- 컴포저블 함수가 처음 호출되어 UI 트리를 구성할 때 발생합니다.
- 컴포저블 함수는 컴포지션 과정에서 호출되어 UI 요소를 생성합니다.
- Recomposition
- UI 상태가 변경될 때 컴포저블 함수가 다시 호출되어 UI를 업데이트하는 과정입니다.
- 상태(State)가 변경되면 Compose는 관련된 컴포저블 함수들을 다시 호출하여 UI를 업데이트합니다.
- Decomposition
- 컴포저블 함수가 UI 트리에서 제거될 때 발생합니다.
- 컴포저블이 더 이상 필요하지 않으면 메모리에서 제거됩니다.
Smart Recomposition
기존의 View 시스템에서는 UI를 트리 구조의 그룹 형태로 관리합니다.
View와 ViewGroup이 트리 구조를 형성하여 부모-자식 관계로 구성됩니다.
ViewGroup 객체 전체를 호출하여 UI를 그리기 때문에 View의 깊이가 깊어지면 성능이 떨어지게 됩니다.
하지만 Compose에서는 트리 구조의 깊이가 존재하지 않고, UI를 선언적으로 정의합니다.
상태가 변경되면 Compose는 변경된 부분만 다시 렌더링하여 UI를 업데이트합니다.
이 과정에서 Skippable, Restartable 등의 개념을 통해 Smart Recomposition을 수행합니다.
Smart Recomposition은 컴포저블 함수가 상태 변경에 따라 UI를 효율적으로 업데이트하는 방식입니다.
이를 통해 불필요한 Recomposition을 최소화하여 성능을 최적화할 수 있습니다.
Compose의 안정성
Compose에서는 컴파일 시점에 객체들의 안정성(Stability)을 확인합니다.
어떤 컴포저블 함수의 모든 인자가 안정적(Stable)이라면, 그 컴포저블 함수는 생략 가능(Skippable)하다고 간주됩니다. Recomposition이 발생했을 때, 어떤 컴포저블 함수의 모든 인자가 안정적(Stable)이고 그 값이 전혀 변하지 않았다면 Recomposition을 생략합니다.
이것이 Smart Recomposition의 기본 원리입니다.
컴포저블 보고서
다음의 컴포저블 보고서를 보면 SnackCollection 함수는 모든 매개변수가 stable이므로, 이 함수는 상태가 변하지 않을 때 Recomposition을 생략할 수 있습니다.
하지만 HighlightedSnacks 함수는 snacks 매개변수가 unstable이므로, Recomposition이 항상 필요하여 Smart Recomposition의 혜택을 받을 수 없습니다.
restartable skippable scheme("[androidx.compose.ui.UiComposable]") fun SnackCollection(
stable snackCollection: SnackCollection
stable onSnackClick: Function1<Long, Unit>
stable modifier: Modifier? = @static Companion
stable index: Int = @static 0
stable highlight: Boolean = @static true
)
restartable scheme("[androidx.compose.ui.UiComposable]") fun HighlightedSnacks(
stable index: Int
unstable snacks: List<Snack>
stable onSnackClick: Function1<Long, Unit>
stable modifier: Modifier? = @static Companion
)
- restartable : 컴포저블 함수의 인자가 변경되거나 상태가 변경될 때마다 해당 함수는 다시 실행되어야 함
- skippable : 컴포저블 함수의 모든 인자가 stable하고, 그 인자가 변경되지 않은 경우에는 Recomposition을 생략할 수 있음
- stable : 객체나 인자가 stable이라면, 그 값이 변경되지 않거나 변경되더라도 UI에 영향을 미치지 않음
- 기본적인 Primitive 타입과 이를 활용한 함수 타입 등이 해당됨 (ex. String, () -> String)
- unstable : 객체나 인자가 unstable이라면, 그 값이 변경될 때마다 UI가 변경될 수 있음
- Smart Recomposition에서 제외가 된다는 뜻이며, Recomposition의 대상에 무조건적으로 포함됨
Stable Marker
Compose에서는 unstable한 타입들을 안정적으로 사용할 수 있도록 도와주는 기능이 있습니다.
이는 Stable Marker라고 하며, @Stable과 @Immutable이 해당됩니다.
하지만 주의할 점은 Stable Marker는 컴파일러에게 약속하는 기능이지, 실제로 검사를 수행하지 않기 때문에 실제로 unstable한 경우에는 예상치 못한 결과가 발생할 수도 있습니다.
@Stable
@Stable 어노테이션은 객체가 변경될 가능성이 있지만, 변경이 UI에 영향을 미치지 않거나 변경이 발생해도 효율적으로 감지할 수 있을 때 사용됩니다.
@Stable
class User(var name: String)
- name 필드는 var로 선언되어있기 때문에 원래는 unstable하지만, 해당 클래스가 @Stable 어노테이션으로 표시되어 있기 때문에 Compose는 해당 객체를 안정적으로 관리한다고 판단합니다.
- 따라서, Compose는 해당 객체가 변경되지 않으면 Recomposition을 생략할 수 있습니다.
@Immutable
@Immutable 어노테이션은 객체가 완전히 불변임을 나타내며, 객체의 모든 속성이 초기화된 후에는 변경되지 않는 경우 사용됩니다.
@Immutable
data class Address(val street: String, val city: String)
- Address 클래스의 모든 속성이 val로 선언되어 있기 때문에 초기화 후에는 변경되지 않습니다.
- 따라서, Compose는 해당 객체를 사용할 때 Recomposition을 생략할 수 있습니다.
Compose 성능 최적화 기법
상태 읽기 연기
상태 변경 시 람다를 통해 값을 갱신함으로써 Composition 단계 이후에 Layout 단계에서 값을 읽게 되어 Recomposition이 일어나지 않도록 합니다.
@Composable
fun MyButton(onClickAction: (String) -> Unit) {
Button(onClick = { onClickAction("Button clicked") }) {
Text("Click!")
}
}
@Composable
fun MyScreen() {
var message by remember { mutableStateOf("") }
MyButton { newMessage ->
// 클릭 이벤트 핸들러에서 상태를 변경하는 대신에,
// 클릭 이벤트를 통해 새로운 메시지를 전달하여 상태 변경을 연기함
message = newMessage
}
Text(text = message)
}
Key 사용
반복되는 컴포저블 요소들에 고유한 key를 할당하여, Compose가 각 항목의 변경 사항을 추적할 수 있도록 합니다.
이를 통해 변경되지 않은 항목에 대해서 불필요한 Recomposition을 피할 수 있습니다.
@Composable
fun MemoList(memoList: List<Memo>) {
LazyColumn {
items(
items = memoList,
key = { it.id }
) {
...
}
}
}
- Column일 경우에는 "Column { for (memo in memoList) { key(memo.id) { ... } } }"으로 사용합니다.
계산 최소화
이 기법을 사용하면 불필요한 계산을 최소화하여 성능을 향상시킬 수 있습니다.
위의 MemoList()에서 "items(items = memoList.sortedBy { it.id }, key = {it.id} ) { ... }"를 수행하면 매번 UI를 그릴 때마다 memoList를 정렬하게 됩니다.
이를 해결하기 위해 remember를 사용하여 리스트를 한 번만 정렬하면, memoList가 변경되지 않는 한 다시 정렬하지 않도록 하여 불필요한 Recomposition을 피할 수 있습니다.
@Composable
fun MyScreen() {
val memoList = remember { memos.sortedBy { it.id }.toMutableStateList() }
MemoList(memoList)
...
}
역방향 쓰기 금지
Compose에서는 데이터를 단방향으로 흐르도록 유지하는 것이 좋습니다.
따라서, UI 요소가 특정 상태를 변경할 때마다 다른 상태도 영향을 받게 되는 상황을 피해야 합니다.
이를 위해서는 UI 컴포저블 내부에서 상태를 직접 변경하지 않도록 해야 합니다.
@Composable
fun Counter() {
var count by remember { mutableStateOf(0) }
Button(
onClick = { count++ }
) {
Text("count : $count")
}
}
- Text 뒤에서 "count++"를 수행하면 상태 변경이 지속적으로 일어나면서 무한 Recomposition이 발생합니다.
- 따라서, 상태 변경은 이벤트 핸들러에서만 수행하여 단방향 데이터 흐름을 유지해야 합니다.
'안드로이드 > Compose' 카테고리의 다른 글
[Android] Compose - 아키텍처 (0) | 2024.06.05 |
---|---|
[Android] Compose - 부수 효과 (0) | 2024.05.27 |
[Android] Compose - Animation (0) | 2024.05.25 |
[Android] Compose - ConstraintLayout (0) | 2024.05.17 |
[Android] Compose - Lazy layout (0) | 2024.05.15 |