[Android/Compose] rememberCoroutineScope vs LaunchedEffect

문승연·2023년 12월 20일
1

Compose와 부수 효과(Side Effect)

기본적으로 Composable 함수 안에서는 기존의 방식으로 코루틴을 사용할 수 없다. 대신 Compose에서도 코루틴을 구현할 수 있도록 Effect API라는 것을 제공한다.

안드로이드에서는 Compose 함수 외부에서 앱 상태가 변화하는 거를 부수 효과(Side Effect)라고 한다. 그리고 공식문서에는 이러한 부수 효과를 일으키는 요소가 Compose 함수 내에 존재해서는 안된다고 정의하고 있다.

컴포저블의 수명 주기 및 속성(예: 예측할 수 없는 리컴포지션 또는 다른 순서로 컴포저블의 리컴포지션 실행, 삭제할 수 있는 리컴포지션)으로 인해 컴포저블에는 부수 효과가 없는 것이 좋습니다.
출처: 안드로이드 공식 홈페이지

Compose 구성요소들은 언제 리컴포지션이 발생할 지 모르기 때문에 Compose 함수에서 부수 효과를 발생시키는 경우 예상치 못한 현상이 발생할 수도 있다는 뜻이다.

하지만 부수 효과가 필요할 때도 있기 때문에 이를 위해서 나온 게 바로 부수 효과(Side Effect) API이다.

LaunchedEffect

부수효과 API의 가장 대표적인 컴포지션이 바로 LaunchedEffect 이다. LaunchedEffect를 사용하면 컴포저블 함수 내에서도 안전하게 정지 함수를 호출할 수 있다.

LaunchedEffectstate 형태의 key 값과 실행 블럭을 매개변수로 받는다. key 값에 변화가 생기면 실행 블럭 내부에 있는 코드가 실행된다.

@Composable
fun MyScreen(
    state: UiState<List<Movie>>,
    scaffoldState: ScaffoldState = rememberScaffoldState()
) {

    // If the UI state contains an error, show snackbar
    if (state.hasError) {

        // `LaunchedEffect` will cancel and re-launch if
        // `scaffoldState.snackbarHostState` changes
        LaunchedEffect(scaffoldState.snackbarHostState) {
            // Show snackbar using a coroutine, when the coroutine is cancelled the
            // snackbar will automatically dismiss. This coroutine will cancel whenever
            // `state.hasError` is false, and only start when `state.hasError` is true
            // (due to the above if-check), or if `scaffoldState.snackbarHostState` changes.
            scaffoldState.snackbarHostState.showSnackbar(
                message = "Error message",
                actionLabel = "Retry message"
            )
        }
    }

    Scaffold(scaffoldState = scaffoldState) {
        /* ... */
    }
}

위의 LaunchedEffect 의 경우 scaffoldState.snakbarHostState)를 key 값으로 받고 있으며 해당 값에 변화가 생길 경우 LaunchedEffect 실행 블럭 내부의 코드가 실행되어 스낵바를 보여준다.

rememberCoroutineScope

부수효과를 구현하는 또 다른 방법으로는 rememberCoroutineScope 함수로 호출된 컴포지션에 바인딩된 CoroutineScope를 반환하여 해당 scope 안에서 suspend 함수를 구현하는 방법이다.

rememberCoroutineScope는 호출된 컴포지션에 바인딩되어있는 scope를 반환하기 때문에 해당 컴포지션이 취소되면 coroutineScope도 같이 취소된다.

@Composable
fun MoviesScreen(scaffoldState: ScaffoldState = rememberScaffoldState()) {

    // Creates a CoroutineScope bound to the MoviesScreen's lifecycle
    val scope = rememberCoroutineScope()

    Scaffold(scaffoldState = scaffoldState) {
        Column {
            /* ... */
            Button(
                onClick = {
                    // Create a new coroutine in the event handler to show a snackbar
                    scope.launch {
                        scaffoldState.snackbarHostState.showSnackbar("Something happened!")
                    }
                }
            ) {
                Text("Press me")
            }
        }
    }
}

val scope = rememberCoroutineScope()를 통해서 생성된 scope는 MovieScreen 이라는 컴포저블 함수의 생명주기를 따르게 된다.

이때 주의해야할 점은 해당 scope로 생성된 코루틴은 어디까지나 MovieScreen이라는 컴포저블 함수의 생명주기만을 따르는 거지 실제 코루틴의 위치는 컴포저블 함수 외부에 있다는 사실이다.

처음에 설명했듯이 컴포저블 함수 내부에서는 외부의 값을 바꾸는 부수효과를 줄 수 없다. 대신 부수효과를 주기 위한 코루틴을 외부에 생성하고 이 코루틴의 생명주기가 Composable 함수 생명주기를 따르게 만들 뿐이다.

rememberCoroutineScope 사용시 주의 사항

rememberCoroutineScope는 컴포저블 외부에 있지만 컴포지션을 종료한 후 자동으로 취소되도록 범위가 지정된 코루틴을 실행하거나코루틴 하나 이상의 수명 주기를 수동으로 관리해야 할 때(예: 사용자 이벤트가 발생할 때 애니메이션을 취소해야 하는 경우) 유용하게 사용할 수 있다.

반면에 외부에 새로 코루틴을 만든다는 특성 때문에 사용시 주의사항이 있다.

바로 재구현(Recomposition)이 일어날 때는 rememberCoroutineScope로 생성된 코루틴이 취소되지 않는다는 것이다.

예를 들어, 컴포저블 함수 안에 Button을 만들고 onClick 블럭에 rememberCoroutineScope로 코루틴을 생성하며 이 작업으로 재구현이 발생한다고 가정해보자.

사용자가 버튼을 누를때마다 재구현된 Button에서 계속해서 새로운 코루틴이 생성될 것이다.

이처럼 재구현이 상당히 자주 일어나는 화면에서 rememberCoroutineScope를 설정했다면 지나치게 많은 코루틴이 생성되어 심할 경우 앱 크래시를 유발할 수도 있다.

레퍼런스)
1. [Jetpack] Compose 사용하기 - 2. Side Effect와 Coroutine 1
2. Compose의 부수 효과

profile
"비몽(Bemong)"이라는 앱을 개발 및 운영 중인 안드로이드 개발자입니다.

0개의 댓글