MutableState — Compose에서의 관찰 가능한 상태 및 업데이트 메커니즘

저자: IT Sectr 게시일: 2026-06-28 읽는 시간: 7 분

MutableState는 Jetpack Compose에서 변경 가능한 관찰 가능한 값을 위한 컨테이너를 나타내는 인터페이스입니다. 이는 Compose 반응형 시스템의 기초입니다: MutableState의 값이 setter를 통해 변경될 때마다 Compose Runtime이 모든 읽기 컴포넌트에 알리고 재조합을 시작합니다. Google Android Developers, 2026에 따르면, 선언적 UI에서 상태를 올바르게 다루기 위해 MutableState를 이해하는 것이 필수적입니다.

주요 포인트

  • MutableState — 하나의 value 텍스터리를 가진 compose.runtime 인터페이스 (getter + setter)
  • State — 읽기 전용 부모 인터페이스, MutableState가 쓰기 기능 추가
  • 재조합은 활성화된 snapshot 사이클 내에서 value의 setter를 호출할 때 시작됨
  • SnapshotMutationPolicy가 재조합에 중요한 변경으로 간주되는 시점을 결정
  • MutableIntState 및 유사 — MutableState의 최적화된 프리미티브 버전

Jetpack Compose에서 MutableState란

MutableState는 androidx.compose.runtime 패키지의 인터페이스로, 하나의 텍스터리를 선언합니다: override var value: T. getter는 현재 값을 반환하고, setter는 새 값을 쓰고 Compose Runtime에 변경을 알립니다. 이 인터페이스는 State<T>를 상속받으며, 여기서 value는 읽기 전용입니다. 이 고층 아키텍처는 액세스 분리를 가능하게 합니다: 값만 읽으면 되는 컴포넌트는 State<T>를 받고, 소유자 컴포넌트는 MutableState<T>를 받습니다.

MutableState의 기본 구현은 내부 클래스 SnapshotMutableStateImpl이며, 변경 추적을 위해 스냅샷 메커니즘을 사용합니다. value의 setter가 호출되면, 현재 스냅샷이 기록을 기록하고 등록된 모든 ObservedScope를 무효로 표시합니다. 이 스코프(보통 Composable 함수)는 다음 프레임에서 재조합됩니다. Lock-free snapshot 아키텍처 덕분에 전체 과정이 동기적이고 잠금 없이 일어납니다.

State vs MutableState: State는 공용 컴포넌트 API에 사용되는 읽기 전용 인터페이스입니다. Composable 함수 매개변수를 State<Int>로 선언하면, 컴포넌트가 상태를 읽을 수는 있지만 변경할 수 없다는 것을 보장합니다. MutableState는 소유자 컴포넌트 내에서 사용됩니다. 이 분리는 권한 없는 변경을 방지하는 Compose의 기본 실무 중 하나입니다.

State, MutableState 및 파생 인터페이스의 계층

Compose에서 상태 인터페이스의 계층에는 여러 레벨이 있습니다. 최상위에는 읽기 전용 value를 가진 State<T>가 있습니다. 그 아래에는 읽기-기록 value를 가진 MutableState<T>가 있습니다. 그 다음으로는 전문 프리미티브 버전인 MutableIntState, MutableFloatState, MutableLongState, MutableBooleanState 등이 있으며, 프리미티브 박싱을 피합니다.

MutableDoubleState와 MutableLongState는 더 드물지만 존재합니다. 컬렉션 인터페이스: MutableListState — 목록 내 변경 추적 용, MutableStateMap — 맵 용. 이 각 인터페이스는 특정 시나리오에 최적화되어 있으며, 추가 컬렉션 조작 메소드로 기본 MutableState를 확장합니다.

SnapshotStateList와 SnapshotStateMap은 스냅샷과 호환되는 변경 가능한 목록 및 맵의 구현입니다. 이들은 단순한 값 대체만 아니라 내부 변경(목록에 항목 추가, 제거, 기존 항목 수정)을 추적할 수 있습니다. 이러한 구조에 대해 mutableStateListOf()와 mutableStateMapOf()가 해당하는 관찰 가능한 컬렉션을 생성합니다.

인터페이스목적생성 방법
State<T>읽기 전용 컨테이너
MutableState<T>읽기-기록 컨테이너mutableStateOf()
MutableIntState박싱 없는 프리미티브 IntmutableIntStateOf()
MutableFloatState박싱 없는 프리미티브 FloatmutableFloatStateOf()
SnapshotStateList관찰 가능한 목록mutableStateListOf()
SnapshotStateMap관찰 가능한 맵mutableStateMapOf()

