Jetpack Compose Stability 파헤치기(성능향상)

동키·2025년 3월 14일

안드로이드

목록 보기
2/14

XML 기반 UI에서는 개발자가 명령형으로 직접 UI 요소들을 업데이트 하는 반면, Compose에서는 상태(state)가 변화하면 해당 상태를 참조하는 UI 컴포저블이 자동으로 재구성(Recomposition)됩니다.
이는 개발자가 UI 갱신 로직을 일일이 관리할 필요 없이, 데이터와 UI의 일관성을 유지할 수 있습니다.

오 개꿀이잖아???

그러나 코드 구조가 부실하거나 상태 관리가 세밀하게 이루어지지 않으면, 불필요한 재구성(Recomposition)이 빈번하게 발생할 수 있습니다. 과도한 재구성은 화면 업데이트가 지나치게 자주 일어나게 만들어, 성능 저하와 함께 사용자 경험에도 부정적인 영향을 미칠 수 있습니다.
즉 컴포저블의 재구성 범위와 최적화 전략을 신중하게 설계함으로써 효율적이고 안정적인 UI 업데이트를 달성하는 것이 중요합니다.

이번 포스팅에서는 Compose UI가 화면에 그려지는 메커니즘과, 상태 변화에 따라 어떻게 리컴포지션이 발생하는지에 대해 알아보고 Compose Stability 전략에 대해 알아보겠습니다.

공부한 내용에 틀린 내용이 있다면 언제든지 훈수 부탁드립니다~

Compose에서 데이터를 UI로 변환하는 세 단계

Jetpack Compose 상태
렌더링 프로세스

Composition

  • Composable 함수를 실행하여 UI의 선언적 설명(트리 구조)을 생성하는 단계

  • 개발자가 작성한 Composable 함수들이 호출되면서, 각 UI 요소의 속성과 계층 구조를 포함한 Composition Tree가 구성됩니다.

Layout

  • 생성된 UI 요소들이 화면 상에서 어떻게 배치될지를 결정하는 단계입니다.

레이아웃 단계에서는 세 단계 알고리즘을 사용하여 트리 경로를 탐색합니다.

1. 하위 요소 측정: 노드가 하위 요소(있는경우)를 측정합니다.

2. 자체 크기 결정: 측정 결과를 바탕으로, 노드가 자체 크기를 결정합니다.

3. 하위 요소 배치: 각 하위 노드는 노드의 자체 위치를 기준으로 배치됩니다.

Drawing

  • 최종적으로 결정된 레이아웃을 바탕으로 UI 요소들을 실제로 화면에 렌더링 하는 단계입니다.

  • 트리가 위에서 아래로 다시 탐색되고 각 노드가 차례로 화면에 그려집니다.


위 3과정을 통해 렌더링이 일어나기 되고 만약 상태가 변경될 시 위 3과정을 다시 거치게 되고
이 과정을 recomposition(재구성) 이라고 부릅니다.


Compose 안정성

Composition 단계에서 UI트리를 생성하고 매핑하는 작업은 많은 연산 비용을 발생시킵니다(레이아웃, 드로잉 또한 비용이 발생합니다).

이처럼, 재구성 시에 Composition -> Layout -> Drawing 까지의 전 과정이 다시 실행되므로, 불필요한 재구성을 최소화하는 것이 성능 최적화에 매우 중요합니다.

스마트 리컴포지션은 상태가 변경되었을 때, 필요한 부분만 재구성하도록 하여 비용을 줄여줍니다.

어떻게 하면 불필요한 재구성을 건너뛰고 성능 최적화를 할 수 있는지 알아보겠습니다.

Stable과 UnStable

Compose는 매개변수(파라미터)의 안정성을 사용하여 컴포저블이 리컴포지션 중 컴포저블을 건너뛸 수 있습니다.
어떤 파라미터가 안정적이고 불안정적일가요?

Stable(안정적인 매개변수)

변경 불가능한 객체는 Stable 합니다.

data class Contact(val name: String, val number: String)

위 데이터 클래스에서의 속셩 변수는 모드 val(불변)으로 이루어졌기 때문에 객체 속성 값을 변경할 수 없습니다.
그러므로 Stable 합니다.

어떤 것들이 Stable 한지 알아보겠습니다.

  • 원시 타입(String, Int, Float 등)

  • 람다(근데 이제 UnStable 값을 캡처하지 않는)

  • 위처럼 모든 속성이 val로 선언된 class(근데 이제 UnStable 타입이 아닌)

  • @Stable, @Immutable 주석이 붙은 class


UnStable(불안정적인 매개변수)

변경할 수 있는 객체
Compose는 파라미터에 불안정한 매개변수가 있을 시 구성요소의 상위를 recomposition(재구성)할 때 항상 이를 재구성 합니다.

data class Contact(var name: String, var number: String)

어떤 것들이 UnStable 할까요?

  • 속성 중 var(가변)가 하나라도 있다면

  • List, Set, Map (인터페이스이므로 mutableList, mutableSet, mutableMap 등은 값이 항상 바뀔 수 있음)


Stable과 UnStable에 알아보았습니다.
정리하면 컴포저블의 모든 파라미터가 안정 유형이라면 재구성 시 이를 건너 뛸 수 있지만 불안정 유형이 있다면 Compose는 재구성 시 이 컴포저블을 건너뛸 수 없습니다.

예시코드를 확인해보겠습니다.

