본문 바로가기

안드로이드 Android

안드로이드 개발 (8) Compose 이해 정리

이번 포스팅부터 Compose에 대해 차근히 파헤쳐 가보겠습니다.

Android Compose 공식 문서를 보면서 정리한 내용들 입니다.

 

기존 안드로이드 UI개발의 단점 

- 일반적으로 뷰를 수동으로 조작하기 때문에 오류 발생률이 커짐

- 데이터를 여러 위치에서 렌더링 할 때 여러뷰 중에 업데이트를 잊어먹는 View가 생기기 쉬움

- 두개의 View 업데이트가 예기치 않는 방식으로 충돌 될 때 잘못된 상태로 UI가 그려질 수 있음

- 업데이트가 필요한 뷰가 많을 수록 소프트웨어 유지관리 복잡성 증가 

 

선언형 프로그래밍 패러다임 

- 프로그래밍 업계 전반적으로 선언형 UI 모델로 전환하기 시작함 

- 인터페이스 빌드 및 업데이트와 관련된 엔지니어링이 크게 간소화 됨 

- 화면 전체를 개념적으로 재생성하고 부분적으로 필요한 부분만 변경하는 방식으로 작동 

(사용자의 실수를 덜어준다.)

- 필요한 부분의 UI를 지능적으로 업데이트를 하기 때문에 불필요한 비용을 감소 할 수 있다.

 

Composable Function

* you can build your user interface by defining a set of composable functions that take in data and emit UI elements.

-Composable 함수는 데이터를 받아서 UI요소로 Emit하는 함수임 

  @Composable
  fun Greeting(name:String){
     Text("hello World")
  }

 

- 사용하려면 함수위에 @Composable 를 달아야함 이는 컴파일러에게 데이터가 UI로 변환하기 위한 함수 임을 알림

- composable 함수는 파라미터로 데이터를 받아서 사용할 수 있다.

- composable 함수는 아무것도 return 하지 않음 

- composable 함수멱등원 이며, Side-effect 가 없음 

* 멱등성(idempotence) 이란?

-연산을 여러 번 적용하더라도 결과가 달라지지 않는 성질을 멱등성(idempotence) 이라고 함. 

  • 함수는 동일한 인수로 여러 번 호출될 때 동일한 방식으로 작동하며, 전역 변수 또는 random() 호출과 같은 다른 값을 사용하지 않습니다.
  • 함수는 속성 또는 전역 변수 수정과 같은 Side-effect 없이 UI를 형성합니다.

 

선언형 패러다임으로 생각 전환하기

(to. xml 사용이 익숙한 안드개발자에게)

- Compose는 비교적 Stateless상태임 

- 명령형 객체지향 UI 제작과 달리 getter 및 setter 메서드를 노출하지 않음 

- 동일한 composable 함수를 다른 인수로 호출하여 UI를 업데이트 할 수 있음

 -> This makes it easy to provide state to architectural patterns such as a ViewModel, as described in the Guide to         app architecture.

     공식문서 앱 아키텍처 가이드에 설명 된대로 ViewModel과 같은 아키텍처 패턴에 상태를 쉽게 제공 할 수 있습니다.

     *Composable은 식별 가능한 데이터가 업데이트될 때마다 현재 애플리케이션 상태를 UI로 변환합니다.*

 

 

Composable 내부 흐름 

- 앱 로직은 최상위의 composable 함수에 데이터를 제공 합니다. 그 이후에 

다른 composable함수를 호출해서 UI를 형성하고 적절한 데이터를

해당 컴포저블 및 계층 구조 아래로 전달합니다. 

(위 사진을 보시면 위에서 Data를 받아, 맨 아래 계층 구조까지 전달하는걸 볼 수 있습니다.)

 

 

사용자가 UI와 상호 작용할 때 UI는 onClick과 같은 이벤트를 발생시킵니다.

이 이벤트를 앱 로직에 전달하여 앱의 상태를 변경해야 합니다.

상태가 변경되면 composable 함수는 새 데이터와 함께 다시 호출됩니다.

이렇게 하면 UI 요소가 다시 그려집니다.

 

이 프로세스를 재구성이라고 합니다.

 

