[Android/Compose] state(상태)와 remember 그리고 MutableState 란?

곽의진·2024년 4월 25일
5
post-thumbnail

이미 이 글을 읽는 당신은 너무나 잘 알겠지만 ^&^ Jetpack Compose는 선언형 UI입니다.

Jetpack Compose에서 StateFul한, 즉 데이터 변경 가능성이 있는 UI에 데이터를 Binding하기 위해서는 State(상태)라는 개념을 활용하여 데이터를 갱신 해주어야 데이터 최신화가 가능합니다.

이러한 과정에서 필요한 상태 관리는 Android 개발 패러다임에 많은 변화를 가져왔다고 개인적으로도 많이 느끼고 있어 이와 같은 주제를 택하게 되었습니다

Jetpack Compose는 이러한 State(상태)를 보다 명확하게 관리하도록 도와주는 다양한 API를 제공합니다. 오늘은 Jetpack Compose에서 State와 State를 관리하는 핵심 기능 중 하나인 remember에 대해 자세히 설명해보겠습니다!

State and composition

Jetpack Compose에서의 상태 관리는 여러 방식으로 이루어지며, 이 중에서 remember와 mutableStateOf API는 컴포저블 함수에서 상태를 효과적으로 다루는 데 중요한 역할을 합니다.

State(상태)란?

앱에서 시간에 따라 변할 수 있는 모든 값들을 의미합니다.

예시💡

  • 네트워크 연결이 설정되지 않았을 때 나타나는 스낵바
  • 블로그 포스트와 관련 댓글
  • 사용자가 클릭할 때 재생되는 버튼의 리플 애니메이션
  • 이미지 위에 사용자가 그릴 수 있는 스티커

특히 Compose는 선언형 UI 이므로 이를 업데이트하는 유일한 방법은 새 인수로 동일한 컴포저블을 호출하는 것입니다.

새 인수로 컴포저블을 다시 호출하게 된다면 상태가 업데이트될 때마다 recomposition(재구성)이 발생합니다.

예시 코드

@Composable
private fun HelloContent() {
    Column(modifier = Modifier.padding(16.dp)) {
        Text(
            text = "Hello!",
            modifier = Modifier.padding(bottom = 8.dp),
            style = MaterialTheme.typography.bodyMedium
        )
        OutlinedTextField(
            value = "",
            onValueChange = { },
            label = { Text("Name") }
        )
    }
}

그렇다면 해당 예시 코드를 잠깐 보실까요?

결과적으로, XML 기반의 명령형 뷰에서처럼 TextField와 같은 요소들이 자동으로 업데이트되지 않습니다. 컴포저블은 새로운 상태를 명시적으로 전달받아야 해당 상태에 맞게 업데이트될 수 있습니다.

그 이유는 TextField 컴포저블이 자체적으로 업데이트되지 않고 value 매개변수가 변경될 때 업데이트되기 때문입니다. 이는 Compose에서 Composition 및 Recomposition 작동하는 방식 때문입니다.

핵심 용어 📖

  • 구성(Composition): Compose 함수들이 실행되어 UI 요소를 트리 구조로 구성하는 것을 포함합니다.
  • 초기 구성(Initial Composition):
    이는 컴포저블 함수들이 처음 실행될 때 일어나는 과정입니다. 이때, Compose는 주어진 컴포저블 함수들을 통해 애플리케이션의 UI를 구축하고, 각 UI 요소를 적절한 위치와 함께 트리에 추가합니다.
  • 재구성(Recomposition):
    데이터나 상태가 변경될 때, Compose는 영향 받은 UI 부분만을 효율적으로 업데이트하기 위해 해당 컴포저블 함수를 다시 실행합니다. 이 과정을 통해 앱은 최신 상태를 반영하여 사용자에게 보여질 수 있습니다.

remember의 역할

Composable에서 State를 관리하는 방법은 대표적으로 remember가 있는데요, remember API의 기본적인 기능은 객체를 메모리에 저장하는 기능입니다.

이를 통해 remember는 컴포저블이 초기 구성(Initial Composition)에서 실행될 때 계산된 값을 "기억"하고, 재구성 시에 이 값을 유지하여 반환합니다. 이는 Compose의 효율성을 높여주는 메커니즘으로, 불필요한 계산을 방지하고 성능을 최적화합니다.

따라서, remember를 사용할 때는 그것이 재구성 시에 다시 호출되지 않고, 초기에 저장된 값을 계속해서 반환한다는 점을 이해하는 것이 중요합니다. 이는 상태 관리를 효과적으로 돕고, Compose UI의 성능을 유지하는 데 도움을 줍니다.

remember는 해당 객체를 Composition에 저장하며, remember를 호출한 컴포저블이 Composition에서 제거되면 저장된 객체를 "잊어버립니다"(즉, 객체를 메모리에서 제거합니다).

MutableState의 사용

