수명 주기 및 부수 효과

컴포저블에는 부수 효과가 없어야 합니다. 하지만 앱의 상태를 변경하는 데 필요한 경우 컴포저블의 수명 주기를 인식하는 관리된 환경에서 부수 효과를 호출해야 합니다. 이 페이지에서는 컴포저블의 수명 주기 및 Jetpack Compose에서 제공하는 다양한 부수 효과 API에 관해 알아봅니다.

컴포저블의 수명 주기

상태 관리 문서에 설명된 대로 컴포지션은 앱의 UI를 설명하고 컴포저블을 실행하여 생성됩니다. 컴포지션은 UI를 기술하는 컴포저블의 트리 구조입니다.

Jetpack Compose는 초기 컴포지션 시 처음으로 컴포저블을 실행할 때 컴포지션에서 UI를 기술하기 위해 호출하는 컴포저블을 추적합니다. 그런 다음 앱 상태가 변경되면 Jetpack Compose는 리컴포지션을 예약합니다. 리컴포지션은 Jetpack Compose가 상태 변경사항에 따라 변경될 수 있는 컴포저블을 다시 실행한 다음 변경사항을 반영하도록 컴포지션을 업데이트하는 것입니다.

컴포지션은 초기 컴포지션을 통해서만 생성되고 리컴포지션을 통해서만 업데이트될 수 있습니다. 컴포지션을 수정하는 유일한 방법은 리컴포지션을 통하는 것입니다.

컴포저블의 수명 주기를 보여주는 다이어그램

그림 1. 컴포지션 내 컴포저블의 수명 주기. 컴포저블은 컴포지션을 시작하고 0회 이상 재구성되고 컴포지션을 종료합니다.

리컴포지션은 일반적으로 State<T> 객체가 변경되면 트리거됩니다. Compose는 이러한 객체를 추적하고 컴포지션에서 특정 State<T>를 읽는 모든 컴포저블 및 호출하는 컴포저블 중 건너뛸 수 없는 모든 컴포저블을 실행합니다.

컴포저블이 여러 번 호출되면 컴포지션에 여러 인스턴스가 배치됩니다. 컴퍼지션의 각 호출에는 자체 수명 주기가 있습니다.

@Composable
fun MyComposable() {
    Column {
        Text("Hello")
        Text("World")
    }
}

이전 코드 스니펫 내 요소의 계층적 배열을 보여주는 다이어그램

그림 2. 컴포지션 내 MyComposable의 표현. 컴포저블이 여러 번 호출되면 컴포지션에 여러 인스턴스가 배치됩니다. 색상이 다른 요소는 요소가 별도의 인스턴스임을 나타냅니다.

컴포지션 내 컴포저블의 분석

컴포지션 내 컴포저블의 인스턴스는 호출 사이트로 식별됩니다. Compose 컴파일러는 각 호출 사이트를 고유한 것으로 간주합니다. 여러 호출 사이트에서 컴포저블을 호출하면 컴포지션에 컴포저블의 여러 인스턴스가 생성됩니다.

리컴포지션 시 컴포저블이 이전 컴포지션 시 호출한 것과 다른 컴포저블을 호출하는 경우 Compose는 호출되거나 호출되지 않은 컴포저블을 식별하며 두 컴포지션 모두에서 호출된 컴포저블의 경우 입력이 변경되지 않은 경우 재구성하지 않습니다.

부수 효과를 컴포저블과 연결하기 위해서는 리컴포지션마다 다시 시작하는 대신 완료할 수 있도록 ID를 유지하는 것이 중요합니다.

다음 예를 참고하세요.

@Composable
fun LoginScreen(showError: Boolean) {
    if (showError) {
        LoginError()
    }
    LoginInput() // This call site affects where LoginInput is placed in Composition
}

@Composable
fun LoginInput() { /* ... */ }

위의 코드 스니펫에서 LoginScreenLoginError 컴포저블을 조건부로 호출하며 항상 LoginInput 컴포저블을 호출합니다. 각 호출에는 고유한 호출 사이트 및 컴파일러가 호출을 고유하게 식별하는 데 사용하는 소스 위치가 있습니다.

showError 플래그가 true로 변경된 경우 앞의 코드가 재구성되는 방식을 보여주는 다이어그램 LoginError 컴포저블이 추가되지만 다른 컴포저블은 재구성되지 않습니다.

그림 3. 상태가 변경되고 리컴포지션이 발생할 때 컴포지션 내 LoginScreen의 표현. 색상이 동일하면 재구성되지 않았음을 의미합니다.

