Android
Compose에서 Stable을 가볍게 보면 안 되는 이유: LazyList 안정성 가이드 Part 2
Jonas여기어때
2025년 10월 24일
원문에서 보기 ↗
Android Compose의 LazyList를 최적화하는 방법은 여러 가지가 있습니다. 이 글에서는 그중 key 사용 과 stable 상태에 따라 어떤 결과가 나타나는지를 중심으로 살펴보겠습니다.
테스트 환경
실험을 위한 환경은 Compose에서 Stable을 가볍게 보면 안 되는 이유: 베드 케이스로 본 안정성의 법칙 Part 1 글과 동일하며 Strong Skipping Mode를 활성화한 상태에서 진행이 됩니다.
@Composable
fun Item(
item: ListItem,
onClick: (ListItem) -> Unit,
) {
Text(
modifier = Modifier
.fillMaxWidth()
.clickable { onClick(item) }
.padding(10.dp)
.background(if (item.isSelected) Color(0x99FF0000) else Color(0xFFFFFFFF)),
text = item.name,
fontSize = 30.sp,
)
}
@Composable
fun LazyColumList(
list: ImmutableList<ListItem>,
onClick: (ListItem) -> Unit = {},
) {
LazyColumn(
verticalArrangement = Arrangement.spacedBy(10.dp),
) {
items(
items = list,
key = { it.name },
) {
Item(it, onClick)
}
}
}
- 테스트를 위해 간단한 화면으로 구성
LazyColumn내부에ListItem을 사용하는TextComponent 포함
Key
우선 LazyColumn 최적화의 가장 기본인 key 사용에 대해 테스트를 해보겠습니다. List 아이템의 순서를 바꾸었을 때 key 사용 유무에 따른 결과는 다음과 같습니다.

key 사용 하지 않았을 경우
- List 아이템 변경 없이
shuffle()함수를 통해 순서를 뒤섞음 - 순서만 바뀌었음에도 모든 리스트의 아이템이 recomposition 이 발생

key 를 사용하였을 경우
shuffle()이 발생되었지만 모든 아이템이 skip 됨
왜 그럴까요?
LazyColumn에서 key를 통해 데이터세트가 변경이 되어도 항목이 유지될 수 있도록 지원하기 때문입니다. 그리고 key 값은 unique key이므로 중복 값을 허용하지 않으며 그렇지 않을 경우 크래시가 발생이 됩니다.
To combat this, you can provide a stable and unique key for each item, providing a block to the
keyparameter. Providing a stable key enables item state to be consistent across data-set changes:
https://developer.android.com/develop/ui/compose/lists#item-keys
아이템 상태 변경 테스트
ListItem에 isSelected 값에 따라 배경색이 바뀌는 화면이며 이때 stable 상태에 따라 리스트 전체 아이템에 어떤 영향을 주는지 테스트해 보겠습니다.
interface ListItem {
val name: String
val isSelected: Boolean
}
class UnstableSelectItem(
override val name: String,
override var isSelected: Boolean = false,
): ListItem
data class SelectItem(
override val name: String,
override val isSelected: Boolean = false,
) : ListItem
@Stable
class StableSelectItem(
override val name: String,
isSelected: Boolean = false,
) : ListItem {
override var isSelected by mutableStateOf(isSelected)
}
ListItem:name,isSelected로 가지고 있는 인터페이스, 구현체에서 stable 이 결정됨UnstableSelectItem: unstable 상태를 테스트하기 위한 classSelectItem: data class로 구현되어 stable 상태를 테스트하기 위한 classStableSelectItem:@Stable및mutableStateOf를 사용하여 stable 상태를 테스트하기 위한 class
unstable class
fun toggleSelect(item: ListItem) {
list = list.map {
UnstableSelectItem(
it.name,
if (it.name == item.name) {
it.isSelected.not()
} else {
it.isSelected
}
)
}.toPersistentList()
}
UnstableSelectItem은 일반 Class이므로map내부에서 새로 객체 생성name이 동일한 값일 경우isSelected상태를 반전

unstable class를 다시 생성하였으므로 인스턴스 동등성에 의해 모든 리스트 아이템이 recompositoin 발생함을 볼 수 있습니다.
data class
fun toggleSelect(item: ListItem) {
list = list.map {
if (it.name == item.name) {
it.copy(isSelected = it.isSelected.not())
} else {
it
}
}.toPersistentList()
}
SelectItem은 data class이므로copy를 통해 상태를 갱신

stable인 data class는 객체 동등성에 의해 비교를 하므로 isSelected 값에 변화가 발생 한 아이템만 recomposition 이 된 것을 볼 수 있습니다.
stable + class
fun toggleSelect(item: ListItem) {
list.forEach {
if (it.name == item.name) {
it.isSelected = it.isSelected.not()
}
}
}
StableSelectItem의isSelected만 상태 변경

StableSelectItem는 mutableStateOf를 사용하여 isSelected상태를 관리합니다. 그리고 @Stable 을 사용하여 상태 변화를 감지하도록 합니다.
data class를 사용할 때와 다르게 매번 List를 새롭게 만들어 발행할 필요 없이 기존 리스트에서 변경할 아이템을 탐색 후 상태만 변경하게 되므로 성능 이점이 있습니다.
요약

아이템 추가/삭제 테스트
리스트를 관리할 때 아이템을 추가 및 삭제를 해야 하는 경우도 있습니다. 이때는 어떤 결과인지 테스트해 보겠습니다.
List 교체
// 제주 감귤 추가
list = list.toMutableList().let {
it.add(UnstableSelectItem(name = "제주 감귤"))
it.toPersistentList()
}
// 포도 삭제
list = list.toMutableList().let {
it.removeAt(2)
it.toPersistentList()
}
- 기존 List에서 제주 감귤을 추가하여 다시 List 발행
- 기존 List에서 3번째 아이템을 삭제하여 다시 List 발행

아이템 추가 및 삭제 후 발행되는 List 교체에 의한 recomposition 이 발생되지만 기존 내부 아이템들은 변화가 없으므로 skip 처리가 됩니다.
SnapshotStateList
List 교체를 하지 않고, 아이템 변화를 관찰 가능하도록 하는 MutableList입니다. 관찰 가능한 List이므로 아이템 추가 및 삭제를 간편하게 할 수 있습니다.
An implementation of
MutableListthat can be observed and snapshot. This is the result type created byandroidx.compose.runtime.mutableStateListOf.This class closely implements the same semantics as
ArrayList.
https://developer.android.com/reference/kotlin/androidx/compose/runtime/snapshots/SnapshotStateList
val snapshotList = mutableStateListOf<ListItem>().apply {
addAll(fruit.map { UnstableSelectItem(name = it) })
}
// 샤인머스켓 추가
snapshotList.add(UnstableSelectItem(name = "샤인머스켓"))
// 포도 삭제
snapshotList.removeAt(2)
snapshotList에서 바로add,removeAt실행 가능

List의 교체가 없으므로 아이템 리스트의 변화만 발생하고 skip이 됨을 볼 수 있습니다.
Summary
LazyList 최적화를 위해서는 key 설정 과 stable 상태 유지가 핵심입니다. key는 항목 식별을 명확히 하여 불필요한 재구성을 줄이고, stable 아이템은 변경 감지를 효율적으로 처리해 전체 리스트의 recomposition을 방지합니다.
또한 아이템의 상태를 변경하는 방법은 여러 가지가 있으므로, 프로젝트의 구조와 목적에 맞는 방식을 선택하는 것이 중요합니다.
긴 글 읽어주셔서 감사합니다.