본문 바로가기

Android/Jetpack Compose

[Jetpack] Compose 사용하기 - 2. Side Effect와 Coroutine 2

728x90

본 게시글은 이전 글에 이어서 작성된 부분입니다.

2022.06.07 - [Android/Jetpack] - [Jetpack] Compose 사용하기 - 1. remember와 MutableState

2022.06.10 - [Android/Jetpack] - [Jetpack] Compose 사용하기 - 2. Side Effect와 Coroutine 1

 

이전 게시글을 작성하다 보니 상당히 양이 많아지고 있어서 분리해서 작성하도록 하였다.

 

Side Effect와 Coroutine 1 편에 이어서 작성된 글이니 1 ~ 3번째 항목에 대하여 확인이 필요한 경우 위의 이전 게시글을 확인하길 바란다.


네 번째 항목은

DisposableEffect이다.

 

이것은 LaunchedEffect와 동일한 역할을 하나, 재구현이나 종료 시.

즉, 생명주기가 끝나는 시점에 onDispose라는 함수가 반드시 호출된다.

 

DisposableEffect(isGo) {
    // 작업
    
    onDispose { 
        // Resource 해제 등 해당 Scope가 끝날 때 수행되는 작업
    }
}

 

이와 같이 선언하여 사용할 수 있으며,

onDispose 블록이 없는 경우 오류가 발생하기 때문에, 해당 블록을 깜빡하고 추가하지 못하여 문제가 발생하는 경우는 없다.

 

Composable이 종료될 때 리소스의 해제와 같이 호출되어야 하는 작업이 있을 경우 LaunchedEffect가 아닌 DisposableEffect를 사용하면 될 것이다.

 

다섯 번째 항목은

SideEffect이다.

 

SideEffect는 설명을 보면 간단하게 이해가 가능하다.

SideEffect Composable은 Recomposition 성공 시마다 호출된다.

 

즉, 최초에 구성, 재구성 등 Composable이 성공적으로 완료되면 해당 블록이 호출되는 것이다.

 

예로 들어, TextField에 SideEffect 블록을 넣는다면 최초에 생성됐을 때, 텍스트를 변경할 때마다 SideEffect 블록이 수행되게 된다.

 

SideEffect {
    ...
}

 

별도의 사용 방법이 있는 게 아닌, 단순히 SideEffect 블록을 선언하고 이하에 재구성 시마다 호출할 작업을 작성해주면 된다.

 

SideEffect는 재구성 시 마다 호출되기 때문에, 재구성이 빈번히 일어나는 곳에서 SideEffect를 사용하게 된다면 반드시 필요한 부분인지 확인하고 적용을 하는 것이 좋다.

DisposableEffect로 대체할 수 있는 경우도 있으니 말이다.

 

여섯 번째 항목은

produceState이다.

비 Compose 상태를  Composable 상태로 변환할 때 사용하는 코루틴이다.

 

이게 무슨 소리인가 싶겠지만, 우선 produceState를 확인하고 설명하도록 하겠다.

 

 

초기 값과, key값, 그리고 람다 형태의 producer를 인자로 받고 State<T> 타입을 반환한다.

함수 내부는 초기 값을 remember 키워드를 사용하여 저장하고, LaunchedEffect를 통해 key값이 변화할 때마다 람다 형태의 producer를 호출하게 된다.

 

하나씩 확인해 보도록 하자.

 

1. initialValue는 어떠한 자료형도 들어갈 수 있으며, mutableStateOf를 통해 값이 저장된다.

즉, 해당 값의 변경을 위해서는 initialValue.value를 통해 변경이 이루어져야 한다.

 

2. key값을 받아와서 LaunchedEffect로 사용한다는 것은, 해당 key값의 개수는 LaunchedEffect와 동일하게 제한이 없다는 것이다. 사용되는 key 값들이 변경이 되면 이하의 producer를 호출하게 된다.

 

3. produceState는 State<T> 타입을 반환하게 되는데, State<T>는 Compose에서 사용할 수 있는 상태라고 생각하면 된다.

 

여기까지 확인하고 다시 produceState의 설명을 보면 얼추 이해가 될 수 있을 것이다.

Flow, LiveData, RxJava 등 Composable 외부에서 사용되는 것들을 produceState 블록 내부에서 사용하고, 그 값을 사용하여 연산 후, Composable에서 사용할 수 있는 형태인 State<T>로 Return 하여 필요한 데이터를 Composable에서 사용할 수 있도록 해준다.

 

이것 외에도 produceState에서 사용되는 함수가 있는데, 그것은 awaitDispose이다.

 

val timer by produceState(initialValue = 0, isTimer) {
    ...

    // 종료 시 호출
    awaitDispose {
        ...
    }
}

 

해당 부분은 필자가 작성한 샘플 예제의 일부이다.

 

주석으로 설명을 적어놓은 것처럼, awaitDispose 블록은 실행된 produceState가 종료될 때 호출되는 것으로, DisposableEffect의 onDispose와 비슷한 역할을 한다고 생각하면 된다.

 

새로운 produceState를 생성할 때, 수행해야 하는 작업들이 있으면 awaitDispose에서 처리를 해주면 된다.

 

 

일곱 번째 항목은 

derivedStateOf이다.

 

derivedStateOf는 remember 블록 안에서 사용하며, 계산에서 사용되는 상태 중 하나가 변경될 때만 계산이 실행된다고 한다.

이 말이 무엇인지 우선 안드로이드 공식 페이지에 나와있는 내용을 확인해보도록 하자.

 

