훌륭한 개발자가 되기 위하여
Jetpack Compose Stability 본문
Jetpack Compose의 3단계
Composition(구성) : Composable 함수를 실행하여 UI 트리를 생성하고, 상태를 저장 및 관리한다.
- @Composable 함수가 실행되어 UI 트리를 생성
- 상태를 관리하기 위해 여러 메모리 슬롯을 할당하며, remember와 같은 메커니즘을 통해 상태를 저장하고, 상태 변경 시 기존 값을 활용하여 해당 부분만 Recomposition함
Layout(레이아웃) : 트리를 작동하고 UI의 각 부분을 측정하여 화면에 배치한다.
- 생성된 UI 트리를 기반으로 각 요소를 측정하고 부모-자식 관계에 따라 화면에 배치
- 이 단계에서 Component의 크기와 위치가 결정
Drawing(그리기) : UI 트리가 작동하고 모든 요소를 랜더링한다.
Recomposition은 매개변수나 상태의 변화에 반응하여 Composable 함수를 다시 실행한다. 이 때 Composition(구성) 단계부터 시작되며, 변경되지 않은 UI는 유지하고 필요한 부분만 갱신하여 성능을 최적화한다. 하지만 전체 UI 트리와 Component들을 다시 구성하는 것은 앱의 성능에 직접적으로 좋지 않은 영향을 미친다.
Compose 안전성
안정성 : 객체가 변경되지 않았음을 Compose가 신뢰할 수 있는지를 나타내는 성질
Compose 컴파일러는 Composable 함수의 매개변수를 안정(stable)과 불안정(unstable) 상태로 분류한다. Compose는 Composable의 매개변수의 안정성을 사용하여 Recomposition을 Skip할 수 있는지를 결정한다.
Stable vs Unstable
Stable한 객체
- 모든 public property가 val로 정의된 경우
- @Stable, @Immutable와 같은 stability 어노테이션을 사용하여 명시적으로 표시한 경우
- Compose가 내부적으로 stability를 확실히 아는 자료형(기본 타입, immutable 리스트 등)
data class Event(
val id: Int,
val name: String,
val content: String,
val eventDate: String,
)
Unstable한 객체
- 하나 이상의 내부적으로 변경 가능하거나 본질적으로 불안정한 public property를 갖는 경우
- var로 선언된 프로퍼티가 있는 경우
Kotlin의 collection에서 제공하는 List, Map 등을 포함한 모든 인터페이스와 Any 타입과 같은 추상 클래스는 컴파일 시 구현을 예측할 수 없어 불안정한 것으로 간주한다.
data class Event(
val id: Int,
var name: String,
val content: List<String>,
val eventDate: String,
)
Smart Recomposition
compose는 상태 변경이 발생했을 때, Smart Recomposition을 통해 변경된 부분만 효율적으로 다시 그린다.
안정성에 따른 결정
매개변수가 안정적이고 값의 변화가 없는 경우 Recomposition을 Skip한다. 반면에, 매개변수가 불안정하거나, 안정하지만 값의 변화가 있는 경우 Recomposition을 발생시킨다.
동등성 검사
Data class에 구현되어 있는 equal 함수를 통해 결정되며, 결과가 false인 경우에 Recomposition이 발생된다.
Composable 함수의 유형 추론
Restartable : 재실행 가능한
Compose 런타임이 입력의 변화를 감지하면, 새로운 입력을 반영하기 위해 함수들 다시 시작한다. 대부분의 함수는 기본적으로 restartable로 간주된다.
-> 입력이나 상태가 변경될 때마다 Compose 런타임이 Composable 함수에 대해 recomposition을 트리거할 수 있다는 의미이다.
Skippable : Recomposition 건너뛸 수 있음
Smart Recomposition에 의해 설정된 조건에서 recomposition 과정을 Skip 할 수 있다. 특정 상황에 따라 recomposition을 Skip하고 UI 성능을 향상시킬 수 있다. 추가로 루트 Composable 함수의 recomposition을 Skip 하는 경우, Compose는 계층 구조의 하위 함수를 호출할 필요성을 효과적으로 제거하여 recomposition 과정을 간소화 할 수 있다.
안정성 문제 진단 방법
Compose Compiler Metrics
Compose Compiler Metrics를 통해 Composable 함수나 객체가 Stable한지 알 수 있고, 이를 통해 Compose 코드의 개선점을 파악할 수 있다.
Compose Compiler Metrics을 생성하려면 아래와 같이 루트 모듈의 build.gradle 파일에 컴파일러 옵션을 추가하면 된다.
subprojects {
tasks.withType<org.jetbrains.kotlin.gradle.tasks.KotlinCompile>().all {
kotlinOptions.freeCompilerArgs += listOf(
"-P",
"plugin:androidx.compose.compiler.plugins.kotlin:reportsDestination=" +
project.buildDir.absolutePath + "/compose_metrics"
)
kotlinOptions.freeCompilerArgs += listOf(
"-P",
"plugin:androidx.compose.compiler.plugins.kotlin:metricsDestination=" +
project.buildDir.absolutePath + "/compose_metrics"
)
}
}
안정성 어노테이션
Immutable
@Immutable은 클래스의 모든 public property가 초기 생성 후 변경되지 않는 불변(immutable)임을 Compose 컴파일러에게 알린다.
조건
- 모든 public property에 val 키워드 사용
- Custom setter를 피하고 public property가 가변성을 지원하지 않도록 함
- 본질적으로 불변으로 간주되어도 되는지 확인해야함. 즉, collection을 사용하는 경우 절대 수정이 발생하지 않는다는 확신이 서는 경우 사용해야함
활용
Kotlin 데이터 클래스 : 비즈니스 모델
효과
불변성 규칙을 준수하는 클래스에 효과적이며, 불필요한 recomposition을 건너뛰어 성능 향상에 중요한 역할을 함
Stable
@Stable은 @Immutable 보다 느슨한 약속, 변경 가능한 데이터에 대해 상태 변화가 예측 가능하고 안정적임을 알린다.
public property는 불변이지만 클래스 자체는 안정적으로 간주될 수 없는 클래서에 적합하다.
활용
인터페이스 : 여러 구현 가능성을 제공하고 내부적으로 변경 가능한 상태
효과
동일한 객체를 다시 전달받을 경우 내부 변화를 가정하지 않고 recomposition을 건너뛸 수 있다.
NonRestartableComposable
@NonRestartableComposable은 Compose 컴파일러에게 Composable 함수가 호출 매개변수의 변경으로 인한 recomposition 프로세스 중에 자동으로 다시 시작되지 않아야 함을 알림
이 어노테이션을 적용하면 함수를 다시 시작하지 않고 매개변수를 업데이트하도록 지시하여 내부 상태와 진행 중인 사이드 이펙트를 유지할 수 있음
대표적인 예는 LaunchedEffect이다. LaunchedEffect에서 이 어노테이션을 사용하여 불필요하게 이펙트가 다시 시작되지 않도록 한다.
불필요한 재시작을 방지함으로써 사이드 이펙트나 recomposition을 통해 지속되어야 하는 상태와 관련된 경우에 효율성과 일관성을 유지하는데 도움이 된다. 그러나 @NonRestartableComposable 어노테이션을 무분별하게 사용해서는 안된다.
Composable 함수 안정화 시키기
Immutable Collections
Kotlin Collection의 내부 요소가 수정을 허용하지 않는데도 Unstable하다고 간주해서 이에 대한 의문을 가질 수 있다.
internal var mutableEventList: MutableList<Event> = mutableListOf()
val eventList: List<Event> = mutableEventList
두번째 줄의 eventList는 List로 선언되어 수정을 허용하지 않는다. 하지만 이 List는 mutableListOf()로 인스턴스화되어 수정 가능한 타입의 리스트일 수 있다. List 인터페이스 자체가 수정을 제한하지만, 그 구현체는 수정 가능할 수 있다는 것을 의미한다. 그래서 Compose 컴파일러는 이러한 인스턴스를 불안정한 것으로 처리하여 정확한 동작을 보장해야 한다.
Android 공식 문서에서는 컬렉션 매개변수의 안정성을 보장하기 위해 kotlinx.collections.immutable 라이브러리를 사용하는 것을 권장한다. kotlinx.collections.immutable 라이브러리는 ImmutableList와 ImmutableSet과 같은 다양한 컬렉션을 제공하며 읽기 전용이며, 생성 후에는 수정이 발생할 수 없다.
Lambda
Compose 컴파일러가 Kotlin의 람다 표현식을 처리할 때는 조금 독특한 방식을 취한다. Compose 컴파일러는 IR(Intermediate Representation) 변환을 통해 개발자가 작성한 소스 코드를 수정한다. Compose 컴파일러는 람다 표현식이 값을 캡처하는지 여부에 따라 처리한다. 여기서 값을 캡처한다는 것은 람다 표현식이 외부의 변수에 의존한다는 것을 의미한다.
값을 캡처하지 않는 경우람다 매개변수가 어떠한 값도 캡처하지 않으면, Kotlin은 람다를 싱글톤으로 취급하여 불필요한 할당을 최소화한다.즉, 새로운 인스턴스를 만들지 않는다.
modifier.clickable {
Log.d("Log", "어떠한 값도 캡처하지 않는다.")
}
값을 캡처하는 경우실행 결과는 캡처된 값에 따라 달라질 수 있다. 이를 해결하기 위해 Compose 컴파일러는 람다를 remember 함수 호출 내에 캡슐화 한다. 캡처된 값은 remember의 key 매개변수로 사용되어, 캡처된 값이 변경될 때마다 람다가 적절하게 다시 호출되도록 보장한다.
var sum = 0
numbers.filter { it % 2 == 0 }.forEach {
sum += it
}
따라서 람다가 값을 캡처하는 여부와 상관없이 Compose Compiler Metrics 상으로는 Composable 함수 내에서 안정적인 것으로 표기된다.
하지만, 실제 캡처하는 값이 unstable하다면 해당 Composable 함수가 skippable임에도 recomposition이 수행될 수 있다. 아래 함수는 Any 타입의 매개변수를 받기 때문에 다양한 값을 포괄할 수 있기에 불안정한 것으로 간주된다.
@Composable
fun TestComposable(parameter: Any?) {
..
}
하지만, 람다 표현식을 사용하여 값을 제공하면 Compose 컴파일러는 일단 람다 매개변수를 안정적인 것으로 처리한다.
@Composable
fun TestComposable(parameter: () -> Any?) {
..
}
여기서 람다식이 어떠한 값을 캡처하는지에 따라 Composable 함수가 skippable 함에도 recomposition 발생 여부가 결정된다. 즉, 람다로 값을 전달하면, 값의 캡처 여부에 따라 stable/unstable를 처리하기 때문에 외부 상태를 캡처하면 불안정할 수 있다. 이를 해결하는 방법은 람다식 자체를 remember를 사용하여 감싸고 안정적인 것으로 만드는 것이다.
@Composable
fun TestComposable(parameter: () -> Any?) {
val stableParameter = remember(parameter) { parameter() } // `remember`로 람다 값 안정화
Text(text = "Received: $stableParameter")
}
Wrapper Class
이 방법은 불안정한 클래스에 대해 wrapper 클래스를 생성하는 것이다.
@Immutable
data class ImmutableEventList(
val event: List<Event>
)
@Composable
fun EventInfo(
modifier: Modifier,
eventList: ImmutableEventList,
)
wrapper 클래스를 Composable 함수의 매개변수 타입으로 사용하여 skippable 하게 만들 수 있다.
Stability Configuration File
Compose 컴파일러 버전 1.5.5부터는 컴파일 시 안정된 것으로 간주할 클래스를 configuration file에 정의하여 Compose 컴파일러에 의해 안정적인 것으로 인식되게끔 할 수 있다. 이를 통해 LocalDateTime과 같은 표준 라이브러리 클래스처럼 사용자가 제어하지 않는 클래스도 안정된 것으로 간주할 수 있다.
composeCompiler {
stabilityConfigurationFile = rootProject.layout.projectDirectory.file("stability_config.conf")
}
'안드로이드' 카테고리의 다른 글
네트워크 상태 읽기 (0) | 2025.02.20 |
---|---|
공유 상태를 사용하는 코루틴의 문제와 해결책, 코루틴의 동작 방식 (0) | 2025.01.15 |
코루틴 이해하기 (0) | 2025.01.14 |
일시 중단 함수와 코루틴 (0) | 2025.01.13 |
코루틴 예외 처리 (0) | 2025.01.12 |