Android/Compose

[Compose] Compose의 Side-Effect(1)

JunsuKim 2023. 4. 25.
728x90

Compose의 부수 효과

부수 효과는 구성 가능한 함수의 범위 밖에서 발생하는 앱 상태에 관한 변경사항이다.

즉 자신이 아닌 외부의 상태에 영향을 만드는 것이다.

 

예측할 수 없는 리컴포지션 또는 예상과는 다른 Composable의 리컴포지션 실행, 삭제할 수 없는 리컴포지션 등의 속성과 Composable의 수명 주기로 인해 부수 효과가 없는 것이 좋다.

 

하지만 스낵바를 표시하거나 특정 상태 조건에 따라 다른 화면으로 이동하는 등 일회성 이벤트를 트리거할 때 부수 효과가 필요하기도 하다.

Composable에서 Composable이 아닌 앱 상태에 대한 변화를 주는 것이므로 양방향 의존성으로 인해 예측할 수 없는 Effect가 생길 수 있다. 이 Effect를 Side Effect(부수 효과)라고 한다.

 LaunchedEffect: Composable 범위에서 정지 함수 실행

LaunchedEffect는 Composable 내에서 안전하게 suspend 함수를 호출하기 위한 Composable이다.

LaunchedEffect가 Composition에 들어가면 코드 블록이 매개변수로 전달된 코루틴을 실행한다.

만약 LaunchedEffect가 Composition을 떠난다면 코루틴이 취소된다.

LaunchedEffect가 다른 키로 재구성되면 기존 코루틴이 취소되고 새로운 suspend 함수가 새로운 코루틴에서 실행된다.

@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) {
        /* ... */
    }
}

위 코드를 분석해 보자.

state.hasError가 true라면 LaunchedEffect가 생성되고, 이로 인해 코루틴이 트리거된다.

state.hasError가 false가 된다면 LaunchedEffect는 Composition에서 사라지고 기존의 LaunchedEffect의 코루틴이 취소되게 된다.

rememberCoroutineScope:
Composable 외부에서 코루틴을 실행할 수 있는 Composition 범위 확보

LaunchedEffect는 Composable 함수이므로 다른 Composable 함수 내에서만 사용할 수 있다.

Composable 외부에서 있지만 Compostion을 종료한 후 자동으로 취소되도록 범위가 지정된 코루틴을 사용해야 할 때,

혹은 하나 이상의 수명 주기를 수동으로 관리해야 할 때 rememberCoroutineScope를 사용한다.

 

rememberCoroutineScope는 호출된 Composition 지점에 바인딩된 CoroutineScope를 반환하는 Composable 함수이다.

호출이 Composition를 벗어나면 Scope는 취소된다.

@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")
            }
        }
    }
}

코드를 분석해 보자.

MovieScreen에서 rememberCoroutineScope를 호출하여 MovieScreen의 생명주기에 바운딩된 CoroutineScope를 반환한다.

이는 하위 Composable인 버튼에서 버튼이 클릭되었을 때 scope.launch를 통해 코루틴을 실행하는 것을 확인할 수 있다.

이 코루틴은 Button의 Composition이 종료된다 하더라도 MovieScreen의 LifeCycle에 따라 MovieScreen이 종료될 때까지 남아있게 된다.

rememberUpdatedState:
값이 변경되는 경우 다시 시작되지 않아야 하는 효과에서 값 참조

매개변수 중 한 개의 값이라도 변경된다면 LaunchedEffect는 다시 시작된다.

하지만 경우에 따라 효과가 변경된 경우 Effect를 다시 시작하지 않는 Effect의 값을 저장할 수 있다.

이 값을 통해 매개변수의 값이 변경되더라도 효과가 변경되지 않게 할 수 있다.

이때 rememberUpdatedState가 실행되게 된다.

이는 비용이 많이 들거나 재생성, 재시작할 수 없도록 금지된 작업을 포함하는 Effect에 유용하다.

@Composable
fun LandingScreen(onTimeout: () -> Unit) {

    // This will always refer to the latest onTimeout function that
    // LandingScreen was recomposed with
    val currentOnTimeout by rememberUpdatedState(onTimeout)

    // Create an effect that matches the lifecycle of LandingScreen.
    // If LandingScreen recomposes, the delay shouldn't start again.
    LaunchedEffect(true) {
        delay(SplashWaitTimeMillis)
        currentOnTimeout()
    }

    /* Landing screen content */
}

call site의 lifecycle과 일치하는 효과를 만들기 위해 Unit 또는 true와 같은 상수를 파라미터로 전달한다.

onTimeOut 람다에 LandingScreen이 재구성된 최신 값이 항상 포함되도록 하려면 RememberUpdatedState 함수로 onTimeout을 래핑해야 한다.

728x90

댓글