동기
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 테스트를 작성하게 된다면 또 어떨지 모르겠네요.
'Android > Tech' 카테고리의 다른 글
[Jetpack Compose] composed {} 와 Modifier.Node 로 Modifier Chain 최적화하기 (0) | 2025.01.15 |
---|---|
LazyList 과 RecyclerView 의 메커니즘 알아보기 (0) | 2025.01.13 |
[Jetpack Compose] 불필요한 Recomposition 을 줄여 앱 퍼포먼스 개선하기 (1) | 2024.02.25 |
[Jetpack Compose] HiltViewModel() 을 통해 주입된 ViewModel 의 생애 알아보기 (2) | 2024.02.11 |
OkHttp3 Interceptor 를 통해 표준화된 응답의 에러 처리하기 (0) | 2024.02.01 |