본문 바로가기

Jetpack Compose

[Jetpack Compose Side-effects] 6.SideEffect / produceState

SideEffect

SideEffect는 Composable에서 recomposition이 일어날 때마다 실행됩니다. Compose의 State를 Compose에서 관리하지 않는 객체와 공유할 때 사용합니다. 구글 공식 문서에서는 사용자 유형을 애널리틱스 라이브러리에 전달할 때, 사용자 유형의 값을 업데이트하기 위해서 SideEffect를 사용하는 예제를 보여주고 있습니다.

 

@Composable
fun rememberAnalytics(user: User): FirebaseAnalytics {
    val analytics: FirebaseAnalytics = remember {
        /* ... */
    }

    // 리컴포지션이 성공적으로 일어날 때마다 실행됩니다
    SideEffect {
        analytics.setUserProperty("userType", user.userType)
    }
    return analytics
}

 

LaunchedEffect와는 다르게 key를 설정할 수 없고, 스코프가 coroutineScope이 아니기 때문에 내부에서 suspend fun을 사용할 수 없습니다. recomposition이 일어날 때마다 재실행되기 때문에 recomposition이 많이 일어나는 곳에서는 사용을 조심해야하고 리컴포지션이 일어날 때마다 실행되어야 하는 로직에 한해서 사용해야 할 것 같습니다. 대부분의 상황에서는 LaunchedEffect로 SideEffect를 대체할 수 있다고 생각합니다.

produceState

produceState는 Compose State가 아닌 것을 Compose State로 바꾸어 주는 Side-effects 입니다. produceState 를 많이 사용하지 않아서 낯설게 느껴질 수 있지만 사실 우리는 내부적으로 produceState를 굉장히 많이 사용하고 있습니다. 

 

흔히 ViewModel에서 화면에 필요한 정보를 Flow 형태로 emit 합니다. 그리고 이러한 정보를 화면에서 표시하기 위해서 Compose에서는 Screen 레벨에서 collectAsStateWithLifecycle() 메서드를 사용해 Flow를 collect 합니다. 신기하게도 collectAsStateWithLifecycle() 메서드를 사용하면 Compose의 State가 아니었던 Flow가 Compose의 State로 바뀌게 되는데 collectAsStateWithLifecycle() 메서드의 내부 구현을 살펴보면 produceState를 활용했음을 알 수 있습니다.

 

@Composable
fun <T> Flow<T>.collectAsStateWithLifecycle(
    initialValue: T,
    lifecycle: Lifecycle,
    minActiveState: Lifecycle.State = Lifecycle.State.STARTED,
    context: CoroutineContext = EmptyCoroutineContext
): State<T> {
    return produceState(initialValue, this, lifecycle, minActiveState, context) {
        lifecycle.repeatOnLifecycle(minActiveState) {
            if (context == EmptyCoroutineContext) {
                this@collectAsStateWithLifecycle.collect { this@produceState.value = it }
            } else withContext(context) {
                this@collectAsStateWithLifecycle.collect { this@produceState.value = it }
            }
        }
    }
}

 

 

이처럼 produceState는 Compose State가 아닌 것을 Compose State로 바꾸어주는 역할을 합니다. 그렇다면 produceState를 어떤 식으로 활용할 수 있을까요? produceState에 대해 자세히 알아봅시다.

 

특정 값을 반환하는 Composable은 Composable 함수의 이름이 소문자로 시작합니다. produceState 역시 소문자로 시작하기 때문에 특정 값을 반환하는 것을 알 수 있습니다. 또한 produceState의 역할이 Composable State로 변환시켜 준다는 것을 되짚어보면 produceState는 State 타입을 반환하지 않을까 추측해볼 수 있습니다. 이러한 추측을 가지고 내부 구현을 살펴보겠습니다.

 

@Composable
fun <T> produceState(
    initialValue: T,
    producer: suspend ProduceStateScope<T>.() -> Unit
): State<T> {
    val result = remember { mutableStateOf(initialValue) }
    LaunchedEffect(Unit) {
        ProduceStateScopeImpl(result, coroutineContext).producer()
    }
    return result
}

 

예상한 것과 같이 State 타입 반환함을 알 수 있습니다. 그리고 반가운 LaunchedEffect도 보입니다. LaunchedEffect가 있기 때문에 produceState 역시 key를 설정할 수 있고 key의 변화에 따라 produceState 스코프가 재실행됩니다. 또한 LaunchedEffect와 마찬가지로 produceState도 coroutineScope에서 실행되기 때문에 내부에서 suspend fun 을 사용할 수 있습니다.

 

produceState는 매개변수로 initialValue를 가지는데 반환하는 State 타입의 초기 값에 해당합니다. 그리고 key 값의 변화에 따라 produceState 스코프가 재실행 되면서 새로운 State가 반환되는 원리입니다.

 

다음과 같이 UiState를 만들고 api를 load하거나 데이터가 변경되어 ViewModel에서 UiState를 수정하는 로직이 있는 경우, Screen 에서 produceState를 활용할 수 있습니다.

data class ScreenUiState(
    val isLoading: Boolean = false,
    val content: Bitmap? = null
)

 

@Composable
fun HomeScreen(
    homeViewModel: HomeViewModel = viewModel()
) {
    val bitmapUiState by produceState(initialValue = ScreenUiState(isLoading = true)) {
        value = homeViewModel.createQRCode()
    }
           /* TODO something */
}

 

위의 경우 produceState의 key를 따로 설정해놓지 않았기 때문에 최초에 1번 실행되고 이후에는 재실행되지 않습니다. 만약 어떤 버튼을 눌렀을 때 api를 재호출해야 하는 경우가 생긴다면, 버튼을 눌렀을 때 특정 State를 변하게 하고 해당 State를 produceState의 key로 설정한다면 produceState가 재호출되면서 api를 재호출하게 될 것 입니다.

 

만약 UiState를 하나의 data class로 정의할 수 있고 여러 trigger를 통해 동일한 로직이 수행되어야 하는 경우에 produceState를 사용하면 기존에는 Screen 화면에서 collectAsStateWithLifecycle()을 통해 collect 한 뒤, LaunchedEffect와 같은 함수를 통해 UiState 분기 처리하던 코드를 한 곳에 모아서 해결할 수 있어 코드가 간결해진다는 장점이 있다고 생각합니다.

 

추후 코드를 개선하거나 새롭게 짤 때 적용해보면 좋을 듯 합니다.