
컴포즈는 데이터 타입을 stable 또는 unstable로 구분한다. (여기서 컴포즈는 컴포즈 컴파일러와 컴포즈 런타임을 아우르는 전체 컴포즈 프레임워크를 의미한다.)
stable: immutable 하거나, 컴포즈가 리컴포지션 동안에 값의 변경 여부를 알 수 있는 경우unstable: 컴포즈가 리컴포지션 동안에 값의 변경 여부를 알 수 없는 경우
컴포즈는 컴포저블 매개변수의 안정성을 통해, 리컴포지션 동안에 해당 컴포저블의 skip 가능 여부를 결정한다.
stable매개변수: 컴포저블에 변경되지 않은 stable 매개변수가 있으면, 컴포즈는 리컴포지션 동안에 해당 컴포저블을 skip 한다.unstable매개변수: 컴포저블에 unstable 매개변수가 있으면, 컴포즈는 해당 컴포저블을 항상 리컴포지션 한다.
만약 앱에 (컴포즈가 항상 리컴포지션을 트리거하는) unstable 매개변수가 불필요하게 많은 경우, 성능 이슈나 기타 다른 문제가 발생할 수 있다.
이번 글에서는 앱의 안정성을 높여서 성능과 전반적인 사용자 경험을 개선할 수 있는 방법에 대해 알아보자!
data class Contact(val name: String, val number: String)
위에 보이는 Contact 데이터 클래스는 immutable 하다. 왜냐하면, 모든 프로퍼티가 val 키워드로 정의된 primitive 타입이기 때문이다. 일단 Contact 클래스의 인스턴스를 한번 생성하면, 해당 객체의 프로퍼티 값은 변경할 수 없다. 만약 변경하고 싶다면, 새로운 객체를 생성해야 한다.
@Composable
fun ContactRow(contact: Contact, modifier: Modifier = Modifier) {
var selected by remember { mutableStateOf(false) }
Row(modifier) {
ContactDetails(contact)
ToggleButton(selected, onToggled = { selected = !selected })
}
}
ContactRow 컴포저블은 Contact 타입의 매개변수를 갖는다.
사용자가 토글 버튼을 클릭해서 selected 상태가 바뀌면, 어떤 일이 일어날까?
- 컴포즈는 ContactRow 내부 코드를 recompose 해야 하는지 검사한다.
- ContactDetails의 유일한 인수가 Contact 타입이라는 걸 확인한다.
- Contact는 immutable한 데이터 클래스이므로, 컴포즈는 ContactDetails의 어떤 인수도 변경되지 않았음을 확신할 수 있다.
- 따라서, 컴포즈는 ContactDetails를 skip하고 recompose 하지 않는다.
- 반면에, ToggleButton의 인수는 변경되었으므로, 컴포즈는 해당 컴포저블을 리컴포지션 한다.
data class Contact(var name: String, var number: String)
위의 코드처럼 클래스의 속성 중에 하나라도 var이면, 해당 클래스는 mutable 타입이다.
이때, 컴포즈는 클래스의 속성이 변경되어도 이를 인지하지 못한다. 왜냐하면, 컴포즈는 오직 state 객체에 대한 변경사항만 추적할 수 있기 때문이다.
이처럼 리컴포지션 동안에 값의 변경 여부를 알 수 없으면, 컴포즈는 unstable 타입으로 간주한다. 컴포즈는 unstable 클래스에 대해 리컴포지션을 skip 하지 않는다.
따라서, 만약 Contact 클래스가 위와 같이 mutable 타입으로 정의되면, ContactRow 컴포저블은 selected 상태가 변경될 때마다 리컴포지션 된다.
컴포즈가 리컴포지션 동안에 skip할 함수를 정확히 결정하는 방법에 대해 알아보자.
프로그램 실행 시 컴포즈 컴파일러는 각 함수와 타입에 특정한 태그를 붙인다. 이러한 태그를 통해 컴포즈가 리컴포지션 동안에 함수나 타입을 어떻게 처리하는지 알 수 있다.
참고: 리컴포지션과 안정성을 이해하는 데 이러한 태그가 반드시 필요한 것은 아니다. 그러나, 안정성 문제를 디버깅 할 때 대체로 유용하다.
컴포즈는 함수에 skippable 또는 restartable 태그를 붙인다. 이러한 태그는 한 함수에 하나 또는 모두, 아니면 모두 안 붙일 수도 있다.
Skippable : 컴포즈 컴파일러가 이 태그를 컴포저블에 붙이면, 컴포즈는 해당 컴포저블의 모든 인자가 이전 값과 동일한 경우 리컴포지션 동안에 해당 컴포저블을 skip 할 수 있다. Restartable : 재시작 가능한 컴포저블은 리컴포지션을 시작할 수 있는 '범위' 역할을 한다. 다시 말해, 해당 컴포저블은 상태 변경 후 컴포즈가 리컴포지션을 트리거 하기 위해 코드를 재실행시키는 시작점이 될 수 있다. 컴포즈는 타입에 immutable 또는 stable 태그를 붙인다. 각 타입은 둘 중에 하나로 결정된다.
Immtable : 클래스의 모든 속성이 절대 변하지 않고, 모든 함수의 참조가 투명하다. (참조가 투명하다는 건, 동일한 입력에 대해 동일한 출력을 반환하며, 예측 가능하고 부작용이 없다는 뜻) 참고로, String, Int, Float 같은 모든 primitive type은 immutable로 간주된다. Stable : 객체가 생성된 후 속성이 변경될 수 있다. 그 대신에 컴포즈는 런타임에 이러한 변경 사항을 알아차릴 수 있다. 참고: 컴포저블을 skippable로 만들기 위해서, 컴포저블의 매개변수를 immutable로 만들 필요는 없다. 컴포즈 런타임에 모든 변경사항이 알려지는 한, 컴포저블은 mutable일 수 있기 때문이다. 그러나, 대부분의 타입에서 이런 규칙을 지키는 것은 비현실적이다. 다행히도, 컴포즈는 MutableState, SnapshotStateMap, SnapshotStateList 같이 이러한 규칙을 준수하는 mutable 클래스를 제공한다.
앱에서 아직 변경되지 않은 매개변수를 갖고 있는 컴포저블을 리컴포지션 하는 경우, 우선 명확하게 mutable한 매개변수가 있지 않은지 정의를 확인한다.
var 속성 또는 unstable 타입으로 알려진 val 속성을 가진 타입을 넘기면, 컴포즈는 항상 리컴포지션을 트리거한다.
컴포즈 안정성 관련 문제를 진단하는 더 자세한 방법은 이 문서에서 확인할 수 있다.
성능 문제를 일으키는 unstable 클래스를 stable로 변경하는 방법에 대해서는 이 문서에서 확인할 수 있다.
컴포즈 안정성 외에도 컴포즈 성능 최적화를 위해 지켜야 할 모범 사례는 다음과 같다.
remember를 사용한다. key 매개변수로 지연 레이아웃에 stable한 키를 제공하여, 불필요한 리컴포지션을 최소화한다. derivedStateOf를 사용해 상태가 빠르게 변할 때 리컴포지션을 제한할 수 있다. Modifier.offset{} 같은 람다 기반의 modifier를 사용한다. 더 자세한 설명은 이 문서에서 확인할 수 있다.