Jetpack Compose Doc 읽기 — Part1[기초]
Compose 공식 가이드 읽기 (4/4)
Compose의 Side-effects
Compose는 기본적으로 side-effect가 없어야합니다. 하지만 앱의 상태를 변경해야 하는 경우, composable의 lifecycle을 인식하고 제어할 수 있는 환경에서 이를 발생시켜야합니다. 이 글에서는 Jetpack Compose의 다양한 side-effect API에 대해 알아보겠습니다.
State와 effect의 사용 사례(use case)
Thinking in Compose 설명서에 나와 있는 것처럼 composable은 side-effect가 없어야 합니다. Managing state 설명서에 설명된 대로 앱의 상태를 변경해야 할 경우 side-effect API를 사용하여 이런 side-effect를 예측 가능한 방법으로 실행해야 합니다.
✋ effect는 UI를 방출하지 않고 composition이 완료되면 side-effect가 발생하는 composable 함수입니다.
Compose에서 나타나는 여러 가지 effect로 인해 데이터가 쉽게 과한 방식으로 사용될 수 있습니다. Managing state 설명서에 설명된 대로 UI와 관련된 작업이므로 단방향 데이터 흐름이 끊기지 않도록 해야합니다.
✅ 참고 : 응답성이 뛰어난 UI는 본질적으로 비동기적이며, Jetpack Compose는 콜백을 사용하는 대신 API 수준에서 코루틴을 응용함으로써 이를 해결합니다. 코루틴에 대해 자세히 알아보려면 Android 가이드의 Kotlin couroutine을 확인해보세요.
LaunchedEffect : composable 스코프에서 suspend 함수 실행
composable 내부에서 안전하게 suspend
함수를 호출하려면 LaunchedEffect
composable을 사용하면 됩니다. LaunchedEffect
로 Composition을 시작하게 되면, 코드 블록이 매개변수로 전달된 코루틴을 실행합니다. LaunchedEffect
가 composition을 떠나면 코루틴이 취소됩니다. 또 LaunchedEffect
가 다른 키로 recompose되는 경우(아래의 Restarting Effects 문단 참고) 기존 코루틴이 취소되고 새로운 suspend
함수가 새로운 코루틴에서 시작됩니다.
위 코드에서 코루틴은 상태가 오류가 포함되어 있으면 트리거되고 그렇지 않으면 취소됩니다. LaunchedEffect
의 call site가 if
문 안에 있으므로 if
문이 false일 때 LaunchedEffect
가 Composition에 있으면 해당 항목이 제거되고, 따라서 코루틴이 취소됩니다.
rememberCoroutineScope — composable 외부에서 코루틴을 시작하기 위한 composition-aware 스코프를 획득합니다.
LaunchedEffect
는 composable 함수이므로, 다른 composable 함수 내부에서만 사용할 수 있습니다. composable 외부에서 코루틴을 시작하되 composition을 떠났을 때 자동으로 취소되도록 하려면 rememberCoroutineScope
를 사용합니다. 또한 사용자 이벤트가 발생할 때 애니메이션을 취소하는 등 하나 이상의 코루틴 lifecycle을 수동으로 제어해야할 때마다 rememberCoroutineScope
를 사용할 수 있습니다.
rememberCoroutineScope
는 호출한 Composition의 CoroutineScope
에 바인딩된 코루틴 스코프를 반환하는 composable 함수입니다. 호출 할 때 Composition을 떠나게 되면 스코프가 취소됩니다.
위의 예시에 이어 사용자가 Button
을 누를 때 아래 코드를 사용하여 Snackbar
를 표시할 수 있습니다.
rememberUpdatedState — 값이 변경된 경우 재시작되지 않아야 하는 effect의 값을 참조합니다.
키 파라미터 중 하나가 변경되면 LaunchedEffect
가 다시 시작됩니다. 그러나 경우에 따라 키가 변경될 때 effect를 다시 시작하지 않는 값을 캡쳐해야할 수 도 있습니다. 이런 방식을 사용하려면 rememberUpdatedState
를 사용하여 캡쳐 및 업데이트 할 수 있는 값에 대한 참조를 생성해야합니다. 이 방법은 재생성 및 재시작에 비용이 많이 들거나 비용이 많이 들 가능성이 있는 긴 시간이 걸리는 연산을 포함하는 effect에 유용합니다.
예를 들어서 앱에 시간이 지나면 사라지는 LandingScreen
이 있다고 가정해봅시다. LandingScreen
이 recompose되는 경우에도 일정 시간동안 기다렸다가 경과된 시간에 의해 재시작되지 않아야함을 알리는 아래 effect 예시를 살펴봅시다.
call site의 lifecycle과 일치하는 effect를 생성하기 위해 Unit
또는 true
와 같은 값이 변하지 않는 상수가 매개변수로 전달됩니다. 위의 코드에서 LaunchedEffect(true)
가 그 예시입니다. onTimeout
람다에 항상 LandingScreen
이 recompose된 최신 값이 포함되도록 하려면 rememberUpdateState
함수로 onTimeout
을 래핑해야합니다. 반환된 State
, 즉 코드의 currentOnTimeout
이 effect에서 사용되어야하기 때문입니다.
⚠️ 주의 : LaunchedEffect(true)
가 while(true)
처럼 생각될 수 있습니다. 비록 유효한 사용사례이더라도, 정말 필요한 사항인지 확인하고 사용하세요.
DisposableEffect — 정리가 필요한 effect
키를 변경한 후 정리해야 하는 side-effect나 Composition에서 composable이 떠났을 경우 DisposableEffect
를 사용합니다. DisposableEffect
의 키가 변경되면 composable은 현재 effect를 정리(dispose)하고 effect를 다시 호출하여 리셋해야합니다.
예를 들어, OnBackPressedDispatcher
에서 뒤로가기 버튼을 누르는 것을 트리거하려면 OnBackPressedCallback
을 등록해야합니다. Compose에서 이러한 이벤트를 사용하려면 DisposableEffect
를 사용하여 콜백을 등록하고 필요한 경우 등록을 취소하세요.
위의 코드에서 이 effect는 기억된 backCallback
을 backDispatcher
에 추가합니다. backDispatcher
가 변경되면 effect가 정리되고 다시 시작됩니다.
DisposableEffect
는 코드 블록의 마지막 줄에서 onDispose
절을 포함해야 합니다. 그렇지 않으면 IDE에 빌드 타임 에러가 표시됩니다.
✅ 참고 : onDispose
에 빈 블록이 있는 것은 좋은 방법이 아닙니다. 사용 사례에 따라 더 적합한 effect가 있는지 항상 고려하세요.
SideEffect — Composite 상태를 non-compose 코드에 게시하기
Compose 상태를 compose에서 관리되지 않는 객체와 공유하려면 성공적으로 recomposition될 때마다 호출되는 SideEffect
composable을 사용하면 됩니다.
이전의 BackHandler
코드를 예시로 들어, 콜백의 활성화 여부를 전달하려면 SideEffect
를 사용하여 값을 업데이트하면 됩니다.
produceState — non-Compose 상태를 Compose 상태로 변환합니다.
produceState
는 반환된 State
로 값을 밀어넣을 수 있는 Composition의 코루틴 스코프를 시작합니다. 예를 들어서 Flow
, LiveData
또는 RxJava
와 같은 외부 구독-기반 상태를 Composition으로 가져와 사용하는 경우 이를 사용하여 non-Compose 상태를 Compose 상태로 변환할 수 있습니다.
produceState
는 Composition에 들어갈 때 시작되며 Composition을 떠나면 취소됩니다. 반환된 State
는 동일한 값을 설정할 경우 recomposition이 트리거되지 않습니다.
produceState
는 코루틴을 생성하지만 non-suspending한 데이터 소스를 관찰하는데도 사용될 수 있습니다. 소스에 대한 구독을 제거하려면 awaitDispose
함수를 사용하세요.
아래 예시는 네트워크에서 이미지를 읽어올 때 produceState
를 사용하는 방법을 보여줍니다. loadNetworkImage
composable 함수는 다른 composable에서 사용할 수 있는 State
를 리턴합니다.
✅ 참고: 반환 타입이 있는 composable은 소문자로 시작하는 일반적인 Kotlin 함수의 이름을 지정하는 방법으로 지정해야합니다.
🤔 키 포인트 : produceState
는 내부에서 다른 effect를 사용합니다! remember { mutableStateOf(initalValue) }
를 사용하여 result
를 유지하고, LaunchedEffect
에서 producer 블록을 트리거합니다. producer 블록에서 값이 업데이트 될 때마다 result
가 새로운 값으로 업데이트됩니다.
이처럼 기존 API를 기반으로 자신만의 effect를 쉽게 만들 수 있습니다.
derivedStateOf — 하나 이상의 객체를 다른 상태로 변환합니다.
특정 상태가 계산되거나 다른 상태 객체에서 파생된 경우 derivedStateOf
를 사용할 수 있습니다. 이 함수를 사용하면 계산에 사용되는 상태 중 하나가 변경될 때 계산이 수행됩니다.
아래 예시는 사용자가 정의한 높은 우선순위 키워드를 가진 태스크가 먼저 나타나는 TODO 리스트를 보여줍니다.
위의 코드에서 derivedStateOf
는 todoTasks
또는 highPriorityKeywords
가 변경될 때마다 highPriorityTasks
작업에 대한 계산이 발생하고 이에 따라 UI가 업데이트 되도록 보장합니다. highPriorityTasks
작업을 계산하는 필터링은 비용이 많이 들 수 있으므로 모든 recomposition이 아닌 리스트 중 하나가 변경될 때만 합니다.
또한 derivedStateOf
에 의해 생성된 상태에 대한 업데이트로 인해 선언된 composable에서 recompose가 발생하진 않으며, Compose는 예시의 LazyColumn
내부에서 반환된 상태를 읽는 부분에서만 composable을 다시 recompose 합니다.
snapshotFlow — Compose의 State를 Flow로 변환
snapshotFlow
를 사용하여 State<T>
객체를 cold Flow에 변환할 수 있습니다. snapshotFlow
는 수집을 시작하면 파라미터 블록을 실행하고 실행된 State
객체의 결과를 방출합니다. snapshotFlow
블록 내부에서 읽은 State
객체 중 하나가 변경되어 새로운 값이 이전에 방출된 값과 동일하지 않으면 Flow가 새로운 값을 collector(수집하는 곳)로 방출할 수도 있습니다. (이 동작은 Flow.distinctUntilChanged
와 유사합니다)
아래 예시에서는 사용자가 분석이 필요한 리스트의 첫 번째 아이템을 스크롤할 때 기록하는 side-effect를 보여줍니다.
위의 코드에서 listState.firstVisibleItemIndex
는 Flow 연산자를 사용할 수 있는 Flow로 변환됩니다.
effect 재시작하기
LaunchedEffect
, produceState
또는 DisposableEffect
와 같은 Compose의 일부 effect는 실행중인 effect를 취소하고 새로운 키로 새로운 effect을 시작할 때 변수의 숫자를 가진 인자, 키를 활용합니다.
이러한 API의 일반적인 모습은 다음과 같습니다.
EffectName(restartIfThisKeyChanges, orThisKey, orThisKey, ...){
block
}
이 동작의 세부 사항으로 인하여 effect를 다시 시작하는데 사용된 매개 변수가 올바른 매개 변수가 아닌 경우 문제가 발생할 수 있습니다.
- 원래보다 적은 매개변수로 effect를 재시작하면 프로그램에 버그가 발생할 수 있습니다.
- effect를 다시 시작하는 것은 비효율적일 수 있습니다.
원칙적으로 코드의 effect 블록에 사용되는 변경 가능한 변수와 불변 변수는 effect composable의 매개 변수로 추가되어야 합니다. 이 외에도 더 많은 매개 변수를 추가하여 effect를 강제로 재시작할 수 있습니다. 변수를 변경해도 effect가 재시작되지 않으면 변수를 rememeberUpdatedState
로 감싸야 합니다. 키가 없는 remember
로 감싸져서 변수가 변경되지 않는 경우 변수를 effect의 키로 전달할 필요가 없습니다.
요점 : effect에 사용할 변수는 effect composable의 매개 변수로 사용하거나 rememberUpdatedState
를 사용하세요.
아래 DisposableEffect
코드에서 effect는 backDispatcher
를 블록에서 사용하는 매개 변수로 활용하며 변경이 발생할 때 effect가 재시작됩니다.
backCallback
은 Composition에서는 값이 변경되지 않으므로 DisposableEffect
의 키로 필요하지 않습니다. 이는 backCallback
이 키가 없는 remember
로 사용되기 때문입니다. backDispatcher
가 매개 변수로 전달되지 않고 변경되면 BackHandler
는 recompose되지만 DisposableEffect
는 정리되지 않고 다시 시작합니다. 이 시점부터 잘못된 backDispatcher
가 사용되기 때문에 문제가 발생합니다.
상수를 키로 사용
true
와 같은 상수를 effect의 키로 사용하여 call site의 lifecycle을 따라가도록 할 수 있습니다. 위에 표시된 LaunchedEffect
예시처럼 유효한 사용 사례가 있을 수 있습니다. 하지만 이렇게 하기 전에, 두 번 생각해보고 이것이 과연 정말 우리에게 필요한 것인지 확인해보세요.
읽어주셔서 감사합니다 🙌
Part2 . Design— 1부 : Layout으로 이어서 작성하겠습니다.
- 이미지 및 본문 참고 링크 : https://developer.android.com/jetpack/compose/side-effects