Compose에서 Recomposition이 어떻게 발생되는지에 대해서 정리하였다.
Compose에서 UI가 렌더링되는 과정은 아래와 같다.
1. Composition
2. Layout
3. Draw
Composition 단계는 실행되어야 할 Composable함수를 판별하고, 이에 대한 메모리 슬롯을 할당하는 과정이다.
Layout은 UI에 대한 view가 inflate되면서 사이즈와 위치가 정해지는 단계이며, Draw는 실제로 UI를 그리는 과정이다.
Recomposition은 이미 렌더링이 종료된 UI에 대해서 Composition이 다시 발생하는 현상을 말하며, 이로 인해 UI가 다시 그려지게 된다.
Recomposition의 발생은 Composable함수에서 받는 매개변수 데이터의 안정성과 관련이 있다.
해당 매개변수가 안정성에 따라서 Recomposition이 일어날수도 있고, 일어나지 않을 수도 있다.
여기서 말하는 안정성은 Stable과 Unstable로 나눌 수 있다.
Stable한 데이터는 아래와 같다.
- String을 포함한 기본 유형(Primitive)
- 람다 표현식(람다 내부에서 unstable한 값을 캡처하는 경우에는 잠재적으로 unstable)
- 모든 public 프로퍼티가 불변(value 형태)이며, stable한 속성(자료형)을 가진 클래스
- @Stable, @Immutable과 같은 stability 어노테이션을 사용한 경우
반대로 Unstable한 데이터는 아래와 같다.
- Collections에서 제공하는 List나 Map 등
- Interface
- Any타입
- 가변적(Variable)이거나 불안정한 public 프로퍼티가 하나라도 포함되어 있는 클래스
Unstable한 데이터는 안정성을 보장하지 못하기 때문에 동일한 Input에 대해서 동일한 Output을 기대할 수 없다. 이에 항상 Recomposition이 일어나게 된다.
언뜻 보면 Recomposition이 자주 발생해야 UI 최신화가 잘 이루어진다고 생각할 수 있다.
그러나, 잦은 Recomposition의 발생은 UI렌더링 성능에 영향을 미칠 수 있다.
표시하는 데이터가 같아서 굳이 바뀌지 않아도 되는 UI에 대해서 Recomposition이 발생한다는 것은 같은 UI를 다시 렌더링 한다는 것이며, 이는 UI렌더링 성능에 영향을 미치게 된다.
즉, Compose에서는 Recomposition이 발생하는 부분을 최소화하여야 UI 렌더링 성능을 향상시킬 수 있다.
따라서, Compose에서는 Stability관리를 통해 Recomposition의 발생 빈도를 줄이는 것이 매우 중요하다!!
데이터의 안정성이 결정되면 Compose 런타임은 내부 로직을 통해 Recomposition 발생 여부를 결정하게 되는데, 이와 같은 기능을 스마트 Recomposition이라고 한다.
스마트 Recomposition이 결정된 안정성을 기준으로 Stable한지 Unstable한지 확인한 이후, 내부 로직을 통해 Unstable하다면 Recomposition을 발생시킨다.
만약 Stable하다면 해당 데이터에 대한 동등성 비교 검사 과정으로 이동한다.
동등성 비교는 해당 데이터의 클래스에 구현되어 있는 equals 함수를 통해 결정되며, 해당 결과가 false인 경우에는 Recomposition이 발생하고 true인 경우에는 Recomposition이 발생하지 않는다.
컴파일러단에서는 효율적인 처리를 위해 Composable함수가 어떤 유형인지 그룹화하게 되는데, Recomposition과 밀접한 연관이 있는 유형이 바로 Restartable과 Skippable이다.
Restartable로 분류된 함수들은 입력이나 상태가 변경될 때마다 Compose 런타임이 Composable함수에 대해 Recomposition을 트리거할 수 있게 된다.
대부분의 Composable함수는 Restartable로 간주된다.
Skippable로 분류된 Composable함수는 Recomposition 프로세스를 완전히 건너뛸 수 있게 된다.
이는 루트 Composable함수를 실행하지 않도록 하여 하위 함수들을 실행하지 않기 때문에 불필요한 렌더링을 줄일 수 있다.
따라서, UI 성능 향상과도 관련이 있다.
@Immutable과 @Stable 어노테이션을 통해 불안정한 클래스를 안정적으로 만들 수 있다.
1. @Immutable
클래스의 초기 생성 이후 절대 변하지 않는다는 강력한 불변을 보장할 수 있다. (val 키워드보다 더 엄격한 보증)
물론, 강력한 보증에도 MutableList로 초기화된 List와 같은 형태는 가변 데이터 구조로 생성될 가능성을 허용하기에, Immutable 어노테이션 사용 시에는 반드시 데이터들의 안정성을 체크하여야한다.
효과적인 안정성 관리를 위해서는 아래 규칙을 모두 만족하여야 한다.
- 모든 public 프로퍼티에 val 키워드 사용
- 커스텀 setter를 지양
- 모든 public 프로퍼티가 본질적으로 불변으로 간주해도 되는지 확인
(collections 형태임에도 데이터가 변하지 않음이 검증되었는지 여부)
해당 어노테이션은 주로 비즈니스 모델에 사용된다.
@Immutable
public data class User(
public val id: String,
public val nickname: String,
public val profileImages: List<String>,
)
2. @Stable
@Immutable보다는 느슨한 약속을 보장한다.
해당 데이터 타입이 변할 수는 있으나, 타입에 따른 데이터의 Stable함은 보장할때 사용한다.
동일한 입력에 대해서 항상 동일한 결과를 반환한다는 점에서 예측 가능한 동작을 보장한다는 의미이다.
인터페이스 형태에 주로 사용된다.
@Stable
interface UiState<T : Result<T>> {
val value: T?
val exception: ServerException?
}
일반적으로 Composable 함수의 입력이 변경되면 UI변경을 위해 Recomposition이 일어나게 되는데, 이러한 UI 변경이 항상 필요한 것은 아닐 것이다.
함수의 내부 상태나 사이드 이펙트가 Recomposition 프로세스에서도 일관되어야 하는 경우에 특히 그렇다.
이럴 때에는 @NonRestartableComposable 어노테이션을 사용하면 된다.
해당 어노테이션을 통해 함수를 다시 시작하지 않고 매개변수를 업데이트하도록 지시하여 내부 상태와 진행 중인 사이드 이펙트를 유지할 수 있다.
대표적인 예로는 LaunchedEffect의 구현이 있다.
@Composable
@NonRestartableComposable
@OptIn(InternalComposeApi::class)
fun LaunchedEffect(
key1: Any?,
block: suspend CoroutineScope.() -> Unit
) {
val applyContext = currentComposer.applyCoroutineContext
remember(key1) { LaunchedEffectImpl(applyContext, block) }
}