LoginInput이 첫 번째로 호출되었다가 두 번째로 호출되었지만 LoginInput 인스턴스는 여러 리컴포지션에 걸쳐 유지됩니다. 또한 LoginInput에는 리컴포지션 간에 변경된 매개변수가 없으므로 Compose가 LoginInput 호출을 건너뜁니다.

스마트 리컴포지션에 도움이 되는 정보 추가

컴포저블을 여러 번 호출하면 컴포저블이 컴포지션에도 여러 번 추가됩니다. 동일한 호출 사이트에서 컴포저블을 여러 번 호출하는 경우 Compose가 각 컴포저블 호출을 고유하게 식별할 수 있는 정보가 없으므로 인스턴스를 구분하기 위해 호출 사이트 외에 실행 순서가 사용됩니다. 이 동작만 필요한 경우도 있지만 경우에 따라 원치 않는 동작이 발생할 수도 있습니다.

@Composable
fun MoviesScreen(movies: List<Movie>) {
    Column {
        for (movie in movies) {
            // MovieOverview composables are placed in Composition given its
            // index position in the for loop
            MovieOverview(movie)
        }
    }
}

위의 예에서 Compose는 호출 사이트 외에 실행 순서를 사용하여 컴포지션에서 인스턴스를 구분합니다. 새 movie가 목록의 하단에 추가된 경우 Compose는 인스턴스의 목록 내 위치가 변경되지 않았고 따라서 인스턴스의 movie 입력이 동일하므로 컴포지션에 이미 있는 인스턴스를 재사용할 수 있습니다.

목록의 하단에 새 요소가 추가된 경우 앞의 코드가 재구성되는 방식을 보여주는 다이어그램. 목록에 있는 다른 항목의 위치는 변경되지 않았으며 재구성되지 않습니다.

그림 4. 목록의 하단에 새 요소가 추가된 경우 되면 컴포지션 내 MoviesScreen의 표현. 컴포지션의 MovieOverview 컴포저블은 재사용할 수 있습니다. MovieOverview의 색상이 동일하면 컴포저블이 재구성되지 않았음을 의미합니다.

하지만 목록의 상단 또는 가운데에 항목을 추가하거나 항목을 삭제하거나 재정렬하여 movies 목록이 변경되면 목록에서 입력 매개변수의 위치가 변경된 모든 MovieOverview 호출에서 리컴포지션이 발생합니다. 이는 예를 들어 MovieOverview가 부수 효과를 사용하여 영화 이미지를 가져오는 경우 매우 중요합니다. 효과가 적용되는 동안 리컴포지션이 발생하면 효과가 취소되고 다시 시작됩니다.

@Composable
fun MovieOverview(movie: Movie) {
    Column {
        // Side effect explained later in the docs. If MovieOverview
        // recomposes, while fetching the image is in progress,
        // it is cancelled and restarted.
        val image = loadNetworkImage(movie.url)
        MovieHeader(image)

        /* ... */
    }
}

목록의 상단에 새 요소가 추가된 경우 앞의 코드가 재구성되는 방식을 보여주는 다이어그램. 목록에 있는 다른 모든 항목의 위치가 변경되며 재구성해야 합니다.

그림 5. 목록에 새 요소가 추가될 때 컴포지션 내 MoviesScreen의 표현 MovieOverview 컴포저블은 재사용할 수 없으며 모든 부수 효과가 다시 시작됩니다. MovieOverview의 색상이 다르면 컴포저블이 재구성되었음을 의미합니다.

이상적으로 MovieOverview 인스턴스의 ID는 인스턴스에 전달된 movie의 ID에 연결된 것으로 간주됩니다. 영화 목록을 재정렬하는 경우 다른 영화 인스턴스로 각 MovieOverview 컴포저블을 재구성하는 대신 컴포지션 트리 내 인스턴스를 재정렬하는 것이 이상적입니다. Compose에서 런타임에 트리의 특정 부분(key 컴포저블)을 식별하는 데 사용할 값을 지정할 수 있습니다.

주요 컴포저블 호출로 코드 블록을 래핑하고 하나 이상의 값을 전달하면 이러한 값이 컴포지션에서 인스턴스를 식별하는 데 함께 사용됩니다. key 값은 전체적으로 고유하지 않아도 되며 호출 사이트에서의 컴포저블 호출 간에만 고유하면 됩니다. 따라서 이 예에서 각 movie에는 movies 사이에 고유한 key가 있어야 합니다. 앱의 다른 위치에 있는 다른 컴포저블과 이 key를 공유하는 것은 괜찮습니다.