SnapshotMutationPolicy: 재조합 시작 시점

SnapshotMutationPolicy는 MutableState의 변경이 중요한것으로 간주되는 시점을 결정하는 인터페이스입니다. mutableStateOf는 두 번째 인수로 policy를 받습니다. 표준 구현: structuralEquality() (equals), referentialEquality() (===), neverEqualPolicy() (항상 변경을 중요한것으로 간주). 사용자 정의 로직을 위한 자체 정책을 구현할 수 있습니다.

structuralEquality() — 기본 동작. Compose는 equals()를 사용하여 새 값과 예전 값을 비교합니다. 결과가 true이면 재조합이 시작되지 않습니다. 이는 동일한 필드를 가진 두 인스턴스가 같을으로 간주되는 프리미티브 및 data class에 편리합니다. 문제점: data class가 List를 포함하는 경우, equals()가 깊은 비교를 수행하여 큰 목록에 비용이 많이 들 수 있습니다.

referentialEquality() — ===을 사용하여 참조를 비교합니다. 재조합은 내용이 동일하더라도 다른 객체가 할당될 때만 시작됩니다. 이는 각 새 인스턴스가 변경을 보장하는 불변 data class에 최적적입니다. neverEqualPolicy() — 비교 없이 항상 변경을 중요한것으로 간주합니다. setter가 간형히 호출되고 equals에 시간을 할애할 필요가 없을 때 유용합니다.

kotlin
    // Policy comparison in practice
data class User(val name: String, val age: Int)

@Composable
fun UserProfile() {
    // structuralEquality: recomposition ONLY if data changed
    var user1 by remember {
        mutableStateOf(User("Alice", 30))
    }

    // referentialEquality: recomposition on ANY assignment
    var user2 by remember {
        mutableStateOf(User("Bob", 25),
            SnapshotMutationPolicy.referentialEquality())
    }

    // user1: copy() with same fields does NOT trigger recomposition
    // user2: even user2.copy() == user2 triggers recomposition (new ref)
}

프리미티브 State: MutableIntState, MutableFloatState, MutableLongState

프리미티브 MutableIntState 및 유사 인터페이스는 박싱 없이 프리미티브를 저장하는 전문 인터페이스입니다. 일반 MutableState<Int>는 Int를 Integer로 저장하여 기록마다 힙에 객체를 생성합니다. MutableIntState는 int(프리미티브)를 저장하여 박싱 오버헤드를 완전히 제거합니다. 이는 고빈도 업데이트(카운터, 스크롤 위치, 애니메이션 값)에 특히 중요합니다.

mutableIntStateOf(), mutableFloatStateOf(), mutableLongStateOf() — 프리미티브 MutableState를 생성하는 함수입니다. 인터페이스는 MutableIntState, MutableFloatState, MutableLongState라고 합니다. 이들은 각각 MutableState<Int>, MutableState<Float>, MutableState<Long>을 확장하고, 빠른 프리미티브 액세스를 위한 intValue 텍스터리를 추가합니다. 내부 구현은 AtomicInteger를 사용하여 잠금 없는 읽기/기록을 구현합니다.

사용: 카운터 (Int), 스크롤 위치 (Float offset), 타임스탬프 (Long). 대부분의 일상적인 시나리오에서 성능 차이는 느껴지지 않지만, 수천 개의 항목과 전이 애니메이션을 가진 LazyList에서 프리미티브 State는 농락한 향상을 제공합니다. Google은 일반적인 mutableStateOf 대신 일반적인 시나리오에 프리미티브 State를 사용할 것을 권장합니다.

kotlin
@Composable
fun ScrollCounter() {
    // Bad: boxing on every update
    var badCount by remember { mutableStateOf(0) }

    // Good: no boxing, primitive storage
    var goodCount by remember { mutableIntStateOf(0) }

    // Usage is identical
    Button(onClick = { goodCount++ }) {
        Text("Count: $goodCount")
    }
}

MutableState 작업 실습 예제

TodoList 컴포넌트를 고려해 봉시다. 여기서 MutableState는 두 가지 형태로 사용됩니다: 입력 상태를 위한 별도 변수와 동적 작업 목록을 위한 SnapshotStateList입니다. 둘 다 코드의 간결함을 위해 위임을 사용합니다.

