mutableStateOf는 Jetpack Compose의 함수로, 변경 가능한 관찰 가능한 상태 컨테이너를 생성합니다. 이 컨테이너 내부의 값이 변경되면 Compose는 이 상태를 읽는 모든 컴포넌트의 재구성을 자동으로 트리거합니다. mutableStateOf가 없으면 데이터가 변경될 때 UI가 반응적으로 업데이트될 수 없습니다. Google Android Developers, 2026에 따르면 mutableStateOf는 Compose에서 로컬 상태의 기본 구성 요소입니다.
핵심 포인트
mutableStateOf는 compose.runtime 패키지의 함수로, 값을 저장하고 Compose Runtime에 변경을 알릴 수 있는 MutableState<T> 객체를 생성합니다. 시그니처: fun <T> mutableStateOf(value: T, policy: SnapshotMutationPolicy<T> = structuralEquality()): MutableState<T>. policy 매개변수는 변경이 중요하다고 간주되는 시점을 결정합니다 — 구조적 동등성, 참조 동등성, 또는 절대 없음.
MutableState는 단일 프로퍼티 value를 가진 인터페이스입니다: 읽기용 getter와 쓰기용 setter입니다. setter가 호출되면 Compose Runtime은 스냅샷에 변경을 기록하고 이 State 변수를 읽는 모든 Composable 함수를 재구성이 필요하다고 표시합니다. 이 프로세스는 단일 스냅샷 주기 내에서 동기적으로 발생하여 계단식 변경 중 중간 상태를 제거합니다.
매개변수 policy — mutableStateOf의 두 번째 인수로, 비교 동작을 정의합니다. structuralEquality()는 equals()를 확인합니다 — 이것이 기본 동작입니다. referentialEquality()는 ===(참조 동등성)을 확인합니다. neverEqual()은 모든 할당을 변경으로 간주합니다. policy 선택은 동일한 값을 할당할 때 재구성이 트리거되는지 여부에 영향을 줍니다.
관찰 가능한 상태를 선언하는 가장 간단한 방법은 mutableStateOf를 remember와 함께 사용하는 것입니다. remember가 없으면 각 재구성마다 새 State가 생성되고 이전 변경 사항이 모두 손실됩니다. remember는 Composable 함수가 컴포지션에 남아 있는 한 동일한 MutableState가 일련의 재구성을 유지하도록 보장합니다.
@Composable
fun Counter() {
// 위임 없음: .value를 통한 읽기/쓰기
val count = remember { mutableStateOf(0) }
Button(onClick = { count.value++ }) {
Text("Count: ${count.value}")
}
}
@Composable
fun CounterDelegated() {
// 위임 있음: var + by = Property Delegation
var count by remember { mutableStateOf(0) }
Button(onClick = { count++ }) {
Text("Count: $count")
}
}
두 접근 방식의 차이는 구문적입니다. Property Delegation(by)은 Kotlin 규칙을 사용합니다: 컴파일러가 읽기와 쓰기를 위해 getValue() 및 setValue() 호출을 생성합니다. 이는 count.value에 직접 접근하는 것과 동일하지만 일반 변수로 작업하는 것처럼 보입니다. 두 접근 방식 모두 기능적으로 동일합니다: Compose는 표기법에 관계없이 getter에서 읽기와 setter에서 쓰기를 추적합니다.
| 형식 | 코드 | 읽기 | 쓰기 |
|---|---|---|---|
| 위임 없음 | val count = mutableStateOf(0) | count.value | count.value = n |
| 위임 있음 | var count by mutableStateOf(0) | count | count = n |
Kotlin의 위임 프로퍼티 메커니즘은 Compose의 기능이 아니라 언어에 내장된 기능입니다. 모든 클래스는 getValue(thisRef, property) 및 setValue(thisRef, property, value) 연산자를 구현할 수 있으며, 그 후 해당 인스턴스를 by 키워드와 함께 사용할 수 있습니다. MutableState는 정확히 이렇게 작동합니다: getValue는 현재 값을 반환하고 setValue는 새 값을 할당합니다.
중요한 차이점: val vs var. mutableStateOf는 val과 var 모두에 할당될 수 있습니다. val(val count = mutableStateOf(0))의 경우 MutableState 객체 자체는 불변이지만 해당 value 프로퍼티는 변경될 수 있습니다. var(var count by mutableStateOf(0))의 경우 위임은 원시 타입으로 작업하는 듯한 환상을 만들지만 setter는 실제로 MutableState에서 setValue를 호출합니다. val과 var의 선택은 .value에 대한 명시적 접근과 암시적 접근 사이의 선택입니다.
State 위임은 코드를 단순화하는 구문 설탕이지만 메커니즘은 변경하지 않습니다. Kotlin 컴파일러는 var x by mutableStateOf(0)을 mutableStateOf.getValue()와 mutableStateOf.setValue()를 호출하는 getter/setter로 변환합니다. 생성된 바이트코드에서 by를 사용한 val과 var 사이에는 차이가 없습니다 — 둘 다 동일한 MutableState 컨테이너를 통해 작동합니다.
// Compose State를 위한 사용자 정의 위임자
class ValidatedState<T>(initialValue: T) {
private val state = mutableStateOf(initialValue)
operator fun getValue(thisRef: Any?, property: KProperty<*>) = state.value
operator fun setValue(thisRef: Any?, property: KProperty<*>, value: T) {
if (value != state.value) {
state.value = value
}
}
}
@Composable
fun Test() {
var text by remember { ValidatedState("") }
}
Snapshot은 병렬 변경 중 State 읽기 일관성을 보장하는 Compose Runtime의 메커니즘입니다. Composable 함수가 mutableStateOf를 읽으면 스냅샷이 현재 값을 기록합니다. 컴포지션 중에 다른 변경이 동일한 State에 쓰면 스냅샷은 쓰기를 보지만 일관되지 않은 데이터 읽기는 허용하지 않습니다 — 읽기는 항상 스냅샷 시작 시 유효한 값을 반환합니다.
setter mutableStateOf.value = newValue가 호출되면 Compose Runtime이 즉시 재구성을 트리거하지 않습니다. 대신 변경이 현재 스냅샷에 등록됩니다. 스냅샷이 적용될 때(프레임 경계에서) Compose는 변경된 State 목록을 스캔하고 읽기 컴포넌트를 Invalid로 표시합니다. 다음 프레임에서만 재구성이 시작됩니다. 이렇게 하면 계단식 변경 중에 UI가 수십 번 다시 그려지지 않습니다.
전역 및 로컬 스냅샷: 기본적으로 mutableStateOf는 자동으로 적용되는 전역 스냅샷에서 작동합니다. Snapshot.takeSnapshot()을 통해 부작용 없이 격리된 읽기를 위한 로컬 스냅샷을 만들 수 있습니다. 이는 변경 사항을 구독하지 않고 State를 읽어야 할 때 Modifier 내에서 사용됩니다. 이 접근 방식은 성능을 최적화하고 예기치 않은 재구성을 방지합니다.
실제 시나리오를 생각해 보겠습니다 — 이메일, 비밀번호, 로딩 상태의 세 가지 필드가 있는 로그인 양식입니다. 세 필드 모두 mutableStateOf를 사용하지만 서로 다른 policy와 다른 중첩 수준으로 사용합니다. email은 위임을 사용하고 password는 직접 접근을 사용합니다.
data class LoginState(
val email: String = "",
val password: String = "",
val isLoading: Boolean = false,
val error: String? = null
)
@Composable
fun LoginForm(onLogin: (String, String) -> Unit) {
// 양식용 단일 State, policy = referentialEquality
var formState by remember {
mutableStateOf(LoginState(), SnapshotMutationPolicy.referentialEquality())
}
val isValid = remember(formState) {
formState.email.contains("@") && formState.password.length() >= 6
}
Column(modifier = Modifier.padding(16.dp)) {
OutlinedTextField(
value = formState.email,
onValueChange = { formState = formState.copy(email = it) },
label = { Text("이메일") }
)
OutlinedTextField(
value = formState.password,
onValueChange = { formState = formState.copy(password = it) },
label = { Text("비밀번호") },
visualTransformation = PasswordVisualTransformation()
)
Button(
onClick = { onLogin(formState.email, formState.password) },
enabled = isValid
) {
Text("로그인")
}
}
}
이 예제에서 mutableStateOf는 사용자 정의 데이터 클래스 LoginState와 policy referentialEquality와 함께 사용됩니다. 이는 copy()를 통해 새 LoginState 인스턴스가 할당된 경우에만 재구성이 트리거됨을 의미합니다. isValid는 formState를 기반으로 계산되며 변경될 때만 다시 계산됩니다. 이 접근 방식은 재구성을 명확하게 제어합니다: 각 양식 필드는 새 복사본을 만들어야만 변경됩니다.
자주 묻는 질문
mutableStateOf는 스냅샷 내에서 작동하는 Compose 특정 컨테이너입니다. StateFlow는 kotlinx.coroutines.flow에서 가져온 것으로 Compose에 바인딩되지 않습니다. mutableStateOf는 자동으로 재구성을 트리거하지만 StateFlow는 collectAsState()가 필요합니다. Composable 내부의 UI 상태에는 mutableStateOf가 선호됩니다.
네, mutableStateOf는 Composable 함수 외부에서 호출할 수 있지만 추적되지 않습니다. UI에서 반응성을 위해 State를 Composable 내부에서 읽어야 합니다. 많은 ViewModel이 StateFlow를 통해 UI에 상태를 전달하기 위해 MutableStateField(mutableStateOf의 래퍼)를 사용합니다.
Snapshot 시스템이 일관성을 보장합니다: 각 재구성은 스냅샷 시작 시 일관된 상태를 봅니다. 다른 스레드의 변경 사항은 프레임 경계에서 원자적으로 적용되어 단일 컴포지션 내 읽기 중 경합 조건을 제거합니다.
새 값을 할당하세요: count.value = 0(또는 위임 시 count = 0). State를 완전히 다시 생성해야 하는 경우 키가 있는 remember를 사용하세요: remember(key) { mutableStateOf(initial) } — 키가 변경되면 State가 새로 생성됩니다.
Composer는 변경 사항을 그룹화하는 스냅샷을 사용합니다: 단일 프레임에서 수백 개의 할당이 있어도 재구성은 한 번만 실행됩니다. 매우 빈번한 업데이트(애니메이션)에는 Animatable 또는 animate*AsState를 사용하세요 — 프레임별 업데이트에 최적화되어 있습니다.
요약
턴키 방식의 모바일 애플리케이션을 개발해 드립니다
IT Sectr는 2017년부터 스타트업과 기업을 위한 iOS 및 Android 애플리케이션을 만듭니다. 저희가 상담해 드리고 최적의 솔루션을 제안하겠습니다.