onClick -> 앱 로직에 전달 -> 앱 상태 변경 -> Composable 함수를 새 데이터와 함께 재 호출 

-> UI를 다시 그림 

 

 

재구성

 *기존 명령형 UI 모델은 위젯을 변경하려면 위젯에서 setter를 호출하여 내부 상태를 변경하는 방식*

 

 Compose에서는 새 데이터를 사용하여 composable 함수를 다시 호출

  -> 함수가 재구성 하는 방식 임

     필요한 경우 함수에서 내보낸 위젯이 새 데이터로 다시 그려짐

     Compose 프레임워크는 변경된 구성요소만 지능적으로 재구성할 수 있습니다.

 

예제

 

@Composable
fun ClickCounter(clicks: Int, onClick: () -> Unit) {
    Button(onClick = onClick) {
        Text("I've been clicked $clicks times")
    }
}

버튼이 클릭될 때마다 호출자는 clicks 값을 업데이트합니다.

 

Compose는 Text 함수를 사용해 람다를 다시 호출하여 새 값을 표시합니다.

이 프로세스를 재구성이라고 합니다.

값에 종속되지 않은 다른 함수는 재구성되지 않습니다.

 

- 전체 UI 트리를 재구성하는 작업은 컴퓨팅 성능 및 배터리 수명을 사용하기 때문에 컴퓨팅 비용이 많이듬

- Compose는 이 지능적 재구성을 통해 위 비용 문제를 해결

- 재구성은 입력이 변경될 때 구성 가능한 함수를 다시 호출하는 프로세스

- 이는 함수의 입력이 변경될 때 발생

- Compose는 새 입력을 기반으로 재구성할 때 변경되었을 수 있는 함수 또는 람다만 호출하고 나머지는 건너뜀

  (매개변수가 변경되지 요소들을 건너뜀으로써 Compose의 재구성이 효율적으로 이루어질 수 있습니다.)

 

 

Side-effect 

함수의 재구성을 건너뛸 수 있으므로 Composable 함수 실행의 Side-effect에 의존해서는 안 됩니다.

그렇게 하면 사용자가 앱에서 이상하고 예측할 수 없는 동작을 경험할 수 있습니다.

Side-effect 은 앱의 나머지 부분에 표시되는 변경사항입니다.

 

(아래 블로그에서 다른 안드로이드 개발자의 정리를 확인해보실 수 있습니다.

아래 링크에서 Side-effect란 함수의 범위를 벗어난 모든 것이라고 정의 합니다.)

https://jorgecastillo.dev/jetpack-compose-effect-handlers

 

Jetpack Compose Effect Handlers

Learn how to run your side effects 🌀 bound to the @Composable lifecycle. What is a side effect? 🌀 Any Android applications contain side effects. They are also called “effects” quite often, in case you’ve been wondering. A side effect is essenti

jorgecastillo.dev

 

예를 들어 다음 작업은 모두 위험한 Side-effect입니다.

  • 공유 객체의 속성에 쓰기
  • ViewModel에서 식별 가능한 요소 업데이트
  • 공유 환경설정 업데이트

Composable 함수는 애니메이션이 렌더링 될 때와 같이 모든 프레임에서와 같은 빈도로 재실행될 수 있습니다.

애니메이션 도중 버벅거림을 방지하려면 composable 함수가 빨라야합니다.

 

그렇기 때문에 공유 환결설정에서 읽기와 같이 비용이 많이 드는 작업을 실행할 때 

백그라운드 코루틴에서 작업을 실행하고 값 결과를

composable 함수 매개변수로 전달 식으로 사용해야합니다.

 

example

@Composable
fun SharedPrefsToggle(
    text: String,
    value: Boolean,
    onValueChanged: (Boolean) -> Unit
) {
    Row {
        Text(text)
        Checkbox(checked = value, onCheckedChange = onValueChanged)
    }
}

 

위 예제는 Composable 를 생성해서  SharedPreferences의 값을 업데이트 하는 상황입니다. 

컴포저블은 공유 환경설정 자체에서 읽거나 쓰지 않아야 합니다.

 

