부수 효과는 구성 가능한 함수의 범위 밖에서 발생하는 앱 상태에 관한 변경사항입니다. 컴포저블의 수명 주기 및 속성(예: 예측할 수 없는 리컴포지션 또는 다른 순서로 컴포저블의 리컴포지션 실행, 삭제할 수 있는 리컴포지션)으로 인해 컴포저블에는 부수 효과가 없는 것이 좋습니다.
그러나 부수 효과가 필요한 때도 있습니다. 예를 들어 스낵바를 표시하거나 특정 상태 조건에 따라 다른 화면으로 이동하는 등 일회성 이벤트를 트리거할 때입니다. 이러한 작업은 컴포저블의 수명 주기를 인식하는 관리된 환경에서 호출해야 합니다. 이 페이지에서는 Jetpack Compose가 제공하는 다양한 부수 효과 API에 관해 알아봅니다.
상태 및 효과 사용 사례
Compose 이해 문서에 설명된 대로 컴포저블에는 부수 효과가 없어야 합니다. 상태 관리 문서에 설명된 대로 앱 상태를 변경해야 하는 경우 이러한 부수 효과가 예측 가능한 방식으로 실행되도록 Effect API를 사용해야 합니다.
Compose에서 다양한 가능성 효과를 이용할 수 있기 때문에 과다하게 사용될 수 있습니다. 상태 관리 문서에 설명된 대로 효과에서 실행하는 작업이 UI와 관련되고 단방향 데이터 흐름을 중단하지 않아야 합니다.
LaunchedEffect
: 컴포저블의 범위에서 정지 함수를 실행합니다.
컴포저블의 수명 동안 작업을 실행하고
정지 함수를 사용하려면
LaunchedEffect
드림
있습니다. LaunchedEffect
가 컴포지션을 시작하면 매개변수로 전달된 코드 블록으로 코루틴이 실행됩니다. LaunchedEffect
가 컴포지션을 종료하면 코루틴이 취소됩니다. LaunchedEffect
가 다른 키로 재구성되면(아래 효과 다시 시작 섹션 참고) 기존 코루틴이 취소되고 새 코루틴에서 새 정지 함수가 실행됩니다.
예를 들어, 다음은 알파 값을 설정 가능한 지연:
// Allow the pulse rate to be configured, so it can be sped up if the user is running // out of time var pulseRateMs by remember { mutableStateOf(3000L) } val alpha = remember { Animatable(1f) } LaunchedEffect(pulseRateMs) { // Restart the effect when the pulse rate changes while (isActive) { delay(pulseRateMs) // Pulse the alpha every pulseRateMs to alert the user alpha.animateTo(0f) alpha.animateTo(1f) } }
위 코드에서 애니메이션은 정지 함수를 사용합니다.
delay
드림
설정된 시간 동안 대기합니다. 그런 다음 순차적으로 알파 버전을
0으로 다시 되돌립니다.
animateTo
이 작업은 컴포저블 수명 동안 반복됩니다.
rememberCoroutineScope
: 컴포지션 인식 범위를 가져와 컴포저블 외부에서 코루틴을 실행합니다.
LaunchedEffect
는 구성 가능한 함수이므로 구성 가능한 다른 함수 내에서만 사용할 수 있습니다. 컴포저블 외부에 있지만 컴포지션을 종료한 후 자동으로 취소되도록 범위가 지정된 코루틴을 실행하려면 rememberCoroutineScope
를 사용하세요.
또한 코루틴 하나 이상의 수명 주기를 수동으로 관리해야 할 때마다(예: 사용자 이벤트가 발생할 때 애니메이션을 취소해야 하는 경우) rememberCoroutineScope
를 사용하세요.
rememberCoroutineScope
는 호출되는 컴포지션의 지점에 바인딩된 CoroutineScope
를 반환하는 구성 가능한 함수입니다. 호출이 컴포지션을 종료하면 범위가 취소됩니다.
이전 예에 따라 사용자가 Button
을 탭할 때 이 코드를 사용하여 Snackbar
를 표시할 수 있습니다.
@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") } } } }
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
이 재구성된 최신 값이 항상 포함되도록 하려면 rememberUpdatedState
로 onTimeout
을 래핑해야 합니다.
코드에서 반환된 State
, currentOnTimeout
은 효과에서 사용해야 합니다.
DisposableEffect
: 정리가 필요한 효과
키가 변경되거나 컴포저블이 컴포지션을 종료한 후 정리해야 하는 부수 효과의 경우 DisposableEffect
를 사용하세요.
DisposableEffect
키가 변경되면 컴포저블이 현재 효과를 삭제(정리)하고 효과를 다시 호출하여 재설정해야 합니다.
예를 들어 LifecycleObserver
를 사용하여 Lifecycle
이벤트를 기반으로 애널리틱스 이벤트를 전송할 수 있습니다.
Compose에서 이 이벤트를 수신 대기하려면 DisposableEffect
를 사용하여 필요에 따라 관찰자를 등록하고 등록 취소하세요.
@Composable fun HomeScreen( lifecycleOwner: LifecycleOwner = LocalLifecycleOwner.current, onStart: () -> Unit, // Send the 'started' analytics event onStop: () -> Unit // Send the 'stopped' analytics event ) { // Safely update the current lambdas when a new one is provided val currentOnStart by rememberUpdatedState(onStart) val currentOnStop by rememberUpdatedState(onStop) // If `lifecycleOwner` changes, dispose and reset the effect DisposableEffect(lifecycleOwner) { // Create an observer that triggers our remembered callbacks // for sending analytics events val observer = LifecycleEventObserver { _, event -> if (event == Lifecycle.Event.ON_START) { currentOnStart() } else if (event == Lifecycle.Event.ON_STOP) { currentOnStop() } } // Add the observer to the lifecycle lifecycleOwner.lifecycle.addObserver(observer) // When the effect leaves the Composition, remove the observer onDispose { lifecycleOwner.lifecycle.removeObserver(observer) } } /* Home screen content */ }
위의 코드에서는 효과가 observer
를 lifecycleOwner
에 추가합니다. lifecycleOwner
가 변경되면 효과가 삭제되고 새 lifecycleOwner
로 다시 시작됩니다.
DisposableEffect
는 onDispose
절을 코드 블록의 최종 문장으로 포함해야 합니다. 그러지 않으면 IDE에 빌드 시간 오류가 표시됩니다.
SideEffect
: Compose 상태를 비 Compose 코드에 게시합니다.
Compose 상태를 Compose에서 관리되지 않는 객체와 공유하려면 다음을 사용하세요.
SideEffect
드림
있습니다. SideEffect
를 사용하면
도움이 될 수 있습니다 반면에
리컴포지션이 성공하기 전에 효과를 실행하는 것이 일반적입니다. 즉
컴포저블에 직접 효과를 작성하는 경우입니다.
예를 들어 애널리틱스 라이브러리를 사용하면 커스텀 메타데이터(이 예에서는 '사용자 속성')를 이후의 모든 애널리틱스 이벤트에 연결하여 사용자 인구를 분류할 수 있습니다. 현재 사용자의 사용자 유형을 애널리틱스 라이브러리에 전달하려면 SideEffect
를 사용하여 값을 업데이트합니다.
@Composable fun rememberFirebaseAnalytics(user: User): FirebaseAnalytics { val analytics: FirebaseAnalytics = remember { FirebaseAnalytics() } // On every successful composition, update FirebaseAnalytics with // the userType from the current User, ensuring that future analytics // events have this metadata attached SideEffect { analytics.setUserProperty("userType", user.userType) } return analytics }
produceState
: 비 Compose 상태를 Compose 상태로 변환합니다.
produceState
는 반환된 State
로 값을 푸시할 수 있는 컴포지션으로 범위가 지정된 코루틴을 실행합니다. 비 Compose 상태를 Compose 상태로 변환하려면, 예를 들어 Flow
, LiveData
또는 RxJava
와 같은 외부 구독 기반 상태를 컴포지션으로 변환하려면 이 코루틴을 사용하세요.
produceState
가 컴포지션을 시작하면 프로듀서가 실행되고 컴포지션을 종료하면 취소됩니다. 반환된 State
는 합성됩니다. 동일한 값을 설정해도 리컴포지션이 트리거되지 않습니다.
produceState
가 코루틴을 만드는 경우에도 정지되지 않는 데이터 소스를 관찰하는 데 사용할 수 있습니다. 이 소스의 구독을 삭제하려면 awaitDispose
함수를 사용하세요.
다음 예에서는 produceState
를 사용하여 네트워크에서 이미지를 로드하는 방법을 보여줍니다. loadNetworkImage
구성 가능한 함수는 다른 컴포저블에서 사용할 수 있는 State
를 반환합니다.
@Composable fun loadNetworkImage( url: String, imageRepository: 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 inputs. return produceState<Result<Image>>(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
: 하나 이상의 상태 객체를 다른 상태로 변환합니다.
Compose에서 리컴포지션이 발생합니다. 관찰된 상태 객체 또는 컴포저블 입력이 변경될 때마다 상태 객체 UI가 실제로 업데이트해야 하는 것보다 더 자주 입력되거나 불필요한 리컴포지션이 발생합니다.
derivedStateOf
함수를 호출하기만 하면 됩니다.
재구성할 수 있습니다 주로 다음과 같이 무언가가 자주 변경될 때 발생합니다.
컴포저블은 스크롤 위치를 가로지르는 시점에만 반응해야 합니다.
특정 임곗값에 도달할 수 있습니다 derivedStateOf
는 개발자가 작성한 새 Compose 상태 객체를 만듭니다.
관찰할 수 있습니다 이러한 방식으로
이는 Kotlin Flow와 유사합니다.
distinctUntilChanged()
드림
연산자와 같습니다.
올바른 사용
다음 스니펫은 derivedStateOf
의 적절한 사용 사례를 보여줍니다.
@Composable // When the messages parameter changes, the MessageList // composable recomposes. derivedStateOf does not // affect this recomposition. fun MessageList(messages: List<Message>) { Box { val listState = rememberLazyListState() LazyColumn(state = listState) { // ... } // Show the button if the first visible item is past // the first item. We use a remembered derived state to // minimize unnecessary compositions val showButton by remember { derivedStateOf { listState.firstVisibleItemIndex > 0 } } AnimatedVisibility(visible = showButton) { ScrollToTopButton() } } }
이 스니펫에서 firstVisibleItemIndex
는 표시되는 첫 번째 항목이 변경될 때마다 항상 변경됩니다.
있습니다. 스크롤하면 값이 0
, 1
, 2
, 3
, 4
, 5
등이 됩니다.
그러나 값이 0
보다 큰 경우에만 리컴포지션이 발생해야 합니다.
이러한 업데이트 빈도 불일치는
derivedStateOf
잘못된 사용
흔히 범하는 실수는 두 개의 Compose 상태 객체를 결합할 때
'상태를 파생'하고 있으므로 derivedStateOf
를 사용해야 합니다. 그러나
다음 스니펫과 같이 전적으로 오버헤드이며 필요하지 않습니다.
// DO NOT USE. Incorrect usage of derivedStateOf. var firstName by remember { mutableStateOf("") } var lastName by remember { mutableStateOf("") } val fullNameBad by remember { derivedStateOf { "$firstName $lastName" } } // This is bad!!! val fullNameCorrect = "$firstName $lastName" // This is correct
이 스니펫에서 fullName
는 firstName
및
lastName
입니다. 따라서 과도한 리컴포지션이 발생하지 않고
derivedStateOf
는 필요하지 않습니다.
snapshotFlow
: Compose의 상태를 Flow로 변환
snapshotFlow
를 사용하여 State<T>
객체를 콜드 Flow로 변환합니다. snapshotFlow
는 수집될 때 블록을 실행하고 읽은 State
객체의 결과를 내보냅니다. snapshotFlow
블록 내에서 읽은 State
객체의 하나가 변경되면 새 값이 이전에 내보낸 값과 같지 않은 경우 Flow에서 새 값을 수집기에 내보냅니다(이 동작은 Flow.distinctUntilChanged
의 동작과 비슷함).
다음 예는 사용자가 목록에서 첫 번째 항목을 지나 분석까지 스크롤할 때 기록되는 부작용을 보여줍니다.
val listState = rememberLazyListState()
LazyColumn(state = listState) {
// ...
}
LaunchedEffect(listState) {
snapshotFlow { listState.firstVisibleItemIndex }
.map { index -> index > 0 }
.distinctUntilChanged()
.filter { it == true }
.collect {
MyAnalyticsService.sendScrolledPastFirstItemEvent()
}
}
위 코드에서 listState.firstVisibleItemIndex
는 Flow 연산자의 이점을 활용할 수 있는 Flow로 변환됩니다.
효과 다시 시작
LaunchedEffect
, produceState
, DisposableEffect
와 같은 Compose의 일부 효과에서 실행 중인 효과를 취소하는 데 사용되는 가변적인 수의 인수를 취하고 새 키로 새 효과를 시작합니다.
이 API의 일반적인 형태는 다음과 같습니다.
EffectName(restartIfThisKeyChanges, orThisKey, orThisKey, ...) { block }
이 동작은 미묘하므로 효과를 다시 시작하는 데 사용되는 매개변수가 올바른 매개변수가 아닌 경우 문제가 발생할 수 있습니다.
- 필요한 것보다 적은 효과를 다시 시작하면 앱에 버그가 발생할 수 있습니다.
- 필요한 것보다 많은 효과를 다시 시작하면 비효율적일 수 있습니다.
대체적으로 효과 코드 블록에 사용되는 변경할 수 있는 변수와 변경할 수 없는 변수는 효과 컴포저블에 매개변수로 추가해야 합니다. 이 매개변수 외에 효과를 강제로 다시 시작하도록 더 많은 매개변수를 추가할 수 있습니다. 변수를 변경해도 효과가 다시 시작되지 않아야 하는 경우 변수를 rememberUpdatedState
에 래핑해야 합니다. 변수가 키가 없는 remember
에 래핑되어 변경되지 않으면 변수를 효과에 키로 전달할 필요가 없습니다.
위에 표시된 DisposableEffect
코드에서 블록의 lifecycleOwner
가 변경되면 효과가 다시 시작되어야 하므로 효과에서 이를 매개변수로 취합니다.
@Composable
fun HomeScreen(
lifecycleOwner: LifecycleOwner = LocalLifecycleOwner.current,
onStart: () -> Unit, // Send the 'started' analytics event
onStop: () -> Unit // Send the 'stopped' analytics event
) {
// These values never change in Composition
val currentOnStart by rememberUpdatedState(onStart)
val currentOnStop by rememberUpdatedState(onStop)
DisposableEffect(lifecycleOwner) {
val observer = LifecycleEventObserver { _, event ->
/* ... */
}
lifecycleOwner.lifecycle.addObserver(observer)
onDispose {
lifecycleOwner.lifecycle.removeObserver(observer)
}
}
}
currentOnStart
및 currentOnStop
은 DisposableEffect
키로 필요하지 않습니다. rememberUpdatedState
의 사용으로 컴포지션에서 이 키의 값이 변경되지 않기 때문입니다. lifecycleOwner
가 매개변수로 전달되지 않고 변경되면 HomeScreen
은 재구성되지만 DisposableEffect
는 삭제되거나 다시 시작되지 않습니다. 이 시점부터 잘못된 lifecycleOwner
가 사용되므로 문제가 발생합니다.
키로 사용되는 상수
true
와 같은 상수를 호출 사이트의 수명 주기를 추적하는 효과 키로 사용할 수 있습니다. 위 LaunchedEffect
예와 같이 유효한 사용 사례가 있습니다. 그러나 사용하기 전에 신중하게 필요한 항목인지 확인하세요.
추천 서비스
- 참고: JavaScript가 사용 중지되어 있으면 링크 텍스트가 표시됩니다.
- 상태 및 Jetpack Compose
- Jetpack Compose용 Kotlin
- Compose에서 뷰 사용