@Composable
fun TodoList(highPriorityKeywords: List<String> = listOf("Review", "Unblock", "Compose")) {

    val todoTasks = remember { mutableStateListOf<String>() }

    // Calculate high priority tasks only when the todoTasks or highPriorityKeywords
    // change, not on every recomposition
    val highPriorityTasks by remember(highPriorityKeywords) {
        derivedStateOf { todoTasks.filter { it.containsWord(highPriorityKeywords) } }
    }

    Box(Modifier.fillMaxSize()) {
        LazyColumn {
            items(highPriorityTasks) { /* ... */ }
            items(todoTasks) { /* ... */ }
        }
        /* Rest of the UI where users can add elements to the list */
    }
}

 

위 코드에서 derivedStateOf는 todoTasks가 변경될 때마다 highPriorityTasks 계산이 실행되고 그에 따라 UI가 업데이트되도록 보장합니다. 
highPriorityKeywords가 변경되면 remember 블록이 실행되고 이전 파생 상태 객체 대신 새로운 파생 상태 객체가 생성되고 기억됩니다. 
highPriorityTasks를 계산하기 위한 필터링은 비용이 많이 들 수 있으므로 매 리컴포지션 시가 아니라 목록이 변경될 때만 실행해야 합니다.

 

derivedStateOf는 todoTasks가 변경될 때 실행되고, highPriorityKeywords가 변경되면 새로운 derivedStateOf가 생성된다고 한다.

 

우선, derivedStateOf는 블록의 마지막 라인을 State<T> 타입으로 return 한다.

그렇기 때문에 위의 예제에서는 todoTasks가 변경되면 계산에서 사용되는 상태가 변경된 것 이기 때문에 derivedStateOf가 실행되게 된다.

 

val showButton by remember {
    derivedStateOf {
        scrollState.firstVisibleItemIndex > 0
    }
}

 

만약 마지막 라인이 다음과 같이 되어있다고 가정해보면, 

scrollState.firstVisibleItemIndex가 0 초과거나, 0 이하가 되는 시점 2가지 경우만 상태가 변경된다.

이처럼, 계산에서 해당 변수로 받아오는 값 자체가 변경되는 경우 derivedStateOf 가 실행된다고 생각하면 된다.

 

highPriorityKeywords가 변경되면 새로운 derivedStateOf가 생성된다 라는 것은, remember 키워드의 특성이다.

remember(key) { ... } 형태로 선언하는 경우, key값이 변경될 때 이하의 블록이 실행되기 때문이다.

 

위의 예제와 같이 사용하는 경우, highPriorityKeywords는 todoTasks보다 적게 변경된다 라는 조건이 있다는 가정하에 작성된 코드이다.

두 가지 값이 변경됐을 때 호출되게 되는데, derivedStateOf에 의하여 상태가 업데이트되는 경우에는 재구성이 되지 않기 때문이다. 반대로 highPriorityKeywords가 변경되면 새로운 derivedStateOf가 생성되면서 재구성이 일어나기 때문이다.

 

마지막으로,

snapshotFlow이다.

 

snapshotFlow는 State<T> 객체를 Flow로 반환시켜주기 때문에, State<T> 객체를 Flow로 사용해야 할 때 쓰면 된다.

snapshotFlow를 사용하여 위의 derivedStateOf의 예시와 동일한 동작을 하도록 구현을 하면 다음과 같다.

 

var flowShowButton by remember { mutableStateOf(false) }
LaunchedEffect(scrollState) {
    snapshotFlow { scrollState.firstVisibleItemIndex }
        .map { index -> index > 0 }
        .distinctUntilChanged()
        .collect {
            flowShowButton = it
        }
}

 

distinctUntilChanged() 새로운 값이 이전의 값과 차이가 있을 경우에만 감지할 수 있도록 해주는 역할이다.

즉, scrollState.firstVisibleItemIndex 값을 비교하여 0 초과나 0 이하로 바뀔 때 collect로 수집하여 flowShowButton 값을 변경해주게 된다.

 

여기서 알아둬야 할 점은, LaunchedEffect를 사용한 부분이다.

Flow에서 사용되는 collect는 코루틴 상에서 동작해야하기 때문에, LaunchedEffect를 통해 코루틴 상에서 동작할 수 있도록 감싸주었다는 점이다.


이것으로 안드로이드 공식 페이지에서 설명하는 Effect API에 대하여 모두 확인해 보았다.

생각보다 이해하기가 쉽지 않은 것들도 있었고, 헷갈리는 부분도 많이 있었다.

 

아직까지는 적합한 상황에 맞춰서 Effect API를 사용하는 것이 쉽지 않을 것으로 생각되기 때문에, 

Compose를 많이 사용해보고, 필요할 때마다 조금 더 생각하고 비교하면서 사용해야 할 것 같다.

 

코루틴을 사용하기 위하여 Effect API에 대해 알아보았는데,

Compose를 더 많이 사용해 보면서 추가적으로 정리가 필요한 것이 있다면 추가적으로 글을 작성해볼 예정이다.

 

작성된 Effect API의 종류를 모두 사용한 예제를 만들어 두었으며,

해당 게시글에 사용한 예제는 Github에 올려두었다.

https://github.com/HeeGyeong/ComposeSample

 

GitHub - HeeGyeong/ComposeSample

Contribute to HeeGyeong/ComposeSample development by creating an account on GitHub.

github.com

 

 

+ 24.05.06 추가.

해당 API들을 실제로 사용하면서 재 정리하여 글을 작성하였습니다.

2024.05.06 - [Android/Jetpack Compose] - [Compose] Side Effect 관련 API 재 정리

728x90