그래서 이 코드는 백그라운드 코루틴의 ViewModel로 읽기 및 쓰기를 이동시킵니다.

앱 로직은 콜백과 함께 현재 값을 전달하여 업데이트를 트리거합니다.

 

Composable 를 프로그래밍 할 때 알아야할 사항

1) Composable 함수는 순서와 관계없이 실행 가능

2) Composable 함수는 동시에 실행 가능 

3) 재구성은 최대한 많은 수의 Composable 함수 및 람다를 건너뜀

4) 재구성은 낙관적이며 취소될 수 있음

5) Composable 함수는 애니메이션의 모든 프레임과 같은 빈도로 매우 자주 실행될 수 있음

 

 

위의 5가지를 다시 살펴보도록 하겠습니다. 

 

1) Composable 함수는 순서와 관계없이 실행 가능

Composable 함수에 다른 Composable 함수 호출이 포함되어 있다면

그 함수는 순서와 관계없이 실행될 수 있습니다.

 

(대신에, Compose에는 일부 UI 요소가 다른 UI 요소보다 우선순위가 높다는 것을 인식하고

그 요소를 먼저 그리는 옵션도 있다고 합니다.)

 

@Composable
fun ButtonRow() {
    MyFancyNavigation {
        StartScreen()
        MiddleScreen()
        EndScreen()
    }
}

위 예제에 탭 레이아웃에 세 개의 화면을 그리는 코드가 있습니다. 

 

StartScreen, MiddleScreen 및 EndScreen 호출은 순서와 관계없이 발생할 수 있습니다.

즉, 예를 들어 StartScreen()이 일부 전역 변수(Side-effect)를 설정하고 

MiddleScreen()이 이러한 변경사항을 활용하도록 할 수 없음을 의미합니다.

대신 이러한 각 함수는 독립적이어야 합니다.

 

 

2) Composable 함수는 동시에 실행 가능 

Compose는 Composable 함수를 동시에 실행하여 재구성을 최적화할 수 있습니다.

이를 통해 Compose는 화면에 없는 구성 가능한 함수를 낮은 우선순위로 실행할 수 있습니다.

 

이 최적화는 Composable 함수가 백그라운드

스레드 풀 내에서 실행될 수 있음을 의미합니다.

 

Composable 함수 ViewModel에서 함수를 호출하면

Compose는 동시에 여러 스레드에서 이 함수를 호출할 수 있습니다.

 

애플리케이션이 올바르게 작동하도록 하려면 모든 Composable 함수에 Side-effect 이 없어야 합니다.

 

대신 UI 스레드에서 항상 실행되는 onClick과 같은 콜백에서 Side-effect를 트리거합니다.

Composable 함수가 호출될 때 호출자와 다른 스레드에서 호출이 발생할 수 있습니다.

즉, Composable 람다의 변수를 수정하는 코드는 피해야 합니다.

 

이러한 코드는 스레드로부터 안전하지 않을 뿐만 아니라 Composable 람다의

허용되지 않는 Side-effect 이기 때문입니다.

 

다음은 목록과 개수를 표시하는 컴포저블을 보여주는 예입니다.

 

@Composable
fun ListComposable(myList: List<String>) {
    Row(horizontalArrangement = Arrangement.SpaceBetween) {
        Column {
            for (item in myList) {
                Text("Item: $item")
            }
        }
        Text("Count: ${myList.size}")
    }
}

이 코드는 부작용이 없으며 입력 목록을 UI로 변환합니다. 

그러나 함수가 지역 변수에 쓰는 경우 이 코드는 스레드로부터 안전하지 않거나 적절하지 않습니다.

 

@Composable
@Deprecated("Example with bug")
fun ListWithBug(myList: List<String>) {
    var items = 0

    Row(horizontalArrangement = Arrangement.SpaceBetween) {
        Column {
            for (item in myList) {
                Text("Item: $item")
                items++ // Avoid! Side-effect of the column recomposing.
            }
        }
        Text("Count: $items")
    }
}

 

위 예제 에서 items는 재구성을 통해 수정됩니다.

수정은 애니메이션의 모든 프레임에서 또는 목록이 업데이트될 때 실행될 수 있습니다.

