컴포즈 공식 가이드 읽고 분석하기 — (5)

김종식
11 min readMar 12, 2022

--

(목차로 돌아가기)

Side-effects in Compose

컴포저블은 부가 작용(side-effect)으로부터 자유롭도록 해야합니다. (번역문서) 하지만, 앱의 상태 변경이 필요할 경우, 컴포저블의 생명주기를 알고 있는 제어된 환경에서 호출해야 합니다. 이 장에서는, 젯팩 컴포즈가 제공하는 부가 작용을 위한 API 를 배울 수 있습니다.

핵심 용어 : 부가 작용(side-effect) 은 컴포저블 함수 외부에서 일어나는 앱의 상태 변화입니다.

State and effect use cases

Thinking in Compose (한글 번역) 문서에서 확인한 것 처럼, 컴포저블은 부가 작용으로 부터 자유로워야 합니다. 앱의 상태를 변경해야 할 경우 (Managing state한글 번역 — 문서에서 설명한 대로), Effect API 를 사용하여 부가 작용이 예측 가능하게 실행 될 수 있어야 합니다.

핵심 용어 : 이펙트(effect) 는 UI 를 방출하지 않고 구성이 완료될 때 부가 작용이 발생하는 컴포저블의 기능입니다.

컴포즈에서는 이펙트가 여러가지 가능성이 열려있기 때문에, 쉽게 남용될 수 있습니다. 당신이 하려는 작업이 UI와 관련이 있고 Managing state 문서에서 설명한 단방향 데이터 흐름을 끊지 않는지 확인해야 합니다.

참조 : 응답형 UI 는 본질적으로 비동기적이며, 젯팩 컴포즈는 이를 콜백 대신 API 레벨에서 코루틴을 수용하여 이 문제를 해결하고 있습니다. Coroutine 에 대해 더 자세히 알아보려면, Kotlin coroutines on Android 가이드를 확인하세요.

LaunchedEffect: run suspend functions in the scope of a composable

컴포저블 내에서 안전하게 중단함수를 호출하려면, LaunchedEffect컴포저블을 사용할 수 있습니다. 구성에 LaunchedEffect 가 들어가면, 인자로 전달된 코드블록으로 코루틴을 실행합니다. 만약 LaunchedEffect 구성에서 떠날 경우 코루틴은 취소됩니다. 만약 LaunchedEffect 가 다른 키로 재구성된다면 (하단의 Restarting Effect 섹션 참조), 존재하던 코루틴은 취소되며 새 코루틴에서 중단 함수가 실행됩니다.

예를 들어, SnackbarHostState.showSnackbar 함수를 이용하여 Scaffold 에서 Snackbar 를 표시하는데, 이는 중단 함수 입니다.

위 코드에서 코루틴은 상태가 오류를 가지고 있으면 트리거가 되고, 그렇지 않다면 취소됩니다. LaunchedEffect 콜 사이트가 조건문 안에 있으므로, 조건문이 거짓일 때 LaunchedEffect 구성에 있다면 제거되므로 코루틴은 취소됩니다.

rememberCoroutineScope: obtain a composition-aware scope to launch a coroutine outside a composable

LaunchedEffect 는 컴포저블 함수이기 때문에, 컴포저블 내부에서만 사용할 수 있습니다. 컴포저블 외부에서 코루틴을 실행하지만 구성 중 자동으로 취소되게 하려면 rememberCoroutineScope 을 사용하세요. 또한 사용자의 이벤트가 발생될 때 애니메이션을 취소하는 등 하나 이상의 코루틴의 수명주기를 제어할 때 마다 rememberCoroutineScope 을 사용하세요.

rememberCoroutineScope 는 중단함수가 호출된 구성과 바인딩 되어있는 CoroutineScope 입니다. 호출이 구성을 떠나면 스코프는 취소되어집니다.

이전 예제에서, 사용자가 버튼을 누를 때 마다 Snackbar 를 표시할 수 있습니다.

rememberUpdatedState: reference a value in an effect that shouldn’t restart if the value changes

LaunchedEffect 는 주요 파라메터중 하나가 변경되면 재시작됩니다. 하지만, 경우에 따라 값이 변경되어도 이펙트가 재시작되지 않도록 포획(capture)할 수 있습니다. 이렇게 하려면, rememberUpdatedState 을 사용해서 포획되거나 업데이트되는 값을 참조하여 생성합니다. 이 방식은 비용이 많이 들거나 재생성 및 재시작이 금지되어야 하는 장기간 살아 동작하는 이펙트에 유용합니다.

예를들어, 앱에서 얼마의 시간 후 사라지는 LandingScreen 을 가진 앱이라고 가정해봅시다. 비록 LandingScreen 이 재구성 되어도, 일정 시간 기다렸다가 시간이 지났음을 알리는 이펙트는 다시 시작되지 않아야 합니다.