@Composable
fun MoviesScreen(movies: List<Movie>) {
    Column {
        for (movie in movies) {
            key(movie.id) { // Unique ID for this movie
                MovieOverview(movie)
            }
        }
    }
}

위에서 목록의 요소가 변경되더라도 Compose는 개별 MovieOverview 호출을 인식하고 재사용할 수 있습니다.

목록의 상단에 새 요소가 추가된 경우 앞의 코드가 재구성되는 방식을 보여주는 다이어그램. 목록 항목이 키로 식별되므로 Compose는 항목의 위치가 변경된 경우에도 항목을 재구성하지 않습니다.

그림 6. 목록에 새 요소가 추가될 때 컴포지션 내 MoviesScreen의 표현 MovieOverview 컴포저블에는 고유 키가 있으므로 Compose가 변경되지 않은 MovieOverview 인스턴스를 인식하고 재사용할 수 있습니다. 인스턴스의 부수 효과는 계속 실행됩니다.

일부 컴포저블에는 key 컴포저블 지원 기능이 내장되어 있습니다. 예를 들어 LazyColumn의 경우 items DSL에 맞춤 key를 지정할 수 있습니다.

@Composable
fun MoviesScreen(movies: List<Movie>) {
    LazyColumn {
        items(movies, key = { movie -> movie.id }) { movie ->
            MovieOverview(movie)
        }
    }
}

입력이 변경되지 않은 경우 건너뛰기

컴포지션에 이미 컴포저블이 있는 경우 모든 입력이 안정적이고 변경되지 않았으면 리컴포지션을 건너뛸 수 있습니다.

안정적인 유형은 다음 계약을 준수해야 합니다.

  • 두 인스턴스의 equals 결과가 동일한 두 인스턴스의 경우 항상 동일합니다.
  • 유형의 공개 속성이 변경되면 컴포지션에 알림이 전송됩니다.
  • 모든 공개 속성 유형도 안정적입니다.

명시적으로 @Stable로 표시되지 않더라도 Compose 컴파일러가 @Stable로 간주하며 이 계약에 포함되는 중요한 일반 유형이 있습니다.

  • 모든 원시 값 유형: Boolean, Int, Long, Float, Char
  • 문자열
  • 모든 함수 유형(람다)

이 유형은 모두 변경할 수 없으므로 @Stable 계약을 따를 수 있습니다. 변경할 수 없는 유형은 절대 변경되지 않으므로 컴포지션에 변경사항을 알리지 않아도 되며 따라서 이 계약을 훨씬 더 쉽게 따를 수 있습니다.

안정적이지만 변경할 수 있는 한 가지 중요한 유형은 Compose의 MutableState 유형입니다. 값이 MutableState로 유지되는 경우 State.value 속성이 변경되면 Compose에 알림이 전송되므로 상태 객체는 전체적으로 안정적인 것으로 간주됩니다.

컴포저블에 매개변수로 전달된 모든 유형이 안정적인 경우 UI 트리 내 컴포저블 위치를 기반으로 매개변수 값이 동일한지 비교합니다. 이전 호출 이후 모든 값이 변경되지 않은 경우 리컴포지션을 건너뜁니다.

Compose는 증명할 수 있는 경우에만 유형을 안정적인 것으로 간주합니다. 예를 들어 인터페이스는 일반적으로 안정적이지 않은 것으로 간주되며 구현을 변경할 수 없으며 변경할 수 있는 공개 속성이 있는 유형도 안정적이지 않습니다.

Compose가 유형이 안정적이라고 추론할 수 없지만 안정적인 것으로 간주하도록 하려면 @Stable 주석으로 표시하세요.

// Marking the type as stable to favor skipping and smart recompositions.
@Stable
interface UiState<T : Result<T>> {
    val value: T?
    val exception: Throwable?

    val hasError: Boolean
        get() = exception != null
}

위의 코드 스니펫에서는 UiState가 인터페이스이므로 Compose가 일반적으로 이 유형을 안정적이지 않은 것으로 간주할 수 있습니다. @Stable 주석을 추가하면 Compose가 이 유형이 안정적임을 알게 되고 스마트 리컴포지션을 선호하게 됩니다. 즉 인터페이스가 매개변수 유형으로 사용되는 경우 Compose가 모든 구현을 안정적인 것으로 간주합니다.

상태 및 효과 사용 사례

