Shunz Android Dev Note

컴포즈 성능 개선 - Stable, Immutable 그리고 ImmutableCollection 본문

Compose

컴포즈 성능 개선 - Stable, Immutable 그리고 ImmutableCollection

_Shun_ 2023. 12. 29. 22:15

컴포즈에서 Stable 하다는 개념은 매우 중요합니다. 그 이유중 하나는 Smart Recomposition 때문입니다. 이는 리컴포지션을 진행할 때, 모든 입력값이 Stable이고 같은 값이라면 스킵하게 된다는 것을 의미합니다. 즉, 바뀐게 없으면 화면을 다시 그리지 않겠다는 의미로 UI 렌더링을 조금 더 효율적으로 하겠다는걸 나타냅니다. 하지만, 값이 바뀌지 않았다는 확신이 없다면 무조건 재구성(재렌더링)을 진행하게 됩니다.

 

 

Stable, Unstable 이란?

컴포즈는 타입을 stable, unstable로 구분을 합니다.

타입이 immutable이면 stable하다는 것을 의미하는데 이는 리컴포즈할때 컴포즈가 값이 변경 되었는지를 알아챌 수 있다는 것입니다.

 

반면, 타입이 unstable 하다는 것은 리컴포즈할때 컴포즈가 값이 변경 되었는지 알 수 없기 때문에 안정을 위하여 항상 리컴포즈를 진행하게 됩니다. 이로 인해, 실제 값이 변경 되지도 않았는데, 리컴포즈를 하게 되어 성능에 좋지 않은 영향을 주게 됩니다.

 


 

 

간단한 샘플 코드를 통하여 개념을 이해 해보도록 하겠습니다.

class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            StableImmutableTheme {
                var selected by remember { mutableStateOf(false) }
                Column {
                    Checkbox(
                        checked = selected,
                        onCheckedChange = { selected = it }
                    )
                    ContactsList(
                        isLoading = false,
                        names = listOf("Devpia")
                    )
                }
            }
        }
    }
}

@Composable
fun ContactsList(isLoading: Boolean, names: List<String>) {
    Box(modifier = Modifier.fillMaxSize()) {
        if (isLoading) {
            CircularProgressIndicator()
        } else {
            Text(text = names.toString())
        }
    }
}

 

코드를 간단히 설명 하자면, 체크박스를 토글 할 때마다 "selected" 의 상태값이 변경되면서 해당 컴포즈의 재구성이 발생하게 됩니다. 이 때, ContactsList는 값의 변화가 없지만, 계속해서 리컴포즈가 발생되고 있습니다. 불필요한 연산이 계속해서 진행되고 있기 때문에 성능저하를 가져올 수 있습니다.

 

위 코드를 실행한 영상입니다.

값의 변화가 없지만 항상 리컴포즈가 발생함.

 

좌측의 레이아웃 인스펙터를 보면 계속해서 리컴포지션이 발생하는것을 확인 할 수 있습니다. (skip 되지 않음.)

 

왜 이런 문제가 나오는것일까?

ContactsList 컴포저블에 인자로 전달되는것은 Boolean, List collection 입니다. Boolean 은 primitive type으로써 기본적으로는 Stable로 취급이 됩니다. 그래서 값이 변경되지 않는다면 리컴포지션이 발생하지는 않습니다.

하지만 List는 Unstable 객체입니다. 그 이유는, List에 MutableList를 넣을 수 도 있기 때문입니다. MutableList가 Unstable 하기 때문에 List도 Unstable 상태가 됩니다.

 

즉, 위 예제 코드에서 리컴포지션이 발생하는 이유는 ContactsList에 전달되는 인자에 List가 있기 때문입니다.

 

※ Stable로 취급되는 객체들
- 모든 프리미티브 타입 : Boolean, Int, Long, Float, Char, etc.
- Strings
- All Function types (람다)

 


 

솔루션1

List 콜렉션을 Stable 처럼 보이게 만드는 것입니다. 이 때 사용하는것이 @Stable 또는 @Immutable 어노테이션입니다.

// 호출부
ContactsList(
    ContactListState(
        isLoading = false,
        names = listOf("Devpia")
    )
)

@Composable
fun ContactsList(state: ContactListState) {
    Box(modifier = Modifier.fillMaxSize()) {
        if (state.isLoading) {
            CircularProgressIndicator()
        } else {
            Text(text = state.names.toString())
        }
    }
}

// 파라메터를 wrapping한 클래스 정의
@Stable
data class ContactListState(
    val isLoading: Boolean,
    val names: List<String>,
)

 

Boolean, List를 wrapping한 데이터 클래스를 정의 후 Stable annotation을 추가하였습니다. 이 annotation을 사용하면 값이 변경되지 않는이상 ContactsList의 리컴포지션이 수행되지 않게 됩니다. 아래 데모 영상을 보면 skip하게 되는것을 확인 할 수 있습니다.

Stable Annotation 사용

 

솔루션2

Immutable collection을 활용하는 것입니다. 

collection은 기본적으로 unstable 합니다. 이를 해결하기 위해 컴포즈 컴파일러는 immutable collection을 지원합니다. ImmutableList, ImmutableSet, ImmutableCollection과 같은 것들을 활용 할 수 있습니다.

 

사용하려면 gradle에 의존성을 하나 추가 해야 합니다. github 저장소는 여기를 참고 바랍니다.

dependencies {
    implementation("org.jetbrains.kotlinx:kotlinx-collections-immutable:0.3.6")
}

 

사용법은 간단합니다. 컴포저블의 파라메터를 List -> ImmutableList로 변환하고, 호출부에서는 listOf를 persistentListOf로 변환하였습니다.

// 호출부
ContactsList(
    isLoading = false,
    names = persistentListOf("Devpia")
)

@Composable
fun ContactsList(isLoading: Boolean, names: ImmutableList<String>) {
    Box(modifier = Modifier.fillMaxSize()) {
        if (isLoading) {
            CircularProgressIndicator()
        } else {
            Text(text = names.toString())
        }
    }
}

 

아래 데모 영상을 통하여 리컴포지션이 발생되지 않고 skip 카운트가 올라가는것을 확인 할 수 있습니다.

Immutable Collection 사용

 

 


 

결론

선언형 UI 프로그래밍의 가장 큰 문제점은 불필요한 리컴포지션(UI 렌더링) 작업으로 인한 성능 이슈입니다. 이러한 작업을 최소화 하기 위한 방법이 여러가지가 있지만, 가장 기본적이고도 중요한 방법은 컴포저블 함수에 전달하는 파라메터의 타입을 최대한 Stable 한 상태로 만들어야 한다는 것입니다. 컴포즈 컴파일러는 Unstable 타입의 경우에는 값이 동일하더라도 무조건 리컴포지션을 수행하기 때문에 이를 회피하기 위한 방법들을 적용하여 이슈를 최소화 하도록 해야 할 것입니다.