본문 바로가기
Android/Compose

Compose 이해

by 겸 2023. 10. 26.

명령형 UI vs 선언형 UI

명령형 UI

지금까지 Android 뷰 계층 구조는 UI위젯의 트리로서 나타냈다.

사용자의 상호작용으로 앱의 상태가 변경되면, 현재 데이터를 표시하기 위해 UI계층을 업데이트 해야한다.

데이터가 변경되면 findViewById()로 뷰를 탐색하고, button.setText(String), container.addChild(View) 또는 img.setImageBitmap(Bitmap)과 같은 메서드를 호출하여 UI 내부 상태를 바꾸어 업데이트 해야하는 것이다.

이런 방식으로 뷰를 수동으로 업데이트하면 오류가 발생할 가능성이 커지고, 앱을 유지 관리하는 것이 복잡해진다.

선언형 UI

처음부터 화면 전체를 개념적으로 재생성한 후 필요한 변경사항만 적용하는 방식으로 작동한다.

이 방식은 Stateful뷰 계층 구조를 수동으로 업데이트할 때의 복잡성을 방지할 수 있다.

Compose는 선언형 UI 프레임워크이다.


Composable 함수

@Composable 어노테이션

모든 컴포저블 함수에는 이 어노테이션이 있어야 한다.

이 함수가 데이터를 UI로 변환하기 위한 함수라는 것을 Compose 컴파일러에 알리는 것이다.

컴포저블 함수는 매개변수를 통해 UI를 결정한다.

컴포저블 함수는 어떤 것도 return하지 않는다.

해당 함수 자체가 원하는 화면 상태를 설정하므로 return할 필요가 없다.

컴포저블 함수는 멱등원이다.

멱등원이란 같은 연산을 여러번 수행하더라도 결과가 달라지지 않음을 의미한다.

함수를 동일한 인수로 여러번 호출해도 동일하게 작동하며, 전역 변수를 사용하지 않는다.

따라서 속성이나 전역 변수의 수정과 같은 부작용 없이 UI를 만들 수 있다.


Recomposition

XML레이아웃을 생각해보자. 각각의 위젯은 자체적으로 내부 상태를 유지하고 앱 로직과 상호작용 할 수 있도록 getter와 setter를 노출한다.

 

Compose는 Stateless상태이며 getter 또는 setter를 노출하지 않는다.

즉, 객체 형태로 노출되지 않는다.

동일한 컴포저블 함수를 다른 인수로 호출하여 UI를 업데이트 한다.

이 방식은 ViewModel과 같은 아키텍처 패턴이 상태를 쉽게 제공할 수 있다.

데이터가 업데이트 된 것을 감지할 때마다 UI에 그대로 반영하면 된다.

앱의 로직이 가장 최상위 컴포저블 함수에 데이터를 제공하면, 다른 컴포저블을 호출하면서 해당 데이터를 가장 아래의 계층 구조까지 전달한다.

 

반대로 사용자가 UI와 상호작용 할 때는 어떻게 동작할까?

UI는 onClick과 같은 이벤트를 발생시키고, 해당 이벤트를 앱의 로직에 전달해 앱의 상태를 변경한다.

상태가 변경되면 컴포저블 함수는 새 데이터를 사용한 새 매개변수로 다시 호출 될 것이고, UI 요소들이 다시 그려질 것이다.

이 과정을 Recomposition이라 한다.

 

버튼이 클릭될 때마다 clicks값을 업데이트 한다고 해보자.

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

Compose는 Text함수를 사용해 람다를 다시 호출하여 새 값을 표시하고, 값에 종속되지 않는 다른 함수는 재구성되지 않는다. 이것이 컴포즈의 재구성 과정이다.

 

만약 클릭할 때마다 모든 UI가 재구성된다면 많은 리소스가 필요할 것이다.

따라서 변경될 가능성이 있는 함수나 람다만 호출하고 나머지는 건너뛰는 방식으로 효율적으로 동작한다.

 

애니메이션 도중 버벅거림을 방지하기 위해서는 컴포저블 함수가 빨라야 한다.

내부에서는 리소스가 많이 드는 작업을 실행하는 대신 백그라운드 코루틴에서 작업을 실행하고 결과를 컴포저블의 매개변수로 전달해야 한다.

ViewModel의 백그라운드 코루틴에서 읽고 쓰는 작업을 한 뒤, 값을 전달해 UI를 업데이트하는 것이 적절하다.


특징

1. 컴포저블 함수는 순서와 상관 없이 실행 가능하다.

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

세 개의 화면을 그리는 코드가 있다고 할 때, 호출이 순서와 상관 없이 발생할 수 있다.

따라서 각 함수는 이전 실행 결과를 이용해서는 안되고, 독립적이어야 한다.

2. 컴포저블 함수는 동시에 실행할 수 있다.

컴포저블 함수를 동시에 실행하여 재구성을 최적화할 수 있다.

컴포즈는 다중 코어를 활용하고, 화면에 없는 컴포저블 함수를 가장 낮은 우선순위로 실행할 수 있다.

 

컴포저블 함수가 ViewModel 함수를 호출하면 컴포즈는 동시에 여러 스레드에서 이를 호출할 수 있다.

즉, 호출자와 다른 스레드에서 호출이 발생할 수 있어 thread-safe하지 않다.

 

함수 내에서 로컬 변수를 사용한다면 어떻게 될까?

@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가 수정되어 잘못된 개수가 표시된다.

 

이런 부작용을 없애기 위해서는 로컬 변수를 없애고 다음과 같이 작성해야 한다.

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

3. 재구성은 최대한 많은 컴포저블 함수와 람다를 건너뛴다.

UI의 일부가 유효하지 않은 경우, 업데이트가 필요한 부분만 재구성하기위해 최선을 다한다.

즉, 버튼을 업데이트 해야하는 경우, 버튼만 업데이트하고 해당 UI의 상위 혹은 하위의 컴포저블을 재구성하는 것을 건너뛸 수 있다.

4. 재구성은 낙관적이고, 취소될 수 있다.

컴포저블 매개변수가 변경되었을 수도 있다고 생각할 때마다 재구성이 시작되므로 낙관적이다.

만약 재구성이 완료 되기 전에 또 다시 매개변수가 변경되면 컴포즈는 재구성을 취소하고 새로운 매개변수를 사용해서 다시 재구성을 시작할 수 있다.

5. 컴포저블 함수는 애니메이션의 모든 프레임에서 같은 빈도로 자주 실행될 수 있다.

UI애니메이션의 모든 프레임에서 실행될 수 있으므로 리소스가 많이 드는 작업을 실행한다면 UI가 버벅거릴 수있다.

만약 기기의 설정을 UI스레드가 읽으려고 하면 초당 수백 번 읽을 수 있으므로 앱 성능에 치명적인 영향을 줄 수 있다.

비용이 많이 드는 작업을 해야한다면 컴포지션 외부의 스레드에서 작업을 진행하고, mutableStateOf 또는 LiveData를 사용하여 Compose에 데이터를 전달할 수 있다.

반응형

'Android > Compose' 카테고리의 다른 글

Compose의 레이아웃 - 기본사항  (0) 2023.12.01
Compose의 상태 - Flow, LiveData  (0) 2023.11.29
Compose의 상태 - 호이스팅이란?  (0) 2023.11.02
Compose의 상태  (0) 2023.11.02