mutableStateOf는 변화 가능한 상태를 생성하고, 이 상태가 변경될 때 관련된 컴포저블 함수들을 재구성하도록 합니다. 이는 Compose 런타임과 통합된 관찰 가능한 타입(MutableState)을 통해 이루어집니다.

예시 코드

interface MutableState<T> : State<T> {
  override var value: T
}

MutableState의 Recomposition(재구성) 유도

오잉 근데 MutableState는 어떻게 상태가 변경된 것을 컴포저블에 알리고 함수를 재구성 할 수 있도록 하는건가요??

mutableStateOf를 통해 생성된 MutableState 객체가 컴포저블 함수들을 재구성(recomposition)할 수 있는 주된 이유는 이 객체가 관찰 가능한 상태(observable state)를 제공하기 때문입니다.

관찰 가능한 상태 (Observable State)

MutableState는 상태의 변화를 자동으로 감지하고 이에 반응할 수 있는 구조로 설계되어 있습니다. 이 객체의 value 속성에 변화가 발생하면, Compose 런타임은 그 변화를 감지하고 value를 사용하는 모든 컴포저블 함수들을 자동으로 재구성하도록 스케줄링합니다.

이러한 방식은 다음과 같은 흐름으로 작동합니다:

  1. 상태 변경 감지: MutableState 내의 value가 변경될 때, 이 상태 객체는 등록된 리스너들(여기서는 컴포저블 함수들)에게 변화를 알립니다.
  2. Recomposition 트리거: 상태를 사용하는 컴포저블 함수들은 변화를 통지받고, 필요에 따라 자신을 재구성하기 위한 요청을 Compose 런타임에 보냅니다.
  3. 최소한의 UI 업데이트: Compose는 선언적 UI 접근 방식을 사용하여, 실제로 변경이 필요한 UI 부분만을 업데이트합니다. 이는 전체 UI를 다시 그리는 대신, 변경된 데이터에 의존하는 UI 요소만을 재구성하여 성능을 최적화합니다.

MutableState 객체를 선언하는 방법은 아래와 같이 세 가지가 있습니다:

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

🔫 간단히 짚어보는 구조 분해 선언 (componentN 함수들의 사용)

Kotlin에서 구조 분해 선언은 복합적인 값을 각각의 변수로 분해하여 할당할 수 있게 해줍니다.

각 요소를 개별 변수에 할당하려면 해당 타입은 component1(), component2(), 등의 메서드를 제공해야 합니다.

MutableState는 아래와 같은 두 개의 메서드를 통해 구조 분해를 지원합니다

  • component1() 함수는 MutableState의 value를 반환합니다. 이는 상태의 현재 값을 얻는 데 사용됩니다.
  • component2() 함수는 value의 값을 설정하는 함수를 반환합니다. 이는 상태를 업데이트할 때 사용되는 세터(setter) 함수입니다.

이 두 함수 덕분에, MutableState 객체는 구조 분해를 통해 value와 세터 함수를 쉽게 할당할 수 있게 되며, 이는 코드의 가독성과 사용의 용이성을 높입니다.

remember와 rememberSaveable의 차이

remember는 Recomposition을 거쳐도 상태를 유지하지만, Configuration Change 즉, 구성 변경(예: 화면 회전)이 발생하면 상태를 유지하지 못합니다. 이에 대한 해결책으로 rememberSaveable을 사용할 수 있습니다.

rememberSaveable은 자동으로 Bundle에 저장될 수 있는 값들을 저장하며, 커스텀 저장 객체를 통해 다른 유형의 값들도 저장할 수 있습니다.

아래는 remember와 mutableStateOf를 사용한 간단한 예시입니다:

사용 예시

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

이 예시에서는 사용자가 입력한 이름을 저장하고, 이름이 비어 있지 않은 경우에만 Text를 표시합니다.

주의사항

가변 객체를 상태로 사용하는 것은 데이터가 잘못되거나 오래된 것으로 보이게 할 수 있으므로, 가능하면 State<List>와 같은 관찰 가능한 데이터 홀더와 listOf()와 같은 불변 컬렉션을 사용하는 것이 좋습니다.

더 나아가 ImmutableList가 무엇인지도 살펴보면 좋을 것 같습니다! Compose에서는 List또한 안정된 타입이 아니기에 불필요한 리컴포지션이 발생할 가능성이 있기 때문이죠!

Jetpack Compose의 remember와 mutableStateOf는 Compose의 선언적 UI 패턴에 맞게 상태를 관리하고 UI를 동적으로 API입니다.

이로 인해서 저희 안드로이드 개발자들은 쉽게 사용자 경험을 향상시키고, 코드의 복잡성을 줄일 수 있다고 생각됩니다! 다같이 열심히 공부해서 좋은 프로젝트 설계 실력을 만들어 나가봅시다!

참고 링크

profile
Android Developer

0개의 댓글