Jetpack Compose Doc 읽기 — Part1[기초]

hongbeom
hongbeomi dev
Published in
12 min readJun 22, 2021

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는 기억된 backCallbackbackDispatcher에 추가합니다. 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 리스트를 보여줍니다.

위의 코드에서 derivedStateOftodoTasks 또는 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으로 이어서 작성하겠습니다.

--

--