[Android/Kotlin] Compose Side-effect
Compose Side-effect
composable 함수의 범위 밖에서 발생하는 앱 상태에 관한 변경사항입니다. 즉, 컴포저블의 상태가 변경되면서 화면에 직접적인 영향을 주거나 외부 작업을 수행하는 것을 의미합니다.
예를 들면 사용자에게 메시지를 표시하는 스낵바를 띄우거나, 특정 조건이 충족되면 다른 화면으로 이동하는 작업이 이에 해당합니다.
컴포저블의 수명 주기를 인식하고 관리하는 환경에서 이러한 Side-effect를 처리하도록 여러 API를 제공합니다. 이를 통해 일회성 이벤트가 특정 상태 변화에 따라 정확히 한 번만 발생하거나 컴포저블이 새로 구성될 때마다 이벤트가 발생하지 않도록 안정적으로 관리할 수 있습니다. 또한 UI 렌더링 로직과 분리시키고 개별적인 coroutine scope에 실행시킬 수 있도록 한다.
주요 Side-effect API
1. LaunchedEffect
특정 키(key)가 변경될 때마다 실행되는 Coroutine scope에서 Side-effect 를 처리하는 데 사용합니다.
LaunchedEffect는 코루틴을 시작할 때 사용됩니다. 주어진 키가 변경되면 기존의 코루틴이 취소되고, 새 키로 코루틴이 다시 시작됩니다.
자세히 얘기하면, 화면이 처음 나타날 때 데이터를 불러오는 작업이나, 특정 상태가 바뀔 때 알림을 띄우는 작업에 유용합니다.
예를 들자면 UI 스레드를 블락하지 않고 긴 시간의 작업(네트워크 호출, 애니메이션)을 처리할 때 사용됩니다.
* 포지션을 종료한 후, 자동으로 취소되도록 범위가 지정된 코루틴을 실행하려면 rememberCoroutineScope를 사용합니다.
LaunchedEffect(key1) {
// 특정 키 값이 변경될 때마다 실행
showSnackbar("New message received")
}
// 한번만 작업을 수행하기
LaunchedEffect(true) {
oneTime()
}
// rememberCoroutineScope()
@Composable
fun MoviesScreen(snackbarHostState: SnackbarHostState) {
//Creates a CoroutineScope bound to the MoviesScreen's lifecycle
val scope = rememberCoroutineScope()
Scaffold(
snackbarHost = {
SnackbarHost(hostState = snackbarHostState)
}
) { contentPadding ->
Column(Modifier.padding(contentPadding)) {
Button(
onClick = {
// Create a new coroutine in the event handler to show a snackbar
scope.launch {
snackbarHostState.showSnackbar("Something happened!")
}
}
) {
Text("Press me")
}
}
}
}
2. SideEffect
재구성이 일어나더라도 항상 실행해야 하는 Side-effect 가 있을 때 사용하는 컴포저블 함수입니다.
컴포저블의 상태나 props에 의존하지 않는 연산을 수행할 때 유용한데, 로깅이나 데이터 수집 같은 작업에 적합합니다.
SideEffect {
Log.d("MyTag", "This is logged on every recomposition")
}
3. DisposableEffect
설정 및 해제 작업이 필요한 Side-effect 에 사용합니다.
부모 컴포저블이 처음 렌더링 될 때 Side-effect를 실행하고, 컴포저블이 UI계층에서 제거될 시에 effect를 폐기하게 하는 컴포저블 함수입니다.
이 함수는 이벤트 리스너를 붙이거나 애니메이션을 실행시키는 것과 같이, 컴포저블 함수가 UI 계층에서 제거될 때 사용(실행)되지 않아야 하는 로직들을 실행시켜야할 때 유용합니다.
DisposableEffect(Unit) {
val listener = object : SomeListener { /*...*/ }
onDispose {
listener.unregister()
}
}
4. rememberUpdatedState
값이 변경되면 다시 시작되지 않아야 하는 효과의 값을 참조합니다.
LaunchedEffect 주요 매개변수 중 하나가 변경되면 다시 시작합니다. 그러나 어떤 상황에서는 변경되더라도 효과를 다시 시작하지 않으려는 값을 효과에서 캡처하고 싶을 수 있습니다. 이를 위해 rememberUpdatedState가 캡처하고 업데이트할 수 있는 이 값에 대한 참조를 만드는 데 사용해야 합니다. 이 접근 방식은 재생성하고 다시 시작하는 데 비용이 많이 들거나 금지될 수 있는 장기 작업이 포함된 효과에 유용합니다.
예를 들어, 앱에 LandingScreen일정 시간 후에 사라지는 가 있다고 가정해 보겠습니다. 재구성하더라도 LandingScreen일정 시간 동안 기다렸다가 시간이 지났음을 알리는 효과는 다시 시작되지 않아야 합니다.
val currentState by rememberUpdatedState(newState)
LaunchedEffect(Unit) {
// 최신 상태 값을 참조하는 일회성 Side-effect
currentState.doSomething()
}
5. derivedStateOf
하나 또는 여러 개의 상태 객체를 다른 상태로 변환합니다.
Compose에서 재구성은 관찰된 상태 객체 또는 구성 가능한 입력이 변경될 때마다 발생합니다. 상태 객체 또는 입력이 UI가 실제로 업데이트해야 하는 것보다 더 자주 변경될 수 있으며, 이로 인해 불필요한 재구성이 발생합니다.
derivedStateOf 컴포저블에 대한 입력이 재구성하는 데 필요한 것보다 더 자주 변경될 때 이 함수를 사용해야 합니다 .
이는 스크롤 위치와 같이 무언가가 자주 변경되지만 컴포저블은 특정 임계값을 넘을 때만 반응하면 되는 경우에 종종 발생합니다. derivedStateOf필요한 만큼만 업데이트되는 것을 관찰할 수 있는 새 Compose 상태 객체를 만듭니다. 이런 방식으로 Kotlin Flows distinctUntilChanged() 연산자와 비슷하게 작동합니다.
* 비용이 많이 들기 때문에 결과가 변경되지 않았을 때 불필요한 재구성을 방지하는 데에만 사용해야 합니다.
val showButton by remember {
derivedStateOf {
listState.firstVisibleItemIndex > 0
}
}
6. snapshotFlow
Compose의 State를 Flow로 변환
이 함수는 Compose Flow나 value이 변경 될 때마다 Flow를 통해 새로운 데이터를 방출하여, UI와의 데이터 동기화가 쉽도록 돕습니다. UI에서 데이터를 구독(subscribe)하고 반응형으로 처리하는데 유용합니다.
* snapshotFlow는 Compose 환경에서 Snapshot 시스템과 연동되기 때문에, Compose 컴포저블 외부에서는 사용할 수 없습니다.값이 변하지 않으면 Flow는 데이터를 방출하지 않으므로, 상태가 빈번하게 변경되는 상황에서 사용해야 효율적입니다.
val textFlow = snapshotFlow { textState.value }
이러한 Compose Side-effect API를 활용하면 특정 조건에 따라 외부 동작을 제어하면서도 컴포저블의 수명 주기를 관리할 수 있습니다. 이는 상태에 따라 적절히 이벤트를 발생시키고, 화면 전환이나 메시지 표시 등 앱의 동작을 예측 가능하게 유지하는 데 매우 중요합니다.