Compose 이해 문서에 설명된 대로 컴포저블에는 부수 효과가 없어야 합니다. 상태 관리 문서에 설명된 대로 앱 상태를 변경해야 하는 경우 이러한 부수 효과가 예측 가능한 방식으로 실행되도록 Effect API를 사용해야 합니다.

Compose에서 다양한 가능성 효과를 이용할 수 있기 때문에 과다하게 사용될 수 있습니다. 상태 관리 문서에 설명된 대로 효과에서 실행하는 작업이 UI와 관련되고 단방향 데이터 흐름을 중단하지 않아야 합니다.

LaunchedEffect: 컴포저블의 범위에서 정지 함수 실행

컴포저블 내에서 안전하게 정지 함수를 호출하려면 LaunchedEffect 컴포저블을 사용하세요. LaunchedEffect가 컴포지션을 시작하면 매개변수로 전달된 코드 블록으로 코루틴이 실행됩니다. LaunchedEffect가 컴포지션을 종료하면 코루틴이 취소됩니다. LaunchedEffect가 다른 키로 재구성되면(아래 효과 다시 시작 섹션 참고) 기존 코루틴이 취소되고 새 코루틴에서 새 정지 함수가 실행됩니다.

예를 들어 ScaffoldSnackbar는 정지 함수인 SnackbarHostState.showSnackbar 함수를 사용하여 표시됩니다.

@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` 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` changes.
            scaffoldState.snackbarHostState.showSnackbar(
                message = "Error message",
                actionLabel = "Retry message"
            )
        }
    }

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

위 코드에서는 상태에 오류가 포함되어 있으면 코루틴이 트리거되고 오류가 포함되어 있지 않으면 취소됩니다. LaunchedEffect 호출 사이트가 if 문 내에 있으므로 문장이 거짓일 때 LaunchedEffect가 컴포지션에 있으면 삭제되고 따라서 코루틴이 취소됩니다.

rememberCoroutineScope: 컴포지션 인식 범위를 확보하여 컴포저블 외부에서 코루틴 실행

LaunchedEffect는 구성 가능한 함수이므로 구성 가능한 다른 함수 내에서만 사용할 수 있습니다. 컴포저블 외부에 있지만 컴포지션을 종료한 후 자동으로 취소되도록 범위가 지정된 코루틴을 실행하려면 rememberCoroutineScope를 사용하세요. 또한 코루틴 하나 이상의 수명 주기를 수동으로 관리해야 할 때마다, 예를 들어 사용자 이벤트가 발생할 때 애니메이션을 취소해야 하는 경우 rememberCoroutineScope를 사용하세요.

rememberCoroutineScope는 호출되는 컴포지션의 지점에 바인딩된 CoroutineScope를 반환하는 구성 가능한 함수입니다. 호출이 컴포지션을 종료하면 범위가 취소됩니다.

이전 예에 따라 사용자가 Button을 탭할 때 이 코드를 사용하여 Snackbar를 표시할 수 있습니다.

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

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

주요 매개변수 중 하나가 변경되면 LaunchedEffect가 다시 시작됩니다. 하지만 경우에 따라 효과에서 값이 변경되면 효과를 다시 시작하지 않을 값을 캡처할 수 있습니다. 이렇게 하려면 rememberUpdatedState를 사용하여 캡처하고 업데이트할 수 있는 이 값의 참조를 만들어야 합니다. 이 접근 방식은 비용이 많이 들거나 다시 만들고 다시 시작할 수 없도록 금지된 오래 지속되는 작업이 포함된 효과에 유용합니다.

예를 들어 앱에 시간이 지나면 사라지는 LandingScreen이 있다고 가정해 보겠습니다. LandingScreen이 재구성되는 경우에도 일정 시간 동안 대기하고 시간이 경과되었음을 알리는 효과는 다시 시작해서는 안 됩니다.

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

호출 사이트의 수명 주기와 일치하는 효과를 만들기 위해 Unit 또는 true와 같이 변경되지 않는 상수가 매개변수로 전달됩니다. 위 코드에서는 LaunchedEffect(true)가 사용됩니다. onTimeout 람다에 LandingScreen이 재구성된 최신 값이 항상 포함되도록 하려면 rememberUpdatedStateonTimeout을 래핑해야 합니다. 코드에서 반환된 State, currentOnTimeout은 효과에서 사용해야 합니다.

DisposableEffect: 정리가 필요한 효과