어느 쪽이든 UI에 잘못된 개수가 표시됩니다.

 

때문에 이와 같은 사용은 Compose에서 지원되지 않습니다.

이러한 쓰기를 금지함으로써 프레임워크가 Composable 람다를 실행하도록 스레드를 변경할 수 있습니다.

 

3) 재구성은 최대한 많은 수의 Composable 함수 및 람다를 건너뜀

UI의 일부가 잘못된 경우 Compose는 업데이트해야 하는 부분만 재구성하기 위해 최선을 다합니다.

 

즉, UI 트리에서 위 또는 아래에 있는 Composable를 실행하지 않고 

단일 버튼의 Composable을 다시 실행하는 것을 건너뛸 수 있습니다. 

 

모든 Composable 함수 및 람다는 자체적으로 재구성할 수 있습니다.

다음은 목록을 렌더링할 때 재구성이 일부 요소를 건너뛸 수 있는 방법을 보여주는 예입니다.

/**
 * Display a list of names the user can click with a header
 */
@Composable
fun NamePicker(
    header: String,
    names: List<String>,
    onNameClicked: (String) -> Unit
) {
    Column {
        // this will recompose when [header] changes, but not when [names] changes
        Text(header, style = MaterialTheme.typography.h5)
        Divider()

        // LazyColumn is the Compose version of a RecyclerView.
        // The lambda passed to items() is similar to a RecyclerView.ViewHolder.
        LazyColumn {
            items(names) { name ->
                // When an item's [name] updates, the adapter for that item
                // will recompose. This will not recompose when [header] changes
                NamePickerItem(name, onNameClicked)
            }
        }
    }
}

/**
 * Display a single name the user can click.
 */
@Composable
private fun NamePickerItem(name: String, onClicked: (String) -> Unit) {
    Text(name, Modifier.clickable(onClick = { onClicked(name) }))
}

 

-Compose는 header가 변경될 때 상위 요소 중 어느 것도 실행하지 않고 Column 람다로 건너뛸 수 있습니다.

-Column을 실행할 때 Compose는 names가 변경되지 않았다면 LazyColumnItems를 건너뛰도록 선택할 수 있습니다.

 

다시 말하지만, 모든 Composable 함수 또는 람다를 실행하는 작업에는 side-effect 없어야 합니다.

side-effect 을 실행해야 할 때는 콜백에서 side-effect를 트리거해야 합니다.

 

4) 재구성은 낙관적이며 취소될 수 있음

Compose가 컴포저블의 파라미터가 변경되었을 수 있다고 생각할 때마다 재구성이 시작됩니다.

재구성은 낙관적입니다.

 

즉, Compose는 파라미터가 다시 변경되기 전에 재구성을 완료할 것으로 예상합니다.

재구성이 완료되기 전에 매개변수가 변경되면 Compose는 재구성을 취소하고

새 매개변수를 사용하여 재구성을 다시 시작할 수 있습니다.

 

재구성이 취소되면 Compose는 재구성에서 UI 트리를 삭제합니다.

표시되는 UI에 종속되는 side-effect가 있다면 재구성이 취소된 경우에도 side-effect가 적용됩니다.

이로 인해 일관되지 않은 앱 상태가 발생할 수 있습니다.

 

낙관적 재구성을 처리할 수 있도록 모든 Composable 함수 및 람다가 멱등원이고

side-effect가 없는지 확인해야 합니다.

 

5) Composable 함수는 매우 자주 실행될 수 있음

경우에 따라 Comsable 함수는 UI 애니메이션의 모든 프레임에서 실행될 수 있습니다.

 

함수가 기기 저장소에서 읽기처럼 비용이 많이 드는 작업을 실행하면

이 함수로 인해 UI 버벅거림이 발생할 수 있습니다.

 

Composable 함수에 데이터가 필요하다면 데이터의 매개변수를 정의해야 합니다.

그런 다음, 비용이 많이 드는 작업을 구성 외부의 다른 스레드로 이동하고 

mutableStateOf 또는 LiveData를 사용하여 Compose에 데이터를 전달할 수 있습니다.

 

 

이상 Compose 이해를 정리한 내용입니다.