본문 바로가기
안드로이드/Compose

[Compose] Side Effect와 Effect API(LaunchedEffect, rememberCoroutineScope 등)

by jinwo_o 2024. 11. 23.

Side Effect (부수 효과)

  • Composable 함수를 벗어난 곳에서 앱의 state(상태) 변경이 일어나는 것
  • Composable 은 Side Effect 가 없는 것이 좋으나, 앱 상태를 변경해야 하는 경우 Side Effect 를 예측 가능한 방식으로 실행되도록 Effect API 를 사용해야 한다.
    • 한 번만 일어나는 UI 이벤트로 변경 사항이 state 로 관리될 필요가 없는 경우 (SnackBar, ToastMessage 등)
    • 다른 Screen 으로 이동하는 Navigation (사용자 인터랙션(예시: 버튼 클릭)에 의해 발생하는 경우 필요 없음)
    • system services 들과 상호작용 하는 것
    • Coroutine 을 이용한 네트워킹이나 디스크 IO

 

1. LaunchedEffect

@Composable
@NonRestartableComposable
fun LaunchedEffect(key1: Any?, block: suspend CoroutineScope.() -> Unit): Unit
  • Composable 함수 안에서 Coroutine 의 suspend 함수를 실행할 수 있도록 해준다.
  • 아래 3가지 경우에 따라 LaunchedEffect 의 Coroutine 이 launch 된다.
    • Composable composition 시작
    • state 값의 변화에 따른 Composable 의 recomposition 시작
    • LaunchedEffect 의 인자로 들어오는 key 값 변화
  • key 값은 Any 타입이며, vararg 타입으로 오버로딩된 함수가 존재하기 때문에 개수에 상관없이 여러 개의 키 값을 사용할 수 있다.
  • composition 이 종료되면 Coroutine 은 Cancel 이 되고, recompose가 되거나 key 값이 변화하면 다시 launch 된다.
  • 한 번만 실행되게 하려면 key 값에 변화를 주지 않기 위해 Unit 이나 Boolean 의 ture 같이 static 한 값을 인자로 넣어준다.
    • flow 를 수집하는 곳에서는 launchedEffect 를 반복해서 실행할 필요가 없으며, 한 번만 실행해 두면 flow 의 emit 이 트리거가 되어 side effect 가 반응하게 된다.
  • 사용자 인터랙션에 사용하기에는 어려움이 있다.
    • key 값이 변하거나 Composable 이 composition 을 떠나게 되면 사용자 인터랙션과는 관계없이 Coroutine 이 Cancel 되고 초기화되기 때문에 원하지 않는 타이밍에 Coroutine 안의 값이 초기화될 수 있다.
    • key 값이 변하지 않더라도(Unit) Composition 의 초기 단계에서 한 번만 실행된다.
      • 사용자 인터랙션과 관련된 비동기 작업을 수행할 때는 rememberCoroutineScope 를 사용하는 것이 좋다.

 

remember vs rememberUpdatedState

  • remember : Composable 이 처음 호출될 때 한 번만 실행되어 초기 값을 설정하고, 이후에는 그 값을 유지한다.
  • rememberUpdatedState : 값이 변경되는 경우, recomposition 이 발생하지 않아야 하는 상황에서 최신 값을 참조하기 위해 사용한다.
@Composable
fun LaunchedScreen() {
    var textState by remember { mutableStateOf("Default") } // Def (by recomposition)

    Column {
        TextField(
            value = textState,
            onValueChange = { textState = it } // Def
        )

        RememberUpdateTestText(textState)
    }
}

@Composable
fun RememberUpdateTestText(text: String) {
    val rememberText by remember { mutableStateOf(text) } // Default
    val rememberUpdatedText by rememberUpdatedState(text) // Def

    Text("RememberText : $rememberText")
    Text("UpdatedText : $rememberUpdatedText")
}

 

2. rememberCoroutineScope

@Composable
inline fun rememberCoroutineScope(
    crossinline getContext: @DisallowComposableCalls () -> CoroutineContext = {
        EmptyCoroutineContext
    }
): CoroutineScope
  • *Composable 함수 밖에서 state 에 영향을 줄 수 있는 Coroutine 을 실행할 수 있도록 해준다.
    • Composable 함수 밖 : Composable 의 범위를 벗어나는 곳 (예시: onClick 의 람다 함수 내)
  • *Composable 의 생명 주기에 따라 Coroutine 의 launch 와 cancel 를 관리해 준다.
    • Composable 의 생명 주기에 따라 : Composable 이 재구성되거나 구성에서 떠날 때, 해당 CoroutineScope 내에서 실행된 Coroutine 은 자동으로 취소되지만, 이 스코프를 통해 실행된 Coroutine 은 독립적으로 관리된다.

 

