이번 공부 할 내용은 컴포즈의 안정성에 관한 내용이다.
컴포즈로 개발을 하다 보면 만든 컴포저블 함수가 자주 다시 호출(Recomposition)되는 경험이 있을 것이다. 텍스트 하나 바뀐 것도 아닌데 Text 컴포저블이 리컴포지션이 된다면 불필요한 리컴포지션이 된다.
만약 계속 불필요한 리컴포지션이 일어나면 성능 저하로 이어질 수 있다.
그럼 컴포즈에서는 어떤 기준으로 리컴포지션을 발생시키고 불필요한 리컴포지션을 줄이는 방법 즉 안정성을 높이는 방법을 알아보자!
# 리컴포지션(Recompoition)이란?
Compose는 상태 기반 UI 프레임워크로 어떤 상태 값이 바뀌면 그 값을 사용하는 컴포저블 함수만 다시 실행된다.
이 과정을 리컴포지션(Recomposition)이라고 한다.
전체 UI를 다시 그리는 대신 실제로 변경된 컴포저블 함수들만 재실행한다. 이런 똑똑한 처리 방식이 바로 스마트 리컴포지션(Smart Recomposition)이다.
# 그럼 어떤 기준으로 리컴포지션을 판단할까?
컴포저블 함수의 파라미터나 상태가 안정적인(Stable) 객체인지 판단해서 만약 안정적이라면 리컴포지션을 발생시킨다.
여기서 중요한 키워드는 안정적인(Stable)인데 객체가 Stable 하다는 뜻은 값이 예측 가능하고 변경되지 않는 것을 의미한다.
## 컴포즈의 타입
방금 위에서 컴포즈는 리컴포지션을 판단할 때 매개변수나 상태가 Stable 한 지 여부를 기준으로 삼는다고 했다.
이때 컴포즈 컴파일러는 컴포저블 함수에 사용된 매개변수에 대해 다음 두 가지 타입 중 하나를 부여한다.
⭐️ stable
⭐️ unstable
그럼 왜 이 타입 구분이 중요할까? 아래와 같은 이유 때문이다.
⭐️ 컴포저블 함수에 unstable 한 매개변수가 하나 이상 포함되어 있으면 리컴포지션이 항상 발생
⭐️ 컴포저블 함수가 모두 stable 한 매개변수로 이루어져 있다면 리컴포지션을 건너뛰고 불필요한 작업 생략
## stable로 간주되는 유형
🟠 원시 타입 (Primitive types) - Int, Long, Float, Double, Boolean, String 등
@Composable
fun Greeting(name: String) {
Text(text = "Hello, $name")
}
Greeting 컴포저블 함수를 보면 name이라는 String 형식의 파라미터 받고 있다.
이 경우 String은 stable 하므로 값이 바뀌지 않으면 리컴포지션은 생략된다.
🟠 data class 또는 class의 pubilc 프로퍼티가 모두 불변이거나 stable 한 경우
data class User(val name: String, val age: Int)
@Composable
fun UserInfo(user: User) {
Text(text = "${user.name}, ${user.age}")
}
이 경우 User data class는 프로퍼티가 원시타입이니 컴포즈가 자동으로 stable로 간주된다.
하지만 아래와 같이 List 경우 mutable일 수 있어서 unstable로 간주된다.
data class User(val name: String, val hobbies: List<String>)
## unstable로 간주되는 유형
🟠 data class 또는 class의 var로 정의된 변경 가능한 프로퍼티를 가진 경우
data class Profile {
var name: String = ""
var age: Int = 0
}
이 경우 프로퍼티가 원시타입이지만 var로 정의되어서 내부 상태가 언제든 바뀔 수 있기 때문에 컴포즈는 안정성을 보장할 수 없다.
따라서 unstable로 간주되어 항상 리컴포지션이 발생한다.
🟠 data class 또는 class의 mutable 한 컬렉션을 포함한 경우(List, Map 등)
data class User(
val name: String,
val hobbies: List<String> // ⚠️ List는 기본적으로 mutable
)
List는 기본적으로 MutableList일 수 있어서 안정성이 떨어진다.
컬렉션이 바뀌었는지 판단이 어려워서 unstable 간주되어 항상 리컴포지션이 발생한다.
🟠 Interface 타입 및 람다 함수를 포함한 구조
interface ClickHandler {
fun onClick()
}
@Composable
fun MyButton(
handler: ClickHandler,
onLongClick: () -> Unit
) {
Button(
onClick = { handler.onClick() },
onLongClick = onLongClick
) {
Text("Click Me")
}
}
이 두 경우(인터페이스 타입, 람다 함수 타입) 모두 컴포즈 컴파일러 내부 동작을 추적하거나 변경 여부를 판단하기 어렵기 때문에 기본적으로 unstable로 간주되어 항상 리컴포지션이 발생한다.
이 처럼 unstable이 된 컴포저블 함수가 많을수록 항상 리컴포지션이 발생하여 앱 성능에 크게 영향을 미친다.
그렇다는 건 컴포저블 함수의 프로퍼티나 내가 원하는 클래스, 객체를 stable로 인식시켜야 성능 향상을 할 수 있다는 뜻이다.
그럼 다음편에서 컴파일러가 항상 완벽하게 판단해 주지 않기 때문에 내가 만든 클래스 혹은 객체가 실제로는 stable 한데 컴파일러는 unstable로 판단할 수 있는 현상을 해결하기 위한 방법을 작성해 보겠다.