반응형
Notice
Recent Posts
Recent Comments
Link
«   2025/02   »
1
2 3 4 5 6 7 8
9 10 11 12 13 14 15
16 17 18 19 20 21 22
23 24 25 26 27 28
Tags
more
Archives
Today
Total
관리 메뉴

안드로이드 개발자 노트

Compose의 State 본문

Android

Compose의 State

어리둥절범고래 2025. 2. 15. 16:45
반응형

앱의 상태(State)는 시간이 지나며 변하는 값으로, 데이터베이스부터 UI 요소까지 포함됩니다.

 

 

 

상태 및 컴포지션

 

Compose에서는 UI를 업데이트하려면 새 인수로 컴포저블을 다시 호출해야 합니다. 상태가 변경될 때마다 재구성이 실행되며, TextField 같은 요소도 자동으로 업데이트되지 않습니다. 따라서 새 상태를 명시적으로 전달해야 합니다.

 

 

 

컴포저블의 상태

 

컴포저블 함수는 remember를 사용해 객체를 메모리에 저장할 수 있습니다.

remember는 초기 컴포지션 시 값을 저장하고, 이후 리컴포지션에서도 해당 값을 유지합니다.

mutable / immutable 객체 모두 저장할 수 있으며, 컴포저블이 컴포지션에서 제거되면 저장된 객체도 삭제됩니다.

 

또한, mutableStateOf는 Compose에서 감지할 수 있는 MutableState<T>를 생성하여 상태 변화를 UI에 반영할 수 있도록 합니다.

value가 변경되면 이를 읽는 컴포저블이 자동으로 리컴포지션됩니다.
MutableState를 선언하는 방법은 다음과 같이 세 가지가 있으머, 각 방법은 같은 기능을 합니다.

val mutableState = remember { mutableStateOf(default) }  
var value by remember { mutableStateOf(default) }  
val (value, setValue) = remember { mutableStateOf(default) }

 

 

remember로 저장한 값은 다른 컴포저블의 매개변수로 사용하거나, 조건문에 활용해 UI를 변경할 수 있습니다. 예를 들어, name이 비어 있을 때 인사말을 표시하지 않으려면 if 문에서 상태를 사용하면 됩니다.

@Composable
fun HelloContent() {
    Column(modifier = Modifier.padding(16.dp)) {
        var name by remember { mutableStateOf("") }
        if (name.isNotEmpty()) {
            Text(
                text = "Hello, $name!",
                modifier = Modifier.padding(bottom = 8.dp),
                style = MaterialTheme.typography.bodyMedium
            )
        }
        OutlinedTextField(
            value = name,
            onValueChange = { name = it },
            label = { Text("Name") }
        )
    }
}

remember가 재구성 과정 전체에서 상태를 유지하는 데 도움은 되지만 구성 변경 전반에서는 상태가 유지되지 않습니다. 이 경우에는 rememberSaveable을 사용해야 합니다.

 

 

 

지원되는 기타 상태 유형

 

MutableState<T>는 Compose에서 가장 기본적인 상태 관리 도구이지만, Compose는 다양한 관찰 가능한 유형(observable types)을 지원합니다.

API 상태
Flow: collectAsStateWithLifecycle() Flow 데이터를 Lifecycle.State.STARTED 상태에서만 수집하여 백그라운드에서 불필요한 작업을 방지함
Flow: collectAsState() Flow 데이터를 항상 수집하여 상태로 변환하지만, 백그라운드에서도 동작하므로 리소스 낭비 가능성이 있음.
LiveData: observeAsState() LiveData를 State<T>로 변환하여 Compose UI에서 관찰할 수 있도록 함.
RxJava: subscribeAsState() Observable 또는 Flowable을 Compose에서 State<T>로 변환하여 UI에서 활용할 수 있도록 함.

 

 

 

스테이트풀(Stateful)과 스테이트리스(Stateless)

 

스테이트풀(Stateful): 내부 상태를 가진 컴포저블로, 상태를 관리하지 않아도 사용할 수 있지만, 재사용성이 떨어지고 테스트가 어려울 수 있습니다. 호출자가 상태를 신경 쓰지 않는 경우 유용합니다.

스테이트리스(Stateless): 상태를 갖지 않는 컴포저블로, 상태를 호출자가 관리해야 합니다. 재사용성이 높고 테스트가 용이하며, 상태를 제어하거나 끌어올려야 하는 경우 적합합니다.

 

 

 

상태 호이스팅

 

상태 호이스팅은 컴포저블을 스테이트리스(Stateless)로 만들기 위해 상태를 컴포저블의 호출자로 옮기는 패턴입니다.

일반적으로 상태는 두 매개변수로 관리됩니다.

 