3. DisposableEffect

@Composable
@NonRestartableComposable
fun DisposableEffect(key1: Any?, effect: DisposableEffectScope.() -> DisposableEffectResult): Unit
  • LaunchedEffect 와 유사한 역할을 하지만, 재구성이나 종료 시, 즉 생명주기가 끝나는 시점에 onDispose 함수가 호출된다.
  • onDispose 블록이 없는 경우 오류가 발생하며, Composable 이 종료될 때 리소스의 해제와 같은 작업에 사용된다.

 

4. SideEffect

@Composable
@NonRestartableComposable
@ExplicitGroupsComposable
fun SideEffect(effect: () -> Unit): Unit
  • 최초에 구성, 재구성 등 Composable 이 성공적으로 완료되면 해당 블록이 호출된다.

 

5. produceState

@Composable
fun <T : Any?> produceState(initialValue: T, producer: suspend ProduceStateScope<T>.() -> Unit): State<T>
  • 비 Compose 상태를 Composable 상태로 변환할 때 사용하는 코루틴이다.
  • Flow, LiveData, RxJava 등 Composable 외부에서 사용되는 것들을 produceState 블록 내부에서 사용하고, 이 값을 기반으로 연산한 후, Composable 에서 사용할 수 있는 형태인 State<T>로 반환하여 필요한 데이터를 Composable 에서 사용할 수 있도록 해준다.
  • 실행된 produceState 가 종료될 때 호출되는 awaitDispose 블록이 있다.

 

6. derivedStateOf

@StateFactoryMarker
fun <T : Any?> derivedStateOf(calculation: () -> T): State<T>
  • remember 블록 안에서 사용되며, 계산에서 사용되는 상태 중 하나가 변경될 때만 계산이 실행된다.
  • 블록의 마지막 라인을 State<T> 타입으로 반환한다.
  • remember(key) { ... } 형태로 선언하는 경우, key 값이 변경될 때 이하의 블록이 실행된다.
val showButton by remember {
    derivedStateOf {
        scrollState.firstVisibleItemIndex > 0
    }
}
  • derivedStateOf 에 의해 상태가 업데이트되는 경우에는 재구성이 발생하지 않는다.
  • key 값이 변경되는 경우에는 새로운 derivedStateOf 가 생성되면서 재구성이 발생한다.

 

7. snapshotFlow

  • State<T> 객체를 Flow 로 반환시켜주기 때문에, State<T> 객체를 Flow 로 사용해야 할 때 사용한다.
var showButton by remember { mutableStateOf(false) }

LaunchedEffect(scrollState) {
    snapshotFlow { scrollState.firstVisibleItemIndex }
        .map { index -> index > 0 }
        .distinctUntilChanged() // 새로운 값이 이전의 값과 차이가 있을 경우에만 감지할 수 있도록 도와준다.
        .collect {
            showButton = it
        }
}
  • Flow 에서 사용되는 collect 는 코루틴 상에서 동작해야 하기 때문에, LaunchedEffect 를 통해 코루틴 상에서 동작할 수 있도록 감싸준다.

 

LaunchedEffect , Side Effect 그리고 rememberCoroutine 정리

오늘은 Jetpack Compose 의 Side Effect 와 LaunchedEffect, 그리고 rememberCoroutine 에 대해서 정리해 보도록 하겠습니다. 1. Side Effect Side effect 의 단어 뜻은 원래 부차적이고, 의도하지 않은 효과를 말하는데요.

developer88.tistory.com

 

[Jetpack] Compose 사용하기 - 2. Side Effect와 Coroutine 1

본 게시글은 이전 글에 이어서 작성된 부분입니다.2022.06.07 - [Android/Jetpack] - [Jetpack] Compose 사용하기 - 1. remember와 MutableState 기존의 코드에서도 코루틴을 많이 사용하기 때문에, Compose를 사용할 때

heegs.tistory.com

 

[Jetpack] Compose 사용하기 - 2. Side Effect와 Coroutine 2

본 게시글은 이전 글에 이어서 작성된 부분입니다.2022.06.07 - [Android/Jetpack] - [Jetpack] Compose 사용하기 - 1. remember와 MutableState2022.06.10 - [Android/Jetpack] - [Jetpack] Compose 사용하기 - 2. Side Effect와 Coroutine 1

heegs.tistory.com