키가 변경되거나 컴포저블이 컴포지션을 종료한 후 정리해야 하는 부수 효과의 경우 DisposableEffect를 사용하세요. DisposableEffect 키가 변경되면 컴포저블이 현재 효과를 삭제(정리)하고 효과를 다시 호출하여 재설정해야 합니다.

예를 들어 OnBackPressedDispatcher에서 뒤로 버튼을 누르는 동작을 수신 대기하려면 OnBackPressedCallback을 등록해야 합니다. Compose에서 이 이벤트를 수신 대기하려면 DisposableEffect를 사용하여 필요에 따라 콜백을 등록하고 등록 취소하세요.

@Composable
fun BackHandler(backDispatcher: OnBackPressedDispatcher, onBack: () -> Unit) {

    // Safely update the current `onBack` lambda when a new one is provided
    val currentOnBack by rememberUpdatedState(onBack)

    // Remember in Composition a back callback that calls the `onBack` lambda
    val backCallback = remember {
        // Always intercept back events. See the SideEffect for
        // a more complete version
        object : OnBackPressedCallback(true) {
            override fun handleOnBackPressed() {
                currentOnBack()
            }
        }
    }

    // If `backDispatcher` changes, dispose and reset the effect
    DisposableEffect(backDispatcher) {
        // Add callback to the backDispatcher
        backDispatcher.addCallback(backCallback)

        // When the effect leaves the Composition, remove the callback
        onDispose {
            backCallback.remove()
        }
    }
}

위의 코드에서는 효과가 저장된 backCallbackbackDispatcher에 추가합니다. backDispatcher가 변경되면 효과가 삭제되고 다시 시작됩니다.

DisposableEffectonDispose 절을 코드 블록의 최종 문으로 포함해야 합니다. 그렇지 않으면 IDE에 빌드 시간 오류가 표시됩니다.

부수 효과: Compose 상태를 비 Compose 코드에 게시

Compose 상태를 Compose에서 관리하지 않는 객체와 공유하려면 리컴포지션 성공 시마다 호출되는 SideEffect 컴포저블을 사용하세요.

콜백을 사용 설정해야 하는지 전달하려면 위의 BackHandler 코드를 참고한 후 SideEffect를 사용하여 값을 업데이트합니다.

@Composable
fun BackHandler(
    backDispatcher: OnBackPressedDispatcher,
    enabled: Boolean = true, // Whether back events should be intercepted or not
    onBack: () -> Unit
) {
    /* ... */
    val backCallback = remember { /* ... */ }

    // On every successful composition, update the callback with the `enabled` value
    // to tell `backCallback` whether back events should be intercepted or not
    SideEffect {
        backCallback.isEnabled = enabled
    }

    /* Rest of the code */
}

produceState: 비 Compose 상태를 Compose 상태로 변환

produceState는 반환된 State로 값을 푸시할 수 있는 컴포지션으로 범위가 지정된 코루틴을 실행합니다. 비 Compose 상태를 Compose 상태로 변환하려면, 예를 들어 Flow, LiveData 또는 RxJava와 같은 외부 구독 기반 상태를 컴포지션으로 변환하려면 이 코루틴을 사용하세요.

produceState가 컴포지션을 시작하면 프로듀서가 실행되고 컴포지션을 종료하면 취소됩니다. 반환된 State는 합성됩니다. 동일한 값을 설정해도 리컴포지션이 트리거되지 않습니다.

produceState가 코루틴을 만드는 경우에도 정지되지 않는 데이터 소스를 관찰하는 데 사용할 수 있습니다. 이 소스의 구독을 삭제하려면 awaitDispose 함수를 사용하세요.

다음 예에서는 produceState를 사용하여 네트워크에서 이미지를 로드하는 방법을 보여줍니다. loadNetworkImage 구성 가능한 함수는 다른 컴포저블에서 사용할 수 있는 State를 반환합니다.

@Composable
fun loadNetworkImage(
    url: String,
    imageRepository: ImageRepository
): State<Result<Image>> {

    // Creates a State<T> with Result.Loading as initial value
    // If either `url` or `imageRepository` changes, the running producer
    // will cancel and will be re-launched with the new keys.
    return produceState(initialValue = Result.Loading, url, imageRepository) {

        // In a coroutine, can make suspend calls
        val image = imageRepository.load(url)

        // Update State with either an Error or Success result.
        // This will trigger a recomposition where this State is read
        value = if (image == null) {
            Result.Error
        } else {
            Result.Success(image)
        }
    }
}
.

derivedStateOf: 하나 이상의 상태 객체를 다른 상태로 변환