상태 호이스팅은 컴포저블 내부의 상태를 외부로 이동하여 호출자가 직접 상태를 관리하도록 하는 패턴입니다. 이를 통해 컴포저블을 스테이트리스(Stateless)로 만들고 재사용성을 높이며, 테스트와 유지보수를 용이하게 할 수 있습니다.

 

  • value: 표시할 값
  • onValueChange: 값 변경 요청 이벤트

호이스팅된 상태는 다음과 같은 장점이 있습니다.

 

  1. 하나의 상태 관리 (단일 정보 소스): 상태를 여러 곳에서 따로 관리하지 않고 한 곳에서 관리하여 버그를 줄일 수 있음
  2. 안전한 상태 변경 (캡슐화): 상태를 직접 변경할 수 있는 컴포저블을 제한하여 예상치 못한 변경을 방지
  3. 여러 곳에서 사용 가능 (공유 가능): 하나의 상태를 여러 컴포저블에서 함께 사용할 수 있음
  4. 이벤트 조작 가능 (가로채기 가능): 호출자가 이벤트를 수정하거나 무시할 수 있어 더 유연하게 상태를 관리할 수 있음
  5. 컴포저블과 분리 가능 (분리됨): 상태를 ViewModel 같은 외부 저장소로 옮길 수 있어 유지보수가 쉬워짐

 

HelloScreen에서 name 상태를 관리하고, HelloContent는 이 상태를 UI에 반영하는 역할을 합니다.
즉, HelloContent는 상태를 직접 관리하지 않고 외부에서 전달받은 값(value)과 이벤트 핸들러(onValueChange) 를 사용하여 동작합니다.

@Composable
fun HelloScreen() {
    var name by rememberSaveable { mutableStateOf("") }

    HelloContent(name = name, onNameChange = { name = it })
}

@Composable
fun HelloContent(name: String, onNameChange: (String) -> Unit) {
    Column(modifier = Modifier.padding(16.dp)) {
        Text(
            text = "Hello, $name",
            modifier = Modifier.padding(bottom = 8.dp),
            style = MaterialTheme.typography.bodyMedium
        )
        OutlinedTextField(value = name, onValueChange = onNameChange, label = { Text("Name") })
    }
}

단일 정보 소스 → 상태를 HelloScreen에서 관리하여 버그를 줄일 수 있음
캡슐화 → HelloContent 내부에서 상태를 직접 변경하지 않도록 제어 가능
공유 가능 → 같은 상태를 여러 UI 요소에서 사용할 수 있음
가로채기 가능 → onNameChange에서 입력값을 가공하거나 필터링 가능
분리됨 → HelloContent는 상태와 독립적이므로 재사용 가능

 

HelloScreen은 상태를 관리하는 Stateful 컴포저블, HelloContent는 상태를 전달받아 UI를 그리는 Stateless 컴포저블입니다.
이처럼 상태를 별도의 상위 컴포저블에서 관리하는 것이 상태 호이스팅의 핵심 개념입니다.



Compose에서 상태 복원 (rememberSaveable)

rememberSaveable은 remember처럼 재구성(recomposition) 시 상태를 유지하면서, 액티비티 또는 프로세스가 재생성될 때도 상태를 보존합니다. 이는 Android의 저장된 인스턴스 상태(savedInstanceState) 메커니즘을 활용하며, 내부적으로 Bundle에 저장할 수 있는 데이터(Int, String 등)를 자동으로 저장하여 화면 회전 등의 상황에서도 상태를 유지할 수 있도록 합니다.

@Composable
fun <T : Any> rememberSaveable(
    vararg inputs: Any?,
    saver: Saver<T, out Any> = autoSaver(),
    key: String? = null,
    init: () -> T
): T
  1. inputs(keys): Any? 타입의 가변 인수(vararg)로, 상태를 저장할 때 사용되는 입력 값입니다. 이 값들이 변경되면 저장된 상태가 무효화되고, 이후 컴포저블이 재구성될 때 init 람다가 다시 실행됩니다.
  2. saver: 상태를 저장하고 복원하는 방법을 정의하는 Saver<T, out Any> 객체입니다. 기본값은 autoSaver()로, 이는 자동으로 객체를 저장하고 복원하는 방법을 제공합니다. 필요에 따라 사용자 정의 Saver를 제공하여, 상태를 원하는 형식으로 변환할 수 있습니다.
  3. key: (선택적) 상태를 저장할 때 사용되는 키입니다. 기본값은 null이며, 주로 inputs를 기준으로 자동으로 키가 생성됩니다. 특정 키를 지정하면, 동일한 키로 상태를 저장하고 복원할 수 있습니다.
  4. init: 상태를 초기화하는 calculation 람다 함수입니다. 이 함수는 상태를 최초로 생성할 때 호출되며, T 타입의 값을 반환합니다. 상태가 재구성될 때마다 inputs의 값이 변경되지 않으면 저장된 상태가 그대로 사용됩니다.

 

 

 

