본문 바로가기

Android/Tech

Composable 파라미터 주의 사항 (feat.Recomposition)

Unsplash, Ricardo Gomez.

동기

저는 Jetpack Compose 와 MVI 조합을 굉장히 좋아합니다. UI 상태는 uiState 라는 이름의 data class 를 주로 사용하는 편인데, 문득 uiState 를 통째로 넘겨 화면을 업데이트하고 있는 코드에 의문이 생겼습니다. '불필요한 Recomposition 은 Jetpack Compose 가 줄여준다고는 하지만, 확실한걸까?' 라는 의문이 말입니다.

 

이에 대해 시험한 결과와 함께, Jetpack Compose 사용 시 Composable 의 파라미터 선정에 대한 주의 사항을 기록하고자 합니다.

 


문제의 코드

@Composable
private fun BoxScope.ColorBox(state: UiState) {
    Box(
        modifier = Modifier
            .align(Alignment.BottomCenter)
            .size(100.dp)
            .background(color = state.color)
    )
}

 

상황 설명을 위해 직관적인 코드를 작성해봤습니다.

코드 자체는 간단합니다. state 는 MutableStateFlow<T> 이므로, update { } 메서드를 통해 새로운 객체로 변경되어 수집됩니다. ColorBox Composable 은 state 의 color 값만 사용합니다. color 가 모종의 이유로 변경되면 Recomposition 됩니다.

 

해당 Composable 의 Recomposition 은 또 다른 변경에 의해 함께 Recomposition 될 수 있는데요.

 

@Composable
private fun Screen(
    uiState: StateFlow<UiState>,
    onClick: () -> Unit,
    onDrag: (Pair<Int, Int>) -> Unit
) {
    val state = uiState.collectAsStateWithLifecycle().value

    Box(modifier = Modifier.fillMaxSize()) {
        NewBox {
            onClick()
        }

        MovableBox(state.x, state.y, onDrag)

        ColorBox(state)
    }
}

 

Screen-Level-Composable 의 명세입니다. MovableBox Composable 은 드래그를 통해 이동될 수 있는 Composable 이며, ViewModel 에 해당 값을 보존하기 위해 onDrag() 람다로 변경된 값을 넘겨주도록 구성되어 있습니다.

 

Jetpack Compose 는 불필요한 Recomposition 의 발생을 최소화하기 위해, input 이 변경된 경우에만 Recomposition 이 발생하도록 최적화되어 있습니다. 그래서, UiState 를 통째로 넘겨줘도 괜찮을 줄 알았습니다.

 

문득, '이대로 괜찮은가?' 라는 의문이 생겼고, 이에 대한 파악을 위해 LayoutInspector 를 사용했습니다.

 

 

드래그를 통해 위치 이동이 가능한 MovableBox Composable 을 움직여서 Recompositon 이 발생하도록 하였습니다. 50회의 이동이 발생하였고, 또 다른 경로를 통해 UiState color 값을 2회 변경해줬습니다. 주목할만한 부분은 ColorBox 인데, 유효한 Recomposition 이 51회 발생하였습니다. 이동된 MovableBox 는 50회 발생하였고요.  

 

@Composable
private fun MovableBox(
    x: Int,
    y: Int,
    onDrag: (Pair<Int, Int>) -> Unit
) {
    Box(
        modifier = Modifier
            .size(100.dp)
            .pointerInput(Unit) {
                detectDragGestures { _, dragAmount ->
                    onDrag(Pair(dragAmount.x.roundToInt(), dragAmount.y.roundToInt()))
                }
            }
            .absoluteOffset(x.dp, y.dp)
            .background(color = Color.Cyan)
    )
}

 

MovableBox Composable 의 명세는 위와 같습니다. 즉, 전체 UiState 를 넘겨주지 않고 필요한 x, y 값만 넘겨주도록 작성했습니다. 이렇게 하니, 불필요한 Recomposition 이 발생하지 않음을 알 수 있습니다. 

그렇다면, ColorBox Composable 에서도 동일하게 작동할 것으로 보입니다. 다음과 같이 코드를 재작성합니다.

 

@Composable
private fun BoxScope.ColorBox(color: Color) {
    Box(
        modifier = Modifier
            .align(Alignment.BottomCenter)
            .size(100.dp)
            .background(color = color)
    )
}

 

이후, 다시 LayoutInspector 를 통해 MovableBox Composable 의 이동으로 인해 발생한Recomposition 이 ColorBox Composable 의 생략 가능한 Recomposition 인지 확인합니다.

 

 

전체 Recomposition 은 52회 발생하였고, MovableBox Composition 의 Recomposition 은 51회 발생하였습니다. ColorBox Composable 의 경우, 이전과 다르게 유효한 Recomposition 이 2회로 줄었습니다. 생략된 Recompositon 은 50회로, UiState 의 color 값이 변경된 것을 제외한 모든 Recomposition 이 생략되었습니다.

 

즉, 꼭 필요한 값만 파라미터로 넘겨주어야 불필요한 Recomposition 을 최소화할 수 있습니다.

 


중요한 건 참조하는 값이 아닌 파라미터임을 알게 되었습니다. 툴 킷의 최적화만 믿고 한 평생 UiState 를 넘겨왔는데, 그러면 안 된다는 걸 알게 됐네요. 조금 더 자세히 알아보고 작성하는 버릇을 확실히 들여야 되겠습니다.