kotlin
data class TodoItem(val id: Int, val text: String, val isDone: Boolean = false)

@Composable
fun TodoScreen() {
    var inputText by remember { mutableStateOf("") }
    val items = remember { mutableStateListOf() }

    Column(modifier = Modifier.padding(16.dp)) {
        Row {
            TextField(
                value = inputText,
                onValueChange = { inputText = it }
            )
            Button(onClick = {
                if (inputText.isNotBlank()) {
                    items.add(TodoItem(items.size, inputText))
                    inputText = ""
                }
            }) { Text("Add") }
        }

        LazyColumn {
            items(items) { item ->
                Row(modifier = Modifier.fillMaxWidth().clickable {
                    val idx = items.indexOf(item)
                    items[idx] = item.copy(isDone = !item.isDone)
                }) {
                    Checkbox(checked = item.isDone, onCheckedChange = null)
                    Text(item.text)
                }
            }
        }
    }
}

mutableStateListOf는 SnapshotStateList를 생성합니다 — 개별 요소의 변경을 추적하는 변경 가능한 목록입니다. items.add()와 items[n] = newValue가 호출되면 Compose가 변경을 감지하고 LazyColumn에서 변경된 요소만 재조합합니다. inputText은 일반 MutableState<String>입니다. 두 MutableState 유형(단일 및 컬렉션)의 조합은 양식과 목록이 있는 화면의 일반적인 패턴입니다.

자주 묻는 질문

MutableState를 remember 없이 사용할 수 있나요?

MutableState를 remember 없이 사용하면 매 재조합마다 새로 생성됩니다. mutableStateOf를 호출할 때마다 새 객체가 생성되고 이전 값은 잃합니다. State가 Composable 밖(ViewModel 등)에서 생성되지 않는 한, 재조합 간 State를 유지하기 위해 항상 remember를 사용하세요.

MutableState를 일반 변수로 변환하는 방법은?

snapshot { }을 통해 스냅샷 밖에서 .value를 한 번 읽으세요. 그러나 이는 반응성을 비활성화하여 변경이 더 이상 재조합을 유발하지 않습니다. 구독 없이 일회성 읽기를 위해서는 읽기 없이 스냅샷 내에서 currentValue()를 사용하세요.

mutableStateOf와 mutableIntStateOf 중 어느 것이 더 빠른가요?

mutableIntStateOf가 더 빠릅니다. int를 Integer로 박싱할 필요가 없기 때문입니다. 초당 수천 회 업데이트(애니메이션, 스크롤)가 있는 경우 할당 시간에서 30-50% 차이가 날 수 있습니다. 드물 업데이트(클릭, 텍스트 입력)의 경우 차이는 무시할 수 있습니다.

MutableState를 Composable 함수 인수로 사용할 수 있나요?

가능하지만 권장하지 않습니다. MutableState 대신 State(읽기 전용) + onValueChange 람다를 전달하세요. 이는 State Hoisting 패턴을 구현하고 컴포넌트를 재사용 가능하게 만듭니다. MutableState를 받들이는 컴포넌트는 단방향 데이터 흐름을 위반합니다.

사용자 정의 MutableState 구현을 만드는 방법은?

MutableState 인터페이스를 구현하고 getter와 setter가 있는 override var value를 제공하세요. setter에서 검증이나 로깅을 추가할 수 있습니다. Compose Runtime과의 호환성을 위해 사용자 정의 구현을 snapshotFlow로 감싸거나 snapshotIncrement를 사용하세요.

요약

  • MutableState — Compose에서 변경 가능한 관찰 가능한 상태를 위한 기본 인터페이스
  • State — 수정 권한 없이 데이터를 전달하기 위한 읽기 전용 버전
  • SnapshotMutationPolicy가 변경 시 재조합 시작 조건을 관리
  • 프리미티브 State (MutableIntState 등)가 박싱 오버헤드 제거
  • SnapshotStateList와 SnapshotStateMap이 컬렉션 내부 변경 추적
  • 재조합은 value의 setter 호출시 자동 시작
  • 권장: 상태 소유에는 MutableState, 아래로 전달에는 State 사용

턴키 방식의 모바일 애플리케이션을 개발해 드립니다

IT Sectr는 2017년부터 스타트업과 기업을 위한 iOS 및 Android 애플리케이션을 만듭니다. 저희가 상담해 드리고 최적의 솔루션을 제안하겠습니다.

프로젝트 논의

더 읽어보기