remember vs rememberSaveable

  remember rememberSaveable
재구성(Recomposition) 중 상태 유지 ✅ 가능 ✅ 가능
화면 회전(Activity 재생성) 후 상태 유지 ❌ 불가능 ✅ 가능
앱이 종료된 후 상태 유지  ❌ 불가능 ❌ 불가능
Bundle에 저장 가능해야 함  ❌ 필요 없음 ✅ 가능

 

rememberSaveable은 내부적으로 savedInstanceState 메커니즘을 활용하기 때문에 Bundle에 저장할 수 있는 타입으로 저장해야 합니다. Bundle에 추가할 수 없는 항목을 저장하려는 경우 몇 가지 옵션이 있습니다. 

 

 

Parcelize

@Parcelize
data class City(val name: String, val country: String) : Parcelable

@Composable
fun CityScreen() {
    var selectedCity = rememberSaveable {
        mutableStateOf(City("Madrid", "Spain"))
    }
}

 

MapSaver

data class City(val name: String, val country: String)

val CitySaver = run {
    val nameKey = "Name"
    val countryKey = "Country"
    mapSaver(
        save = { mapOf(nameKey to it.name, countryKey to it.country) },
        restore = { City(it[nameKey] as String, it[countryKey] as String) }
    )
}

@Composable
fun CityScreen() {
    var selectedCity = rememberSaveable(stateSaver = CitySaver) {
        mutableStateOf(City("Madrid", "Spain"))
    }
}

 

 

 

키가 변경될 때 remember 계산 다시 실행하기

 

remember API는 MutableState와 함께 자주 사용됩니다.

var name by remember { mutableStateOf("") }

여기서 remember 함수를 사용하면 MutableState 값이 재구성 시에도 유지됩니다.

일반적으로 remember는 calculation 람다 파라미터를 받습니다. remember가 처음 실행되면 계산 람다를 호출하여 그 결과를 저장합니다. 이후 재구성 시에는 마지막에 저장된 값을 반환합니다.

상태를 캐시하는 것 외에도, remember는 초기화나 계산이 비용이 많이 드는 객체나 연산 결과를 컴포지션에 저장하는 데 사용할 수 있습니다.

 

 

remember API는 key 또는 여러 개의 keys를 받을 수 있는데, 이러한 키 중 하나라도 변경될 경우 다음번에 함수가 재구성될 때 remember는 캐시를 무효화하고 계산 람다 블록을 다시 실행합니다. 이 메커니즘을 통해 컴포지션 내 객체의 전체 기간을 제어할 수 있습니다.

 

 

이 예시는 remember와 key를 활용해 성능 최적화를 하는 방식입니다. ShaderBrush는 배경 이미지를 반복적으로 생성하는데, 비용이 크므로 remember로 저장됩니다. avatarRes 값이 변경될 때만 ShaderBrush를 새로 생성하고, 이를 배경에 적용합니다. 이렇게 하면 이미지가 변경되지 않으면 ShaderBrush를 다시 만들지 않고 성능을 최적화할 수 있습니다.

@Composable
private fun BackgroundBanner(
    @DrawableRes avatarRes: Int,
    modifier: Modifier = Modifier,
    res: Resources = LocalContext.current.resources
) {
    val brush = remember(key1 = avatarRes) {
        ShaderBrush(
            BitmapShader(
                ImageBitmap.imageResource(res, avatarRes).asAndroidBitmap(),
                Shader.TileMode.REPEAT,
                Shader.TileMode.REPEAT
            )
        )
    }

    Box(modifier = modifier.background(brush)) {
        /* ... */
    }
}

 

이 예시에서는 MyAppState라는 상태 홀더 클래스를 사용하여 상태를 호이스팅합니다. rememberMyAppState 함수는 remember와 windowSizeClass를 활용하여 MyAppState 인스턴스를 유지합니다. windowSizeClass가 변경되면 상태 홀더 클래스를 다시 생성합니다. 이는 화면 크기 변화나 기기 회전 시 최신 상태를 유지하는 Compose의 일반적인 패턴입니다.

@Composable
private fun rememberMyAppState(
    windowSizeClass: WindowSizeClass
): MyAppState {
    return remember(windowSizeClass) {
        MyAppState(windowSizeClass)
    }
}

@Stable
class MyAppState(
    private val windowSizeClass: WindowSizeClass
) { /* ... */ }

 

 

 

 

 

 

 

 

https://developer.android.com/develop/ui/compose/state?hl=ko

반응형

'Android' 카테고리의 다른 글

상태를 어디에 호이스팅할 것인가  (0) 2025.02.17
Compose의 Side-effects  (1) 2025.02.09
Compose의 단계  (0) 2025.02.09