본문 바로가기

Android/Tech

[Jetpack Compose] CompositionLocal 로 이벤트 처리하기

Unsplash, Tuệ Nguyễn.

동기

Jetpack Compose 를 활용하여 UI 를 개발하다 보면, Composable 의 테스트 용이성과 재사용성을 높이기 위해 StateHoisting 을 적용하곤 합니다.

 

제 경우, StateHoisting 을 엄격하게 적용하려다 보니 수많은 람다 파라미터로 인해 Composable 메서드를 호출하는 부분이 과도하게 길어지고, 드릴링이 발생하는 문제가 있었습니다.

 

저는 이를 CompositionLocal 을 통해 해결하였고, 오늘은 이에 대해 간략하게 작성해보려 합니다.


Stability

불필요한 Recomposition 의 발생을 최대한 줄이기 위해 이벤트 처리와 관련된 로직을 Stable 하게 만들어야 합니다. 이에 관한 내용은 아래에서 확인하실 수 있습니다.

 

 

[Jetpack Compose] 불필요한 Recomposition 을 줄여 앱 퍼포먼스 개선하기

동기 Jetpack Compose 를 활용하여 개발 중인 앱 에는 드래그 앤 드랍과 같은 유저 인터랙션이 존재합니다. 다만 문제가 좀 있었습니다. 드래그 앤 드랍 시 화면이 버벅거린다는 점이었고, 이는 매우

blothhundr.tistory.com

 

val onEvent = remember { { event: UiEvent -> uiEvent(event) } }

 

저는 remember 를 이용하여 이와 같이 작성하였습니다. remember 스코프 내부를 이벤트 처리 로직으로 구성합니다. 이를 통해 uiEvent 라는 이름의 람다 변수를 Stable 하게 만들 수 있습니다.


CompositionLocal

 

[Jetpack Compose] CompositionLocal 은 어떻게 동작하는가?

동기 Jetpack Compose 를 꽤 오랜 시간 사용해 왔지만, 여전히 모르는 부분이 많습니다. 그중 가장 가려웠던 부분은 CompositionLocal 인데, 언제 어떻게 사용해야 하는지에 대해서는 알고 있으나, 어떠한

blothhundr.tistory.com

 

이벤트 처리 메커니즘의 핵심인 CompositionLocal 에 대한 설명은 위 포스트에 자세히 작성해두었습니다. 

 

CompositionLocal 에 적용된 값은 UI Tree 를 타고 아래로 흐릅니다. 그 값은 Group 이라는 스코프에 일괄적으로 적용이 되는데, Group 에 값을 적용하는 방법과 그 값에 접근하기 위한 방법에 대해 간단히 소개합니다.

CompositionLocal 을 통해 데이터 제공하기

먼저, 적용하고자 하는 CompositionLocal 의 기본값을 설정해야 합니다. 제공하고자 하는 타입에 따라 달라지게 됩니다. 이 경우에는 이벤트를 처리하기로 하였으므로, 이벤트 타입의 객체를 수신하는 람다를 작성하면 됩니다.

 

private val LocalUiEvent = compositionLocalOf { { _: UiEvent -> } }

 

이와 같이 작성하면, LocalUiEvent 라는 이름의 CompositionLocal 에 지정될 데이터에 대한 기본값이 설정됩니다. 즉, compositionLocalOf 의 스코프에 어떠한 타입의 데이터가 적용될지를 결정합니다.

 

이후, 이를 실제 UI Tree 에 적용하기 위해 CompositionLocalProvider() 메서드를 사용합니다. Slot API 를 통해 생성되는 스코프 내에서 Composable 메서드를 호출하면 해당 Composable 이 속한 Group 에 제공되는 값이 적용됩니다. 이후에는 각 Composable 메서드 내에서 CompositionLocal 에 접근하여 값을 획득하여 사용할 수 있는 구조입니다.

 

CompositionLocalProvider(LocalUiEvent provides onEvent) {
    Box(
        modifier = Modifier
            .fillMaxSize()
            .background(color = MaterialTheme.colors.background)
    ) {
        ...
    }
}

 

적용하고자 하는 CompositionLocal CompositionLocal 을 키로 받아 값을 저장하는 Map<K, T> 에 저장될 수 있도록 하기 위해 CompositionLocalProvider 를 사용합니다. 즉, 해당 스코프 내에서 LocalUiEvent 를 참조하면 onEvent 값을 획득할 수 있습니다.

 

이후 생성되는 스코프 내에 Composable 메서드를 호출하면 사용을 위한 준비는 끝이 납니다. 

CompositionLocal 이 제공하는 값 획득하기

Jetpack Compose 를 통해 UI 를 개발해보셨다면, 여기부턴 매우 간단합니다. 단순히 CompositionLocal 이 제공하는 값을 호출하여 사용하면 됩니다.

 

@Composable
private fun TopSection(displayCount: Int) {
    val localUiEvent = LocalUiEvent.current

    TopBar(
        onClickProfileIcon = { /*TODO*/ },
        middleContents = {
            Row(verticalAlignment = CenterVertically) {
                Icon(
                    modifier = Modifier.clickableWithoutRipple { localUiEvent(OnClickReset) },
                    painter = painterResource(id = R.drawable.ic_reset),
                    tint = AddedIngredientDescColor,
                    contentDescription = null
                )

                ...
            }
        }
    )
}

 

Modifier.clickable 에 직접 이와 같이 작성하여 사용하면 되기 때문에, 특수한 이유가 있지 않은 이상 람다 파라미터를 Composable 메서드에 작성할 필요가 없어집니다. 즉, 수많은 드릴링이 발생하는 상황에서, 이벤트 처리를 위한 람다 파라미터 작성을 회피할 수 있습니다.

 

추가로, 불필요한 Recomposition 이 발생하지는 않는지 확인하기 위해, 이벤트 처리 람다 변수의 해시 코드를 여러 곳에서 출력해보았고, 다음과 같은 결과를 얻을 수 있었습니다.

 

remembered onEvent hashCode : 266084374
Screen Level LocalComposition value hashCode : 266084374
Composable Level LocalComposition value hashCode : 266084374

 

 

이와 같이 작성하여도 Preview 등의 기능을 이용하는 데에는 아무런 문제가 없으므로, 조금은 더 깔끔하게 Jetpack Compose 를 활용한 UI 코드 작성이 가능합니다.


Jetpack Compose 를 통해 UI 를 작성하던 도중, 복잡한 UI 구성으로 인해 깊어지는 프로퍼티 드릴링을 회피하기 위해 사용하게 되었던 방식입니다. 현재로써는 별다른 문제를 야기하지 않는 것 같습니다만, UI 테스트를 작성하게 된다면 또 어떨지 모르겠네요.