콜 사이트의 주기와 일치하는 이펙트를 만들기 위해, Unit 이나 true 같은 절대 변하지 않는 상수값을 파라미터로 전달합니다. 위의 코드에서, LaunchedEffect(true)를 사용했습니다. onTimeout 람다가 항상 LandingScreen 이 재구성 될 때 최신의 값을 포함하려면, onTime 은 rememberUpdatedState 함수로 감싸야 합니다. 반환된 State, 이 코드에서는 currentOnTimeout 가 이펙트에서 사용되어야 합니다.

주의 : LaunchedEffect(true)while(true) 만큼 의심스럽습니다. 비록 유효하게 사용될 수 있더라고, 항상 정말로 필요한 부분이 맞는지 잠깐 멈추고 확인하세요.

DisposableEffect: effects that require cleanup

키가 변경되거나 컴포저블이 구성에서 떠난 후 정리가 필요한 부가 작용을 위해 DisposableEffect 을 사용할 수 있습니다. 만약 DisposableEffect 키가 변경될 경우, 컴포저블은 현재 이펙트를 처분하고(정리를 하고), 이펙트를 다시 호출함으로써 초기화 합니다.

예를들어, LifecycleObserver 을 이용하여 Lifecycle 이벤트를 기반으로 분석 이벤트들을 보내고 싶을 수 있습니다. 컴포즈에서 이러한 이벤트를 청취하기 위해, DisposableEffect 을 이용해서 옵저버를 필요할 때 등록하거나 등록 취소를 할 수 있습니다.

위 코드에서, 이펙트에서 observer를 lifecycleOwner 에 추가합니다. lifecycleOwner 가 변경되면, 이펙트는 정리(disposed)되고 새로운 라이프사이클과 함께 재시작됩니다.

DisposableEffect 는 반드시 코드 마지막 구문으로 onDispose를 포함해야 합니다. 그렇지 않으면, IDE 는 빌드 타임에 에러를 표시합니다.

참조 : onDispose 에 빈 코드블럭을 사용하는 것은 좋은 방법이 아닙니다. 항상 이펙트를 사용하는 사례에서 더 나은 케이스가 있을지 재고해야 합니다.

SideEffect: publish Compose state to non-compose code

컴포즈로부터 관리되어지지 않는 객체와 컴포즈의 상태를 공유하려면, SideEffect 컴포저블을 사용하세요.

예를들어, 분석 라이브러리를 (컴포즈에서) 사용하면 별도 정의한 메타데이터(여기에서는 “사용자 속성”) 접근함으로써 다음부터 모든 분석 이벤트에 접근하게 됩니다. 현재 유저의 유저타입을 분석 라이브러리에 전달하려면, SideEffect 를 사용해서 값을 업데이트 하세요.

produceState: convert non-Compose state into Compose state

produceState 는 이미 반횐된 상태에 값을 밀어넣기 위해 구성에서 이미 범위가 지정된(scoped) 코루틴을 실행합니다. 예를들어 Flow, LiveData 또는 RxJava 와 같은 외부의 구독 기반를 컴포즈 내 상태로 변환하는데 사용합니다.

생산자는 produceState가 구성이 시작되면 때 실행되며, 구성에서 빠질 경우 취소되어 집니다. 반환된 상태는 결합됩니다; 동일한 값이 설정될 경우 재구성이 트리거 되지 않습니다.

비록 produceState 는 코루틴을 생성하지만, 중단되지 않는(non-suspending) 데이터 소스를 구독하는데 사용할 수 있습니다. 해당 소스에 대한 구독을 제거하려면, awaitDispose 함수를 사용하세요.

다음 예제는 네트워크를 통해 이미지를 로드하는데 어떻게 produceState 를 사용하는지 보여줍니다. loadNetworkImage 컴포저블 함수는 다른 컴포저블에서 사용할 수 있는 State 를 반환합니다.

참조 : 리턴 타입이 있는 컴포저블 함수는 소문자로 시작하는 일반 코틀린 함수처럼 명명해야 합니다.

중요 : 자세히 살펴보면, produceState 는 다른 이펙트를 활용합니다! { mutableStateOf(initialValue) } 을 사용해서 result 변수를 가진 채, LaunchedEffectproducer 블록을 트리거합니다. producer 블록에서 값이 변경될 때 마다, result 상태는 새로운 값으로 업데이트 됩니다.

기존의 API 에서도 나만의 이펙트를 쉽게 만들 수 있습니다.

derivedStateOf: convert one or multiple state objects into another state

특정 상태가 계산되거나 다른 상태객체로부터 파생될 때는 derivedStateOf 를 사용합니다. 이 함수는 계산에 사용되는 상태들 중 하나가 변경될 때만 계산이 수행되는것을 보장합니다.

