카테고리 없음

[Android] Smart Recomposition[1] - Key[Positional memoization]

Jun.LEE 2024. 3. 11. 08:56

오늘 이야기하고 싶은 것은 Jetpack compose의 최적화에 대한 이야기이다.

그전에 Composable 함수의 Data -> UI 과정을 대략적으로 살펴보자.

 

출처: https://developer.android.com/jetpack/compose/phases

 

 

Composition Step에서는 UI Tree를 그린다.

출처: https://developer.android.com/jetpack/compose/phases

 

 

Layout Step에서는 트리를 순회하며 자식 노드의 크기를 측정하고 위치를 배정하며

그것을 토대로 자신의 크기를 측정하는 일을 수행한다.

출처: https://developer.android.com/jetpack/compose/phases

 

Drawing Step에서는 UI Tree를 선회하며 각 node를 픽셀을 그린다.

출처: https://developer.android.com/jetpack/compose/phases

 

 

우리가 Composable을 활용하여 UI를 구성할 때 State 값의 변경을 통하여 Recomposition이 발생하도록 의도하는 부분이 있을 것이다.

하지만 그 외의 부분 UI의 변화를 의도하지 않은 곳에서 마저 Recomposition이 발생한다면 효율면에서 좋지 못할 것이다.

 

Jetpack compose에서는 각 Composable을 Composable Tree에 그리고 각 위치에 대해서 identity를 준다.

그리고 function memoization과 같이 Recomposition이 발생하였을 때 연산을 다시 실행하지 않고

기존 값을 사용하는 방식을 채택하였다.

 

위의 결과로 리스트의 값을 UI로 하는 Composable들과 이들의 parent Composable이 있다고 가정했을 때

자식 Composable들에게 Recomposition이 트리거 되어도 key값과 input 값이 보존되는 한에서는 기존 값을 그대로

사용할 수 있다는 것이다.

class MainActivity : ComponentActivity() {
    @OptIn(ExperimentalMaterial3Api::class)
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            NetWorkMemoTheme {
                val columnList = remember { mutableStateListOf<Element>() }
                Surface(
                    modifier = Modifier.fillMaxSize(),
                    color = MaterialTheme.colorScheme.background
                ) {
                    Scaffold(
                        floatingActionButton = {
                            Column {
                                FloatingActionButton(
                                    onClick = {
                                        columnList.add(
                                            Element(
                                                id = ++Element.idGenerator,
                                                text = "${Element.idGenerator}",
                                                color = makeRandomColor()
                                            )
                                        )
                                    }
                                ) { Icon(imageVector = Icons.Filled.ArrowDownward, contentDescription = "") }
                                FloatingActionButton(
                                    onClick = {
                                        columnList.add(
                                            index = 0,
                                            element = Element(
                                                id = ++Element.idGenerator,
                                                text = "${Element.idGenerator}",
                                                color = makeRandomColor()
                                            )
                                        )
                                    }
                                ) { Icon(imageVector = Icons.Filled.ArrowUpward, contentDescription = "") }
                            }
                        }
                    ) {
                        Column(
                            modifier = Modifier
                                .padding(it)
                                .fillMaxSize()
                        ) {
                            columnList.forEach { element ->
                                Box(
                                    modifier = Modifier
                                        .fillMaxWidth()
                                        .padding(16.dp)
                                        .background(color = element.color)
                                ) {
                                    Text(
                                        modifier = Modifier
                                            .align(Alignment.CenterStart)
                                            .padding(16.dp),
                                        text = "Text ${element.text}",
                                        fontSize = 24.sp,
                                        fontWeight = FontWeight.Bold
                                    )
                                }
                            }
                        }
                    }

                }
            }
        }
    }
}

fun makeRandomColor(): Color {
    val a = 0xFF000000
    val b = Random.nextLong(0, 0xffffff)
    return Color(a + b)
}

data class Element(
    val id: Int,
    val text: String,
    val color: Color,
) {
    companion object {
        var idGenerator = 0
    }
}

 

아이디, 텍스트, 색을 가지고 있는 Element data class를 만들어주고 

Element 리스트의 삽입이 있을 때 Recomposition이 트리거 될 수 있도록 columnList를 

MutableStateList로 만들어 주었다.

 

Scaffold에 2개의 버튼을 만들었고 하나는 columnList의 tail에 Element를 삽입,

나머지 하나는 head에 Element를 삽입하도록 해주었다.

 

그리고 테스트를 해보자.

 

 

columnList의 tail에 element를 추가할 때 기존에 있던 Composable들의

recomposition skip count가 증가하는 것을 확인할 수 있다.

 

그렇다면 head에 추가하면 어떻게 될까?

 

기존에 있던 Composable들의 Recomposition count가 증가하는 것을 확인할 수 있다.

 

위의 예시처럼 iteration을 사용하여 Composable들을 정의해 줄 경우에는 그 순서에 따라서 

identity가 결정되기 때문에 Composable의 key와 input value의 매치가 되지 않아서 기존 값을 그대로 사용할 수 없게 된다.

따라서, 모두 Recomposition이 발생하게 되고 위가 그 결과라고 할 수 있겠다.

 

하지만 이 identity를 명시적으로 정해줄 수 있는 방법이 있으니 그것이 key Composable이다.

key를 사용하여 테스트해보자.

) {
    Column(
        modifier = Modifier
            .padding(it)
            .fillMaxSize()
    ) {
        columnList.forEach { element ->
            key(element.id) { // 변경
                Box(
                    modifier = Modifier
                        .fillMaxWidth()
                        .padding(16.dp)
                        .background(color = element.color)
                ) {
                    Text(
                        modifier = Modifier
                            .align(Alignment.CenterStart)
                            .padding(16.dp),
                        text = "Text ${element.text}",
                        fontSize = 24.sp,
                        fontWeight = FontWeight.Bold
                    )
                }
            }
        }
    }
}

 

 

 

head, tail에 어디에 추가하는지 상관없이 Recompostion을 skip 하고 있는 것을 확인할 수 있다.

 

위와 같이 Composable Tree Position에 의해서 결정된 identity와 input value의 값으로 Composable 구성에 대한

연산을 생략하고 재사용하는 방식을 Positional Memoization이라고 한다.