이 글은 "[Jetpack Compose] 공식문서 읽기 Side-effects in Compose Part 1" 에 이어진 글입니다. 오늘도 공식문서를 읽어보며 Jetpack Compose 의 SideEffect 에 대해 공부해 보겠습니다.
https://seonghoonc.tistory.com/56
[Jetpack Compose] 공식문서 읽기 Side-effects in Compose Part 1
들어가며 LaunchedEffect, DisposableEffect 를 많이 사용하는데 제대로 알고 사용하자는 의미로 공식문서를 읽어보며 SideEffect 에 대해 공부해보겠습니다. 제 생각과 의역이 들어가기 때문에 정확한 정
seonghoonc.tistory.com
State and effect use cases
5. SideEffect: publish Compose state to non-Compose code
compose state 를 compose 가 관리되지 않는 객체에 공유하고 싶다면 SideEffect Composable 을 사용할 수 있습니다.
recomposition 이 성공된 후에 실행됩니다. 반면에, composable 내부에 effect 를 직접 작성하여 recomposition 이 성공하기 전에 Effect 를 실행해서는 안됩니다.
예를 들어 analytics 가 이후 발생하는 모든 analytics 이벤트에 커스텀 메타데이터를 전달하여 사용자들을 세분화 할 수 있습니다. 즉, SideEffect 를 사용해 현재 사용자의 타입을 analytics 라이브러리에 전달할 수 있는 것이죠.
@Composable
fun rememberFirebaseAnalytics(user: User): FirebaseAnalytics {
val analytics: FirebaseAnalytics = remember {
FirebaseAnalytics()
}
// 컴포지션이 성공할 때마다 Analytics 를 새 userType으로 업데이트
SideEffect {
analytics.setUserProperty("userType", user.userType)
}
return analytics
}
6. produceState: convert non-Compose state into Compose state
produceState 는 Composition 에 Coroutine Scope 를 launch 해 반환된 State 값을 전달할 수 있습니다. 이를 사용해 Compose 외부의 상태를 Compose 상태로 변환할 수 있으며 Flow, LiveData, RxJava 와 같이 구독 기반의 상태를 Composition 으로 가져올 때 유용하죠.
producer 는 produceState 가 Composition 되었을 때만 실행되면, Composition 이 끝나면 취소됩니다. 또한 같은 값이 setValue 되면 recomposition 을 트리거하지 않습니다.
produceState 는 코루틴을 생성하지만, non-suspending Data Source 도 관찰할 수 있습니다. 만약 구독을 해제하고 싶다면 awaitDispose 함수를 사용하면 됩니다.
다음 예시는 produceState 를 사용해 네트워크에서 이미지를 로드합니다. loadNetworkImage Composable 함수는 다른 Composable 함수가 사용할 수 있는 State 를 반환하죠.
@Composable
fun loadNetworkImage(
url: String,
imageRepository: ImageRepository = ImageRepository()
): State<Result<Image>> {
// 초기값 Result.Loading 으로 생성된다.
// url 혹은 imageRepository 가 변경되면 실행중인 producer 가 취소되고 다시 실행된다.
return produceState<Result<Image>>(initialValue = Result.Loading, url, imageRepository) {
val image = imageRepository.load(url)
// State 를 Error 나 Success 로 업데이트한다.
// 다른 composable 이 state 를 보고 있다면 recomposition 이 트리거 될 것이다.
value = if (image == null) {
Result.Error
} else {
Result.Success(image)
}
}
}
여기서 주의할 점은 returnType 이 있는 Composable 은 일반 함수와 같이 소문자로 시작해야한다는 것입니다. 다른 Composable 이 대문자로 시작하는 것과 차이가 있습니다.
7. derivedStateOf: convert one or multiple state objects into another state
Compose 에서 관찰중인 state 나 composable input 이 변경되면 매번 Recomposition 이 발생합니다. 이는 UI 가 실제 업데이트 해야되는 것보다 자주 일어날 수도 있습니다.
recompose 가 필요한 시점보다 더 자주 상태가 변경된다면 derivedStateOf 를 사용해야합니다. 예를 들어 스크롤 위치처럼 자주 변하는 값이 있을 때, Composable 이 특정 위치 이상을 넘었을 때만 Recomposition 하면 되는 경우가 있습니다. 오직 update 가 필요할 때만 관찰되는 state 를 만들 수 있습니다. kotlin Flow 의 distinctUntilChanged() 와 비슷하죠.
그러나, 공식문서에 따르면 derivedStateOf 는 더 expensive 하다고 표현됩니다. 일반적으로 state 를 관찰하는 것보다 비용이 많이 든다는 뜻이겠죠. 꼭 필요할때만 사용하는게 좋다고합니다.
- 올바른 사용 방법
@Composable
// messages 가 변경되면 MessageList 가 재구성되겠죠.
// derivedStateOf 는 이 재구성이랑은 상관이 없습니다(영향을 주지 않습니다).
fun MessageList(messages: List<Message>) {
Box {
val listState = rememberLazyListState()
LazyColumn(state = listState) {
// ...
}
// 첫번째로 보이는 item 의 index 가 0 이상이면 버튼을 표시합니다.
// 하지만 1, 2, 3, 4, ... 그 이후로 계속 재구성되겠죠.
// 이를 최소화하기 위해 derivedStateOf 를 사용했습니다.
val showButton by remember {
derivedStateOf {
listState.firstVisibleItemIndex > 0
}
}
AnimatedVisibility(visible = showButton) {
ScrollToTopButton()
}
}
}
- 잘못된 사용 방법
// 이렇게 사용해서는 안됩니다.
var firstName by remember { mutableStateOf("") }
var lastName by remember { mutableStateOf("") }
val fullNameBad by remember { derivedStateOf { "$firstName $lastName" } } // This is bad!!!
val fullNameCorrect = "$firstName $lastName" // This is correct
이 코드는 firstName 혹은 lastName 이 변경될 때마다 무조건 Recomposition 이 일어나야합니다. 이런 경우엔 derivedStateOf 가 오히려 성능 저하를 일으킬 수 있습니다. 그러므로 필요할 때만 사용해야합니다.
8. snapshotFlow : convert Compose's State into Flows
snapshotFlow 는 compose State 를 cold Flow 로 바꿔줍니다. snapshotFlow는 수집(collect)할 때 블록을 실행하며, 그 안에서 읽은 State 객체의 결과를 발행(emit)합니다. 만약 snapshotFlow 내에서 State 객체 중 하나가 변경되면, 새 값이 이전에 발행된 값과 다를 경우 Flow 는 collector 에게 새로운 값을 emit 합니다. 이 동작은 Flow.distinctUntilChanged와 유사합니다.
다음 예제는 사용자가 리스트에서 첫 번째 항목을 넘어서 스크롤할 때 이를 분석(analytics)에 기록하는 부작용(side effect)을 보여줍니다.
val listState = rememberLazyListState()
LazyColumn(state = listState) {
// ...
}
LaunchedEffect(listState) {
snapshotFlow { listState.firstVisibleItemIndex }
.map { index -> index > 0 }
.distinctUntilChanged()
.filter { it == true }
.collect {
MyAnalyticsService.sendScrolledPastFirstItemEvent()
}
}
Restarting effects
Compose 의 LaunchedEffect, produceState, or DisposableEffect 같은 effect 들은 여러 key 인자들을 받는데, 변경될 때마다 실행중인 effect 가 cancel 되고 새로운 effect 가 시작됩니다.
EffectName(restartIfThisKeyChanges, orThisKey, orThisKey, ...) { block }
이 특성 때문에 올바르게 사용되지 않는다면 버그나 비효율을 야기할 수 있습니다.
필요한 만큼 발생하지 않는다 -> 버그
필요 이상으로 발생한다 -> 비효율
일반적으로, Effect 코드 블록에서 사용되는 mutable 변수와 immutable 변수는 Effect Composable 의 매개변수로 추가되어야 합니다. 이 외에도, Effect 를 강제로 다시 시작하기 위해 추가 매개변수를 넣을 수 있습니다. 만약 변수의 변경이 Effect 를 다시 시작하게 해서는 안 된다면, 해당 변수를 rememberUpdatedState로 감싸야 합니다. 만약 그 변수가 키 없이 remember로 감싸져서 한 번도 변경되지 않는다면, 그 변수를 Effect 의 키로 전달할 필요가 없습니다.
아래 예시에서는 onState, onStop 이 rememberUpdatedState 때문에 변경되지 않기 때문에 DiposableEffect 의 key 로 전달할 필요가 없습니다.
@Composable
fun HomeScreen(
lifecycleOwner: LifecycleOwner = LocalLifecycleOwner.current,
onStart: () -> Unit, // Send the 'started' analytics event
onStop: () -> Unit // Send the 'stopped' analytics event
) {
// These values never change in Composition
val currentOnStart by rememberUpdatedState(onStart)
val currentOnStop by rememberUpdatedState(onStop)
DisposableEffect(lifecycleOwner) {
val observer = LifecycleEventObserver { _, event ->
/* ... */
}
lifecycleOwner.lifecycle.addObserver(observer)
onDispose {
lifecycleOwner.lifecycle.removeObserver(observer)
}
}
}
만약 lifecycleOwner를 DisposableEffect 의 매개변수로 전달하지 않고 변경된다면, HomeScreen은 재구성되지만 DisposableEffect는 폐기되거나 재시작되지 않습니다. 이로 인해 이후로 잘못된 lifecycleOwner가 사용되어 문제가 발생하게 됩니다.
Constants as keys
true 같은 effect key 를 사용하면 Lifecycle 을 따르도록 할 수 있습니다.
마무리하며
오늘은 SideEffect, produceState, derivedStateOf, snapshotFlow 에 대해 공부해보았습니다. 오늘 공부한 것들은 한 번에 이해되지 않아 여러번 읽어보았네요. 조금 더 효율적으로 작성할 수 있는지 고민해보면서 써야겠습니다.
'Jetpack Compose' 카테고리의 다른 글
[Jetpack Compose] Testing in Jetpack Compose (3) | 2025.02.09 |
---|---|
[Jetpack Compose] 공식문서 읽기 Side-effects in Compose Part 1 (0) | 2025.01.26 |
[Jetpack Compose] 컴포즈 Codelab 기초 따라하기 - 2 (0) | 2023.12.24 |
[Jetpack Compose] 컴포즈 Codelab 기초 따라하기 - 1 (2) | 2023.12.19 |
Compose 를 왜 사용해야하는가? (0) | 2023.12.09 |