아래 예제는 사용자가 정의한 높은 우선순의의 키워드를 가진 태스크를 표시하는 기본적인 TODO 목록을 보여줍니다.

이 코드에서, derivedStateOf 는 todoTasks 가 변경될 때마다 highPriorityTasks 가 계산되고 이에 따라 UI가 업데이트 되는것을 보장합니다. 만약 highPriorityKeywords 가 변경되면, remember 블록이 실행되고 파생된 새로운 객체가 생성되며 remember 안의 옛 값을 새 값으로 교체합니다. highPriorityTasks 필터링 연산은 큰 비용이 발생할 수 있기 때문에, 모든 재구성 과정에 연산되지 않고 관련 목록 중 하나가 변경될 때만 실행되어야 합니다.

또한 derivedStateOf 로 생산되어 업데이트 하는 경우 컴포즈는 오직 읽기 위해 반환된 상태들을 읽고있는 컴포저블에 의해 재구성되어야 하고, (이 예제에서는 LazyColumn 내부) 그것을 선언한 컴포저블을 재구성하는 원인이 되어서는 안됩니다.

이 코드는 highPriorityKeywords 가 todoTasks 보다 훨씬 덜 변경될 것이라고 가정하고 있습니다. 만약 그렇지 않을경우, derivedStateOf 대신 remember(todoTasks, highPriorityKeywords) 을 사용할 수 있습니다.

snapshotFlow: convert Compose’s State into Flows

snapshotFlow 를 사용하여 State<T> 로 변환된 객체를 콜드 플로우(Cold Flow) 로 보냅니다. snapshotFlow 는 코드블록의 State 객체정보를 읽어 그 방출된 결과를 수집합니다. snapshotFlow 코드 블륵의 State 객체가 변경되면, 새 값이 이전에 방출한 값과 같지 않으면 Flow 에서 수집가에게 새로운 값을 방출합니다. (이 동작은 Flow.distinctUntilChanged 의 동작과 유사합니다.)

이 코드에서 listState.firstVisibleItemIndex 는 Flow의 강력한 기능을 활용할 수 있는 Flow로 변환됩니다.

Restarting effects

LaunchedEffect, produceState, DisposableEffect등 컴포즈의 일부 이펙트는 실행중인 이펙트를 취소하고 새로운 키로 다시 실행하기 위해 다양한 인자나 키를 사용합니다.

이 동작의 미묘한 차이 때문에, 이펙트가 재시작될 때 올바른 파라미터를 사용하지 않을 경우 문제가 발생할 수 있습니다.

  • 재시작되어야 하는 이펙트보다 더 적은 이펙트로 인해 앱에서 버그가 발생할 수 있습니다.
  • 재시작되어야 하는 이펙트보다 더 많은 이펙트를 다시 시작하는 것은 비효율적일 수 있습니다.

경험적으로, 가변 및 불변 변수는 컴포저블의 이펙트 코드블록의 매개변수로 사용되어야 합니다. 이 외에도, 더 많은 매개변수를 추가하여 강제로 이펙트를 재시작할 수 있습니다. 만약 변수의 변경이 이펙트를 재시작하지 않아야 한다면, 변수를 rememberUpdatedState 로 감싸야 합니다. 키가 없이 remember 로 감싸진 변수가 절대 변하지 않는다면, 이펙트의 키로 변수를 전달할 필요가 없습니다.

핵심 : 이펙트에서 사용중인 변수는 이펙트 컴포저블로 추가하거나, rememberUpdatedState 를 사용해야 합니다.

위에서 확인한 DisposableEffect 코드에서, 이 이펙트는 lifecycleOwner 을 코드블록의 매개 변수로 사용하고 이 매개변수가 변경되면 이펙트를 재시작합니다.

currentOnStart 와 currentOnStop 은 구성 중 절때 변하지 않아 rememberUpdatedState 를 사용했기 때문에 DisposableEffect 의 키로 필요하지 않습니다. 만약 lifecycleOwner 가 파라미터로 전달되지 않고 변경되면, HomeScreen 은 재구성되나 DisposableEffect 는 폐기도, 재시작도 되지 않습니다. 그러면 lifecycleOwner 가 해당 시점부터 잘못 사용되기 때문에 문제가 발생할 수 있습니다.

Constants as keys

true와 같은 상수를 이펙트의 키로 사용하여 콜 사이트의 라이프사이클을 따르게 할 수 있습니다. 위에서 확인한 LaunchedEffect 의 예시가 유효한 케이스입니다. 하지만, 그렇게 하기 전 두 번 생각하고 반드시 필요한 것인지 확실히 해야 합니다.

--

--

김종식
김종식

Written by 김종식

앱 개발자 / 꿈은 축구선수 / 쌍둥이 아빠

No responses yet