Jetpack Compose에서 리컴포지션 줄이기
Jetpack Compose에서 리컴포지션 줄이기
Jetpack Compose는 입력값이 변경된 컴포저블 함수만 리컴포지션(Recomposition)하지만, 이 최적화는 컴파일러가 입력값의 안정성(stability)을 판별할 수 있느냐에 달려 있습니다. 컴파일러가 안정성을 증명하지 못하면 해당 매개변수를 불안정(unstable)으로 분류하고, 실제 값이 바뀌었는지와 무관하게 부모 리컴포지션이 발생할 때마다 해당 함수를 다시 실행합니다. Compose 컴파일러가 안정성을 평가하는 원리와, 불필요한 리컴포지션을 줄이는 기법을 이해하는 것은 고성능 UI를 구축하는 데 필수적입니다. 면접에서도 Compose 성능 관련 질문으로 자주 등장하므로 반드시 숙지하시길 권장합니다.
이번 면접 질문을 통하여 아래 내용들을 학습하실 수 있습니다.
- Compose 컴파일러가 매개변수 안정성을 판별하는 방식과 건너뛰기(skipping)에 미치는 영향을 설명할 수 있습니다.
@Immutable과@Stable어노테이션을 도메인 클래스에 올바르게 적용할 수 있습니다.- 불변 컬렉션(immutable collection)을 사용하여 안정성 추론 실패를 방지할 수 있습니다.
- 람다 캡처 동작이 리컴포지션에 어떻게 영향을 미치는지 설명할 수 있습니다.
- 안정성 설정 파일(stability configuration file)과 Strong Skipping Mode를 구성할 수 있습니다.
안정성과 건너뛰기 메커니즘
Compose 컴파일러는 컴파일 타임에 모든 컴포저블 함수와 매개변수 타입을 분석합니다. 컴파일러가 모든 매개변수가 안정적(stable)이라고 증명할 수 있으면 해당 컴포저블 함수는 건너뛰기가 가능(skippable)해집니다. 타입이 안정적으로 간주되려면 두 가지 조건을 충족해야 합니다. 첫째, equals() 결과가 일관적이어야 합니다. 즉, 동일한 두 인스턴스에 대해 equals()를 호출하면 항상 같은 값을 반환해야 합니다. 둘째, 공개 프로퍼티의 변경이 스냅샷 상태(snapshot state)를 통해 Compose 런타임에 관찰 가능해야 합니다.
원시 타입(primitive type), String, 함수 타입은 본질적으로 안정적입니다. data class의 경우 모든 생성자 프로퍼티가 안정적인 타입이면 컴파일러가 해당 클래스도 안정적이라고 추론합니다. 하지만 List, Set, Map 같은 표준 컬렉션 타입은 안정적이지 않습니다. 호출 지점에서 컴파일러가 가변(mutable) 구현체인지 불변(immutable) 구현체인지 구분할 수 없기 때문입니다.
// 불안정: List는 런타임에 MutableList일 수 있음
data class UserList(val users: List<User>)
// 안정: ImmutableList는 변경이 불가능함을 보장
data class UserList(
val users: ImmutableList<User>
)
매개변수가 불안정하면 해당 컴포저블 함수는 건너뛰기가 불가능합니다. 매개변수 인스턴스가 변경되지 않았더라도, 부모가 리컴포지션될 때마다 런타임이 함수 본문을 매번 실행합니다. 이것이 대부분의 Compose 애플리케이션에서 불필요한 리컴포지션이 발생하는 가장 큰 원인입니다. 실무에서도 성능 프로파일링을 해보면 안정성 문제로 인한 과도한 리컴포지션이 프레임 드롭의 주범인 경우가 매우 많으므로, 이 메커니즘을 정확히 이해해 두시는 것이 중요합니다.
@Immutable과 @Stable 어노테이션
@Immutable 어노테이션은 클래스의 모든 공개 프로퍼티가 생성 이후 절대 변경되지 않음을 컴파일러에게 알립니다. @Stable 어노테이션은 이보다 조금 넓은 범위를 다루며, 프로퍼티가 변경될 수 있지만 그 변경이 스냅샷 시스템을 통해 Compose 런타임에 통지된다는 것을 의미합니다. 가령, mutableStateOf로 뒷받침되는 프로퍼티가 이에 해당합니다.
코드 생성 수준에서 두 어노테이션은 동일한 효과를 냅니다. 컴파일러가 해당 타입을 안정적으로 취급하고, 이 타입을 매개변수로 받는 컴포저블 함수에 건너뛰기 로직을 생성합니다. 차이는 의미론적(semantic)인 부분에 있습니다. 객체가 진정으로 불변인 경우에는 @Immutable을 사용하세요.