Compose에서 Recomposition이 발생하는 이유는 다음과 같다.
- 관찰 중인 State가 변경되는 경우
- Composable 함수의 매개 변수의 값이 변경되는 경우
1. State의 변경
Compose를 사용하다보면 데이터를 저장하기 위해 State를 사용한다.
State의 사용 방법에는 두 가지가 있는데 다음과 같다.
val test: MutableState<Int> = remember{ mutableStateOf(0) }
val test2: Int by remember { mutableIntStateOf(0) }
assign의 경우 MutableState 객체를 반환하고, by의 경우 State 객체의 value를 반환한다.
@Composable
fun ParentCompose(modifier: Modifier = Modifier) {
var firstNum by remember { mutableIntStateOf(0) }
var secondNum by remember { mutableIntStateOf(0) }
Column(
modifier = modifier
.padding(20.dp)
.fillMaxSize(),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.SpaceEvenly,
) {
First(
num = firstNum,
onClick = { firstNum++ }
)
Second(
num = secondNum,
onClick = { secondNum++ }
)
}
}
@Composable
fun First(
num: Int,
onClick: () -> Unit
) {
Text(text = "First-$num")
Button(onClick = { onClick() }) {
Text(text = "First Text Change")
}
}
@Composable
fun Second(
num: Int,
onClick: () -> Unit
) {
Text(text = "Second-$num")
Button(onClick = { onClick() }) {
Text(text = "Second Text Change")
}
}
다음과 같은 코드에서 Second Button을 Click 시 어떤 Composable 함수가 Recomposition 될까?
ParentCompose와 Second 함수가 Recomposition 된다.
by를 위와 같이 직접적으로 사용하게 되면 불필요하게 ParentCompose까지 Recomposition 되기 때문에 다음과 같이 변경할 수 있다.
@Composable
fun ParentCompose(modifier: Modifier = Modifier) {
val firstNum = remember { mutableIntStateOf(0) }
val secondNum = remember { mutableIntStateOf(0) }
Column(
...
) {
First(
num = firstNum,
onClick = { firstNum.value++ }
)
Second(
num = secondNum,
onClick = { secondNum.value++ }
)
}
}
@Composable
fun First(
num: State<Int>,
onClick: () -> Unit
) {
Text(text = "First-${num.value}")
Button(onClick = { onClick() }) {
Text(text = "First Text Change")
}
}
@Composable
fun Second(
num: State<Int>,
onClick: () -> Unit
) {
Text(text = "Second-${num.value}")
Button(onClick = { onClick() }) {
Text(text = "Second Text Change")
}
}
assign을 사용하여 State 객체를 넘겨주고 하위 Composable 함수에서 State 객체의 Value 값을 참조하는 방법이다.
위와 같이 사용하면 State객체는 바뀌지 않고 내부 Value 값만 변하기 때문에 ParentCompose 함수는 Recomposition이 일어나지 않는다.
단점은 매번 State.value를 사용해야되기 때문에 코드양이 많아진다면 코드를 작성하는데 있어 많은 불편함이 따라오게 된다.
따라서 by delegate 구문을 사용하되 lambda를 사용하여 이를 해결할 수 있다.
@Composable
fun ParentCompose(modifier: Modifier = Modifier) {
var firstNum by remember { mutableIntStateOf(0) }
var secondNum by remember { mutableIntStateOf(0) }
Column(
...
) {
First(
num = { firstNum },
onClick = { firstNum++ }
)
Second(
num = { secondNum },
onClick = { secondNum++ }
)
}
}
@Composable
fun First(
num: () -> Int,
onClick: () -> Unit
) {
Text(text = "First-${num()}")
Button(onClick = { onClick() }) {
Text(text = "First Text Change")
}
}
@Composable
fun Second(
num: () -> Int,
onClick: () -> Unit
) {
Text(text = "Second-${num())")
Button(onClick = { onClick() }) {
Text(text = "Second Text Change")
}
}
2. Unstable
Unstable 타입을 갖는 경우는 다음과 같다.
- 인터페이스 또는 추상 클래스를 상속받은 객체
ex) List, Map과 같이 Collection Interface를 상속받은 객체 - (data) class의 public 프로퍼티 중 하나 이상이 가변적(var)이거나 Unstable 한 경우
data class UnStableData(
var number: Int = 0,
)
@Composable
fun ParentCompose(
modifier: Modifier = Modifier,
unStableData: UnStableData
) {
var firstNum by remember { mutableIntStateOf(0) }
Column(
...
) {
First(
num = firstNum,
onClick = { firstNum++ }
)
Second(
obj = unStableData
)
}
}
@Composable
fun First(
num: Int,
onClick: () -> Unit
) {
Text(text = "First - $num")
Button(onClick = { onClick() }) {
Text(text = "First Text Change")
}
}
@Composable
fun Second(
obj: UnStableData,
) {
Text(text = "Second - ${obj.number}")
}
위와 같이 var로 선언된 UnStableData 객체를 Second 함수에서 사용하고 있고, First 함수의 버튼을 클릭하면 다음과 같이 Second 함수도 Recomposition이 일어난다.
이는 List 또한 마찬지로
@Composable
fun ParentCompose(
modifier: Modifier = Modifier,
list: List<Int>
) {
var firstNum by remember { mutableIntStateOf(0) }
Column(
...
) {
First(
num = firstNum,
onClick = { firstNum++ }
)
Second(
obj = list
)
}
}
var 과 List 모두 Unstable 타입이기 때문이다.
실제로 Unstable인지 확인하기 위해 Compose Compiler Metrics Report를 통해 확인해보면
- var가 포함된 객체
- List
- 객체 내에 Unstable 타입이 하나라도 있는 경우에도 Unstable한 것을 확인 할 수 있다.
data class UnStableData(
val number: Int = 0,
val list: List<String> = listOf("first", "second")
)
따라서 var을 val로 바꿔주거나 Unstable 객체가 포함된 경우 @Stable 또는 @Immutable 어노테이션을 사용하여 해당 객체 타입을 바꿔줄 수 있다.
@Stable
data class UnStableData(
val number: Int = 0,
val list: List<String> = listOf("first", "second")
)
@Immutable
data class UnStableData(
val number: Int = 0,
val list: List<String> = listOf("first", "second")
)
@Stable 또는 @Immutable 어노테이션으로 타입을 바꿔주면
당연히 해당 객체는 Stable 타입으로 바뀌게되고
불필요한 Recomposition도 방지할 수 있다.
State 객체 또한 @Stable 어노테이션이 붙은 Stable 타입 객체이다.
Unstable 타입은 부모 Composable이 Recomposition이 일어나면 값이 변하지 않아도 Recomposition 대상이 되기 때문에 각별히 주의해야 한다.
반대로 위의 코드에서 firstNum을 Lambda로 넘겨 ParentCompose 함수(부모 Composable 함수)에 Recomposition이 일어나지 않는다면 Unstable 객체를 넘기더라도 First 함수만 Recomposition이 일어나게 된다.
@Composable
fun ParentCompose(
...
) {
...
First(
num = { firstNum },
...
)
...
}
'안드로이드 > Compose' 카테고리의 다른 글
[Compose] Composable 함수의 데이터 관리 (0) | 2025.06.28 |
---|---|
Side Effect (0) | 2025.06.26 |
Recomposition (0) | 2025.01.21 |
Compose 동작 원리와 ComponentActivity를 상속하는 이유 (0) | 2025.01.21 |