data class User(
    val age: Int,
    val name: String,
    val favoriteNumber: List<Int>
)
우선 User 데이터 클래스에 age, name, favoriteNumber를 val로 선언했습니다.
이때 favoriteNumber는 val로 지정을 했지만 List타입이기 때문에 UnStable이라고 할 수 있습니다.
@Composable
fun MainScreen(
    viewModel: MyViewModel = viewModel(),
    paddingValues: PaddingValues = PaddingValues()
) {
    var title by remember { mutableStateOf("유저 정보") }
    val user = viewModel.user.value
    Column(modifier = Modifier.padding(paddingValues)) {
        TitleScreen(title = title)
        Spacer(modifier = Modifier.height(30.dp))
        UserScreen(user = user)
    }

    Button(onClick = { title = "타이틀 바꾸기 ${UUID.randomUUID()}"}) {
        Text(text = "타이틀 바꾸기")
    }
}

@Composable
fun TitleScreen(
    title: String
) {
    Text(text = title)
}

@Composable
fun UserScreen(
    user: User
) {
    Column {
        Text(text = "나이 ${user.age}")
        Text(text = "이름 ${user.name}")
        user.favoriteNumber.forEach { info ->
            Text(text = info.toString())
        }
    }
}

간단한 예시입니다.
버튼을 누르면 title이 변경되어 TitleScreen이 recomposition 되게 됩니다.
이 때 UserScreen은 같이 recomposition이 될까요 안될까요??
LayoutInspector를 통해서 바로 확인해보겠습니다.

TitleScreen 과 UserScreen의 재구성 횟수가 동일한 것을 확인할 수 있습니다.

그럼 이번에는 데이터 클래스에서 List 타입 속성을 삭제해볼가요?

data class User(
    val age: Int,
    val name: String,
)

데이터 클래스를 이렇게 수정한뒤 실행해보겠습니다.

이번에는 TitleSreen은 4번 재구성이 되었지만 UserScreen은 재구성을 건너뛴 것을 확인할 수 있습니다.!!
대박...

이렇게 컴포저블의 파라미터에 안정적인 유형들이 온다면 Compose는 Smar Recomposition 즉 변경사항이 없다면 재구성을 건너뛸 수 있게 됩니다.

그렇다면 데이터 클래스에 List타입이나 Set, Map은 사용하면 안되는건가요?? 할 수 있습니다.

이를 위해 데이터 클래스 타입에 @Immutable 주석을 달아주거나 Kotlinx Immutable Collections을 사용할 수 있습니다.

@Immutable
data class User(
    val age: Int,
    val name: String,
    val favoriteNumber: List<Int>
)

이렇게 @Immutable 주석을 붙이게 되면 컴파일러가 안정적인 유형으로 판단하게 됩니다.
하지만 @Immutable 주석은 객체 생성후 절대!! 변경이 되지 않을 경우에 사용해야 합니다.

data class User(
    val age: Int,
    val name: String,
    val favoriteNumber: ImmutableList<Int>
)

위에서 언급한 Kotlin Immutable Collecetion을 사용해도 됩니다.


restartable, skippable

우리가 작성한 컴포저블이 제대로 잘 작성되었는지 어떻게 확인할 수 있을가요?
안정성 문제 진단에서 확인할 수 있습니다.

 android { ... }
 composeCompiler {
    reportsDestination = layout.buildDirectory.dir("compose_compiler")
    metricsDestination = layout.buildDirectory.dir("compose_compiler")
  }

우선 build.gradle 파일에 해당 옵션을 추가합니다.

경로에 composables.txt 파일이 생성되었고 내용을 확인해볼까요?

restartable skippable scheme("[androidx.compose.ui.UiComposable]") fun MainScreen(
  stable viewModel: MyViewModel? = @dynamic viewModel(null, null, null, null, $composer, 0, 0b1111)
  stable paddingValues: PaddingValues? = @static PaddingValues()
)
restartable skippable scheme("[androidx.compose.ui.UiComposable]") fun TitleScreen(
  stable title: String
)
restartable skippable scheme("[androidx.compose.ui.UiComposable]") fun UserScreen(
  stable user: User
)
restartable skippable scheme("[0, [0]]") fun CoroutinesTheme(
  stable darkTheme: Boolean = @dynamic isSystemInDarkTheme($composer, 0)
  stable dynamicColor: Boolean = @static true
  stable content: Function2<Composer, Int, Unit>
)

위에서 공부한 stable을 확인할 수 있습니다.
즉 컴포저블 함수의 파라미터들이 stable 한지, unstable한지 확인할 수 있습니다.

그럼 위에 적힌 restartable과 skippable은 무엇을 의미하는 걸가요?

restartable(재실행 가능한)

Compose 컴파일러에 의해 추론된 Composable 함수의 유형입니다.
Compose Runtime이 입력의 변화를 감지하면, 이 새로운 입력을 반영하기 위해 함수를 다시 시작합니다.

skippable(생략 가능)

Recomposition 시 값이 바뀌지 않았다면 생략할 수 있을 지 없을지

skippable이 붙은 컴포저블은 변경사항이 없을 시 recomposition을 건너뛰어 성능 향상에 직접적인 연관이 있습니다.
즉 전체 recomposition 프로세스를 간소화 시킬 수 있다고 볼 수 있습니다.

profile
오늘 하루도 화이팅

0개의 댓글