개요
컴포저블의 수명 주기는 기존 뷰(View) 기반 시스템과는 다르다. 이번 글에서 컴포저블 수명 주기에 대해 정리해보려고 한다.
1. 컴포저블 수명주기 개요
컴포지션은 앱의 UI를 설명하고 컴포저블을 실행하여 생성된다. 컴포지션은 UI를 기술하는 컴포저블의 트리 구조이다.
초기 컴포지션시 처음으로 컴포저블을 실행할 때, 컴포지션에서 UI를 기술하기 위해 호출하는 컴포저블을 추적합니다. 그 다음 앱의 상태가 변경되면 리컴포지션을 예약합니다.
초기 컴포지션, 리컴포지션의 용어가 나오는데 예전에도 정리했지만 한 번 더 정리해보자!
컴포지션
-> 컴포저블이 처음으로 호출되어 UI가 그려지는 시점이다.
-> 컴포지션은 UI 계층 구조를 생성하는 과정으로, 컴포저블 함수가 처음 실행되면서 트리를 구성하게 된다.
리컴포지션
-> 컴포저블 내 상태가 변경되면 재구성 단계가 발생한다.
-> 이때 전체 UI를 다시 그리는 것이 아니라, 상태 변경에 영향을 받는 부분만 선택적으로 변경된다.
즉 위의 그림과 같이 컴포저블은 컴포지션을 시작하고, 0회 이상 재구성되고 컴포지션을 종료한다.
리컴포지션은 State<T> 객체가 변경되면 트리거되는데, 컴포즈에서는 이러한 객체를 추적하고 컴포지션에서 특정 State<T>를 읽는 모든 컴포저블 중 건너뛸 수 없는 모든 컴포저블을 실행한다.
컴포저블이 호출될 때 컴포지션에 여러 인스턴스가 배치되는데, 컴포지션의 각 호출에 자체 수명주기가 존재한다.
@Composable
fun MyComposable() {
Column {
Text("Hello")
Text("World")
}
}
MyComposable이 호출되면, 컴포지션에 여러 인스턴스가 배치되며 아래와 같이 트리 구조가 만들어진다.
2. 컴포지션 내 컴포저블 분석
컴포지션 내 컴포저블의 객체는 호출 사이트로 식별된다.
-> 여기서 호출 사이트란?
(컴포저블이 호출되는 소스 코드 위치이다! 위의 MyComposable 코드에서 두 개의 Text 컴포저블이 있다. 같은 Text 함수를 호출하고 있지만, 첫 번째 Text("Hello")는 첫 번째 줄에, 두 번째 Text("World")는 두 번째 줄에 호출된다. 컴포즈에서는 각 호출이 발생한 위치를 기억하고, 식별자처럼 사용한다고 한다.)
< 리컴포지션 시 컴포저블이 이전 컴포지션에서 호출한 것과 다른 컴포저블을 호출하는 경우 컴포즈는 호출되거나 호출되지 않은 컴포저블을 구분해 두 컴포지션 모두에서 호출된 컴포저블의 경우 입력이 변경되지 않았다면 리컴포지션 하지 않는다. >
위에서 건너 뛴다는 내용이 이 내용에 해당한다.
@Composable
fun LoginScreen(showError: Boolean) {
if (showError) {
LoginError()
}
LoginInput() // This call site affects where LoginInput is placed in Composition
}
@Composable
fun LoginInput() { /* ... */ }
@Composable
fun LoginError() { /* ... */ }
LoginScreen의 경우 LoginError 컴포저블을 조건부로 호출하고, 항상 LoginInput 컴포저블을 호출한다.
LoginInput이 첫 번째로 호출되고, 다시 두 번째로 호출되었지만 LoginInput에는 변경된 매개변수가 없기 때문에 컴포즈가 LoginInput 호출을 건너뛰어 재구성하지 않는다!!
스마트 리컴포지션에 도움이 되는 정보
지금까지의 내용. 즉, LogiScreen의 경우 스마트 리컴포지션이라는 것을 보여주는 하나의 사례이다.
스마트 리컴포지션의 핵심은 컴포즈가 변경된 상태만을 추적하고, 그 상태에 의존하는 컴포저블만 리컴포지션 한다는 것이다.
이를 통해 불필요한 UI 업데이트를 피하며 성능을 극대화한다.
또 다른 코드를 살펴보자.
@Composable
fun MoviesScreen(movies: List<Movie>) {
Column {
for (movie in movies) {
// MovieOverview composables are placed in Composition given its
// index position in the for loop
MovieOverview(movie)
}
}
}
위의 코드에서 컴포즈는 호출 사이트가 아닌 실행 순서를 사용해 컴포지션에서 객체를 구분한다. 새로운 movie가 목록의 하단에 추가된 경우에 그 전에 추가되었던 객체는 변경되지 않았기 때문에 컴포지션에 이미 있던 객체를 재사용한다.
그렇다면, 상단 또는 가운데에 항목을 추가하거나 삭제하는 경우는 어떻게 될까??
이 경우 위치가 변경된 모든 MovieOverView에 대해 리컴포지션이 발생한다.
이때의 성능을 높이기 위해서는 key 컴포저블을 식별하여 리컴포지션을 효과적으로 진행할 수 있다.
@Composable
fun MoviesScreenWithKey(movies: List<Movie>) {
Column {
for (movie in movies) {
key(movie.id) { // Unique ID for this movie
MovieOverview(movie)
}
}
}
}
movie의 id를 key 컴포저블을 통해 생성된 객체에 대해 리컴포지션이 발생하지 않도록 제한할 수 있다.
입력이 변경되지 않은 경우 건너뛰기
여기서 Stable 개념에 대해 나온다.
Stable은 컴포저블 함수가 리컴포지션 될 때, 컴포즈가 객체의 상태를 적절하게 추적하여 불필요한 리컴포지션을 방지하도록 하는데 도움을 준다.
Stable : 불변성을 보장하거나 그 객체가 변하지 않음을 나타낸다!
컴포즈에서는 객체가 Stable로 가준될 경우, 그 객체에 대해 더 이상 리컴포지션이 필요하지 않다고 판단하고, 객체와 관련된 UI를 다시 그리지 않는다.
Unstable : 상태를 불안정하게 변경하거나 추적할 수 없는 상태. 이 경우에 컴포즈는 매번 객체가 변경되었는지 감지하고 UI를 리컴포지션한다.
data class User(val name: String)
@Composable
fun UserProfile(user: User) {
Text(text = "User name: ${user.name}")
}
위의 User 클래스는 Unstable하기 때문에, User 객체가 변경되지 않았다 하더라도 Compose는 안전을 위해 User 클래스가 있는 UI를 다시 리컴포지션한다.
@Stable 어노테이션 없어도 되는 경우
-> 모든 원시값 유형 : Boolean, Int, Long, Float, Char 등
-> 문자열
-> 모든 함수 유형(람다)
Stable 과 Immutable의 차이
Stable은 반드시 불변하지는 않다. 즉, Stable 객체는 변경이 가능하지만, 컴포즈에서 변경에 대해 잘 감지하고 적절히 처리할 수 있다고 한다. 반면 Immutable 객체는 아예 상태가 변하지 않기 때문에 리컴포지션이 필요하지 않다.
@Stable
data class Product(val name: String, val price: Int)
Product 클래스에 @Stable을 붙인다면, 컴포즈에서 이 객체가 내부적으로 변하지 않거나 변경이 드물다고 간주하고, 상태 변화가 없을 경우 재구성하지 않게 된다.
참고
https://developer.android.com/develop/ui/compose/lifecycle?hl=ko
'Android > Android Compose' 카테고리의 다른 글
[Android] Compose - (6) 부수효과 첫 번째 (0) | 2024.11.12 |
---|---|
[Android] Compose - (5) Navigation (1) | 2024.10.23 |
[Android] Compose - (3) 상태를 호이스팅할 대상 위치 (0) | 2024.08.19 |
[Android] Compose UI - (3) 페이저(Pager) (1) | 2024.08.15 |
[Android] Compose UI - (2) 수정자(Modifier) (0) | 2024.08.14 |