다른 상태 객체에서 특정 상태가 계산되거나 파생되는 경우 derivedStateOf를 사용하세요. 이 함수를 사용하면 계산에서 사용되는 상태 중 하나가 변경될 때만 계산이 실행됩니다.

다음 예에서는 우선순위가 높은 사용자 정의 키워드가 있는 작업이 먼저 표시되는 기본 할 일 목록을 보여줍니다.

@Composable
fun TodoList(
    highPriorityKeywords: List<String> = listOf("Review", "Unblock", "Compose")
) {
    val todoTasks = remember { mutableStateListOf<String>() }

    // Calculate high priority tasks only when the todoTasks or
    // highPriorityKeywords change, not on every recomposition
    val highPriorityTasks by remember(todoTasks, highPriorityKeywords) {
        derivedStateOf {
            todoTasks.filter { it.containsWord(highPriorityKeywords) }
        }
    }

    Box(Modifier.fillMaxSize()) {
        LazyColumn {
            items(highPriorityTasks) { /* ... */ }
            items(todoTasks) { /* ... */ }
        }
        /* Rest of the UI where users can add elements to the list */
    }
}

위 코드에서 derivedStateOftodoTasks 또는 highPriorityKeywords가 변경될 때마다 highPriorityTasks 계산이 실행되고 그에 따라 UI가 업데이트되도록 보장합니다. highPriorityTasks를 계산하기 위한 필터링은 비용이 많이 들 수 있으므로 매 리컴포지션 시가 아니라 목록이 변경될 때만 실행해야 합니다.

또한 derivedStateOf에 의해 상태가 업데이트되어도 업데이트가 선언된 컴포저블이 재구성되지 않습니다. Compose는 예의 LazyColumn 내에서 반환된 상태를 읽는 위치의 컴포저블만 재구성합니다.

효과 다시 시작

LaunchedEffect, produceState, DisposableEffect와 같은 Compose의 일부 효과에서 실행 중인 효과를 취소하는 데 사용되는 가변적인 수의 인수를 취하고 새 키로 새 효과를 시작합니다.

이 API의 일반적인 형태는 다음과 같습니다.

EffectName(restartIfThisKeyChanges, orThisKey, orThisKey, ...) { block }

이 동작은 미묘하므로 효과를 다시 시작하는 데 사용되는 매개변수가 올바른 매개변수가 아닌 경우 문제가 발생할 수 있습니다.

  • 필요한 것보다 적은 효과를 다시 시작하면 앱에 버그가 발생할 수 있습니다.
  • 필요한 것보다 많은 효과를 다시 시작하면 비효율적일 수 있습니다.

대체적으로 효과 코드 블록에 사용되는 변경할 수 있는 변수와 변경할 수 없는 변수는 효과 컴포저블에 매개변수로 추가해야 합니다. 이 매개변수 외에 효과를 강제로 다시 시작하도록 더 많은 매개변수를 추가할 수 있습니다. 변수를 변경해도 효과가 다시 시작되지 않아야 하는 경우 변수를 rememberUpdatedState에 래핑해야 합니다. 변수가 키가 없는 remember에 래핑되어 변경되지 않으면 변수를 효과에 키로 전달할 필요가 없습니다.

위에 표시된 DisposableEffect 코드에서 블록의 backDispatcher가 변경되면 효과가 다시 시작되어야 하므로 효과에서 이를 매개변수로 취합니다.

@Composable
fun BackHandler(backDispatcher: OnBackPressedDispatcher, onBack: () -> Unit) {
    /* ... */
    val backCallback = remember { /* ... */ }

    DisposableEffect(backDispatcher) {
        backDispatcher.addCallback(backCallback)
        onDispose {
            backCallback.remove()
        }
    }
}

backCallback은 컴포지션에서 값이 변경되지 않으므로 DisposableEffect 키로 필요하지 않으며 키가 없는 remember에 래핑됩니다. backDispatcher가 매개변수로 전달되지 않고 변경되면 BackHandler는 재구성되지만 DisposableEffect는 삭제되지 않고 다시 시작됩니다. 따라서 이 시점부터 잘못된 backDispatcher가 사용되기 때문에 문제가 발생합니다.

키로 사용되는 상수

true와 같은 상수를 호출 사이트의 수명 주기를 추적하는 효과 키로 사용할 수 있습니다. 위 LaunchedEffect 예와 같이 유효한 사용 사례가 있습니다. 그러나 사용하기 전에 신중하게 필요한 항목인지 확인하세요.