[JetPack Compose] State, remember 그리고 MutableStateOf
명령형 UI와 달리 선언형 UI는 값을 업데이트하는 방식으로 State를 사용한다.
명령형 UI와 선언형 UI의 차이를 알고 싶은 독자라면 해당 링크에서 정보를 얻길 바란다.
JetPack Compose에서는 이런 State를 안전하고 편리하고 사용할 수 있도록 도와주는 여러 API가 존재한다.
최종적으로는 remember, MutableState에 대해서 알아볼 것이며 그 전에, State가 무엇인지 살펴보자.
State
시간이 지남에 따라 변경될 수 있는 모든 값을 말한다.
예를 들어, 다음의 값들은 State가 될 수 있다.
- 네트워크 연결을 설정할 수 없을 때를 표시하는 스낵바.
- 블로그 게시물과 관련 댓글.
- 사용자가 버튼을 클릭하면 재생되는 리플 애니메이션.
- 사용자가 이미지 위에 그릴 수 있는 스티커.
어떤 값이 변경함에 따라 UI를 렌더링하고 싶다면 State를 컴포지션 트리에 저장하는 작업이 필요하다. 당연히 등록하지 않으면 값을 관찰할 수 없어 리컴포지션이 일어나지 않는다. 어떻게하면 리컴포지션이 발생하는지, 발생하지 않는 코드와 비교하며 알아보자.
다음은 리컴포지션이 일어나지 않는 간단한 예시다.
리컴포지션이 발생하지 않는 코드
@Composable
fun StateExample() {
var count = 0
Column {
Button(onClick = {
count += 1
Log.d("SideEffectEx", "count is $count")
}) {
Text("count is $count")
}
}
}
count라는 값을 컴포지션 트리에 등록하지 않아서 count라는 값이 변경되어도 리컴포지션이 발생하지 않는다. 그렇다면 이번엔 count를 remember과 mutableStateOf를 사용하여 리컴포지션이 발생하도록 코드를 수정해보자.
리컴포지션이 발생하는 코드
@Composable
fun StateExample() {
var count = remember { mutableStateOf(0) }
Column {
Button(onClick = {
count.value += 1
Log.d("SideEffectEx", "count is ${count.value}")
}) {
Text("count is ${count.value}")
}
}
}
count라는 값을 컴포지션 트리에 등록하고 관리하기위해 사용한 API는 remember과 mutableStateOf이다. 이 둘을 살펴보면서, 리컴포지션이 어떻게 발생하는지 알아보자.
rememeber
Jetpack Compose에서 컴포저블 함수가 재구성(Recomposition)될 때도 값을 저장하고 재사용하기 위해 사용하는 API다. 컴포지션 트리의 특정 노드에 값을 저장하여, 불필요한 값 재계산을 방지하고 성능을 최적화하는 데 사용된다.
remember의 특징
1. 컴포지션 트리에 값 저장
- 컴포지션 트리의 특정 노드에 저장하여, 이 노드가 유지되는 동안 저장된 값을 재사용한다.
2. 리컴포지션 중 값 유지
- 컴포지션 트리에 초기화할 때만 초기화 블록이 실행되며, 리컴포지션 될 때는 초기화 블록은 실행되지 않는다.
- 컴포저블 함수가 리컴포지션될 때 remember는 이전에 저장된 값을 반환하여, 불필요한 재계산을 방지한다.
3. 컴포지션 제거 시 값 폐기
- 컴포지션 트리 혹은 값이 저장되어 있는 특정 노드가 제거되면(key 변경, 조건부 UI 제외 등), remember로 저장된 값도 폐기된다.
4. 컴포지션 생명주기 동안 값 유지
- 컴포지션의 생명주기에 따라 값을 유지하며, 상태 관리의 기본 단위를 제공한다.
remember의 기능은 간단히 말해서 특정 노드에 값을 저장하는 역할이다. 이 때문에 값이 리컴포지션 중에도 유지되야하는 책임을 가지고 있다. remember API만 사용하는 경우는 계산 비용이 큰 값을 캐싱하거나, 고정된 데이터를 저장하는데 사용한다.
🔍참고
Compose 런타임은 직접적으로 메모리를 관리하지 않고, 가비지 컬렉터에 의존한다. 컴포지션 트리는 객체에 대한 참조를 유지하며, 트리에서 노드가 제거되면 가비지 컬렉터가 이를 메모리에서 해제한다.
다음은 mutableStateOf에 대해 알아보자.
mutableStateOf
Jetpack Compose에서 제공하는 상태 객체로, 값의 변경을 추적하고, Compose 런타임이 이를 감지할 수 있도록 돕는 역할을 한다.
Compose 런타임은 mutableStateOf를 통해 상태 변경을 감지하고, 해당 상태와 연결된 UI를 리컴포지션(Recomposition)하여 새로운 상태를 반영한다.
mutableStateOf의 특징
1. 상태 추적
- mutableStateOf는 Compose 런타임에 의해 추적된다.
2. 값 변경 감지
- mutableStateOf는 값이 변경될 때 자동으로 UI를 업데이트하는 Observable State Object(관찰 가능한 상태 객체)를 제공한다.
3. 효율적인 리컴포지션
- 상태 변경 시, Compose 런타임은 상태와 연결된 컴포지션 트리의 특정 노드만 리컴포즈하여 성능을 최적화한다
mutableStateOf은 관찰 가능한 상태 객체(Observable State Object)인데, 이 객체는 mutableStateOf 외에도 mutableIntStateOf, derivedStateOf 등이 존재한다. Observable State는 다음과 같이 말할 수 있다.
관찰 가능한 상태 (Observable State)
값의 변경을 관찰할 수 있는 상태 객체로, 상태가 변경되었을 때 이를 자동으로 감지하여 특정 동작(예: UI 업데이트)을 트리거하도록 설계된 상태 관리 메커니즘이다. Jetpack Compose에서는 UI와 상태를 동기화하기 위해 사용한다.
마지막으로, 이전에 비교를 통해 봤던 코드를 단계에 따라 어떤 동작이 일어나는지 확인하면서 이 글을 마치겠다.
@Composable
fun StateExample() {
var count = remember { mutableStateOf(0) }
Column {
Button(onClick = {
count.value += 1
Log.d("SideEffectEx", "count is ${count.value}")
}) {
Text("count is ${count.value}")
}
}
}
동작 과정
- 초기 상태:
- mutableStateOf(0)으로 초기 상태 객체가 생성.
- remember를 통해 이 상태 객체가 컴포지션 트리에 저장.
- 상태 변경:
- 버튼 클릭으로 count.value가 증가하면, mutableStateOf가 Compose 런타임에 상태 변경 알림을 보낸다.
- Compose 런타임 감지:
- Compose 런타임은 상태 변경 알림을 감지하고, 컴포지션 트리에서 이 상태와 연결된 노드(Text)를 찾아 더럽혀진 상태(dirty)로 표시.
- 리컴포지션 트리거:
- dirty 노드만 리컴포지션.
- Text("Count: ${count.value}")가 리컴포지션되어 UI가 업데이트.
- Reference
Jetpack Compose의 메모리 관리 이해: 실용 가이드