Jetpack Compose는 기존의 명령형 xml을 대신하고 Android를 위한 현대적인 선언형 UI 프레임워크이다.
Compose는 명령형으로 변형하지 않고도 앱 UI를 렌더링할 수 있게 하는 선언형 API를 제공하여 앱 UI를 더 쉽게 작성하고 유지관리할 수 있도록 지원한다.
선언형 UI를 통해 UI를 더 직관적이고 유지보수하기 쉽게 만드는 한편, 재구성(Recomposition) 매커니즘을 통해 성능을 최적화한다.
1. 선언형 UI의 개념
UI를 어떻게 업데이트할지 명령하는 대신, 어떤 상태가 되었을 때 UI가 어떻게 보여야 하는지를 선언하는 방식이다. Jetpack Compose에서는 UI 상태에 따라 화면을 구성하는 컴포저블 함수들이 실행되며, 앱의 상태가 변경될 때마다 필요한 부분만 다시 그려진다. 이로 인해 명령형 UI에서 자주 발생하는 복잡한 상태를 관리할 수 있다.
기존 명령형 UI의 문제점
개발자가 UI가 어떻게 그려져야 하는지 "일일이 지시"하는 방식으로 동작한다. 즉, 상태 변화가 있을 때마다 findViewById()로 특정 뷰를 찾아서 일일이 업데이트 해주었다. 이는 잘못된 상태 업데이트가 발생할 수 있으며, 특히 여러 요소에서 상태를 다루다보면 특정 UI를 갱신하지 않거나, 서로 충돌하는 업데이트가 일어나기 쉽다.
선언형 UI의 장점
무엇을 표현해야 하는지에만 집중한다. 개발자는 UI의 최종 상태만 선언하며, 그 상태에 도달하는 방법은 프레임워크가 알아서 처리한다. 상태 변화가 있으면 UI를 다시 선언하는 방식으로 작동하기 때문에 UI를 직접 갱신할 필요 없이 새로운 상태를 반영하여 전체 UI가 다시 정의되며, Compose는 자동으로 필요한 부분만 다시 그리게 된다.
상태 기반 UI (Stateful UI)
선언형 UI의 핵심 개념 중 하나는 상태이다. UI는 상태에 따라 그려지며, 상태가 변경되면 UI가 재구성된다. 이때 개발자는 UI가 직접적으로 어떻게 변해야 하는지 신경 쓸 필요 없이, 상태를 선언하고 UI가 이를 반영하도록 한다.
Jetpack Compose에서는 상태를 remember, State와 같은 매커니즘으로 관리하며, 이러한 상태가 변화할 때 UI는 자동으로 갱신된다. 이것은 명령형 UI에서 발생할 수 있는 상태 불일치 문제를 해결하는 데 매우 유용하다.
선언형 패러다임 전환
많은 명령형 프레임워크의 각 위젯들은 자체의 내부 상태를 유지하고 앱 로직이 위젯과 상호작용할 수 있는 getter와 setter 메소드를 노출한다. 하지만 Compose의 선언형 접근 방식에서는 Stateless 상태이며 getter와 setter를 노출하지 않는다. 사실상 위젯은 객체로 노출되지 않는다. 동일한 컴포저블 함수는 다른 인수로 호출하여 UI를 업데이트 한다. 이런 방식은 ViewModel과 같은 아키텍처 패턴에 상태를 쉽게 제공할 수 있다.
사용자가 UI와 상호작용할 때 UI는 onClick과 같은 이벤트를 발생시킨다. 이러한 이벤트를 앱 로직에 전달하여 앱의 상태를 변경해야 한다. 상태가 변경되면 컴포저블 함수는 새 데이터와 함께 다시 호출된다. 이것이 재구성이다.
※ stateless 와 Stateful에 대한 정보는 아래 게시글 참고
https://ehdnsdlek.tistory.com/46
[Jetpack Compose] Compose에서의 Statelss/Stateful
명령형 프로그래밍과 비교하여 Compose 선언형 접근 방식에 있어서 UI요소는 기본적으로 상태가 없는 Stateless라고 일컫는다. 그러나 컴포저블 내부에서 상태 관리가 불가능하다는 것이 아니고, Stat
ehdnsdlek.tistory.com
2. 재구성 (Recomposition)
Jetpack Compose의 핵심 동작 방식으로는 재구성(Recomposition)을 꼽을 수 있다. 간단히 말해서 앱 상태가 변경될 때 필요한 부분만 다시 그려주는 매커니즘이다. 즉, 앱의 상태에 따라 UI가 동적으로 변할 때 전체 UI를 다시 그리는 것이 아니라 상태 변화에 따라 변경이 필요한 부분만 효율적으로 다시 구성한다.
재구성의 작동 방식
(1) 앱의 상태가 변경되면 Compose는 상태를 관찰하고 해당 상태에 의존하는 컴포저블 함수를 다시 호출한다.
(2) 이 때, 상태가 변경된 부분에만 재구성이 일어나고, 변경되지 않은 컴포저블 함수는 건너뛰게 된다.
(3) 재구성은 "낙관적(optimistic)"으로 처리되며, 만약 재구성(Recomposition)이 필요하지 않다고 판단되면, 최종적으로 재구성을 취소할 수 있다.
@Composable
fun Counter(clicks: Int, onClick: () -> Unit) {
Button(onClick = onClick) {
Text("Clicked $clicks times")
}
}
위 예시에서 버튼이 클릭될 때마다 Clicks 상태가 변경되면, Compose는 Counter 컴포저블을 재구성한다. 이 때, 버튼 자체가 변하는 것이 아니라 Button 내부의 Text 컴포저블만 재구성되어 클릭 횟수가 업데이트된다는 것이다.
재구성은 낙관적(optimistic)이다.
"재구성은 낙관적이다"라는 말인 즉슨, 재구성이 시작되면 Compose는 최종적으로 재구성이 성공적으로 완료될 것이라는 전제하에 작동한다는 의미이다. 재구성은 UI 상태가 변경될 때 UI를 업데이트 하는 과정인데, 실행 중에 변경된 상태에 따라 재구성을 취소하거나 다시 시작할 수 있는 매커니즘을 가지고 있다.
재구성이 필요하다고 판단되면 Compose는 컴포저블 함수를 다시 호출한다. 이 과정에서 Compose는 재구성이 성공적으로 끝날 것이라고 "낙관적"으로 가정하고, 기존 UI 트리와 새로 생성된 트리의 차이점을 반영한다.
그러나 이 과정에서 상태가 다시 변경되거나 매개변수가 변하면, 재구성을 취소하고 새 상태나 매개변수를 기반으로 다시 재구성을 시작할 수 있다. 즉, 한 번 시작한 재구성이 반드시 끝까지 완료되지 않을 수 있다는 점이 낙관적이라는 의미이다.
낙관적인 접근 덕분에 불필요한 재구성을 미리 취소하고, 상태 변화에 민감하게 대응할 수 있다. 즉, 최종적으로 안정된 상태에로 재구성될 것이라고 가정하고, 재구성 중에 변경 사항이 생기면 빠르게 새 상태에 맞춘 재구성을 시작할 수 있는 유연성을 제공한다.
재구성이 취소되면 Compose는 재구성에서 UI트리를 삭제한다. 표시되는 UI에 종속되는 부작용이 있다면 구성이 취소된 경우에도 부작용이 적용된다. 따라서 낙관적 재구성을 처리할 수 있도록 모든 구성 가능한 함수 및 람다가 멱등원이고 부작용이 없는지 확인해야 한다.
재구성의 멱등성(Idempotency)
낙관적 재구성에서 발생할 수 있는 문제의 해결책은 바로 멱등성이다. Jetpack Compose의 재구성 과정에서는 멱등성이 매우 중요하다. 멱등성은 같은 입력에 대해 함수가 언제나 동일한 결과를 반환해야 함을 의미한다. 이는 재구성 도중 같은 컴포저블 함수가 여러 번 호출되더라도 동일한 출력을 보장해야 한다.
3. 효율적인 재구성
Jetpack Compose는 지능적인 재구성을 통해 성능을 최적화한다. 상태가 변경되지 않은 컴포저블 함수는 재구성을 건너뛰고, 필요한 경우에만 UI를 다시 그린다. 이를 통해 불필요한 컴퓨팅 자원을 절약하고 성능 저하를 방지할 수 있다.
컴포저블 함수는 순서와 관계없이 실행할 수 있음
@Composable
fun ButtonRow() {
MyFancyNavigation {
StartScreen()
MiddleScreen()
EndScreen()
}
}
위 코드에서 코드가 표시된 순서대로 실행된다고 가정할 수 있다. 그러나 코드가 반드시 표시된 순서대로 실행되는 것은 아니다. 컴포저블 함수에 다른 컴포저블 호출이 포함되어 있다면 그 함수는 순서와 관계없이 실행될 수 있다. Compose에는 일부 UI요소가 다른 UI 요소보다 우선순위가 높다는 것을 인식하고 그 요소를 먼저 그리는 옵션이 있다.
전통적인 명령형 UI 방식에서는 코드가 작성된 순서대로 UI 요소가 그려지고, 이러한 순서가 유지되는 것이 중요했다. 하지만 선연형 UI 프레임워크인 Jetpack Compose는 컴포저블 함수가 개별적인 독립성을 가지고 있기 때문에, 순서와 관계없이 실행되는 것이다.
따라서 Compose는 UI 요소 간의 의존 관계보다는 상태에 따른 결과물을 중시하는 방식으로 동작한다. 예를 들어 여러 UI 요소가 병렬적으로 렌더링되거나 최적화된 순서로 그려질 수 있다. 컴포저블 함수가 서로 의존 관계가 없다면 어떤 함수가 먼저 실행되더라도 최종 결과에는 영향을 미치지 않는다. 이것이 UI를 효율적으로 그리는 유연성의 확보 방법이다.
컴포저블 함수는 동시에 실행할 수 있음
Compose는 컴포저블 함수를 동시에 실행하여 재구성을 최적화할 수 있다. 이를 통해서 Compose는 다중 코어를 활용하고 화면에 없는 컴포저블 함수를 낮은 우선순위로 실행할 수 있다. 이는 Compose가 성능 최적화를 위해 재구성 과정에서 여러 컴포저블을 병렬로 처리할 수 있다는 것을 의미한다. 다중 스레드에서 동시에 실행되기 때문에, 스레드 간의 충돌을 방지하기 위해 부작용 없이 코드를 작성하는 것이 중요하다.
이 최적화는 컴포저블 함수가 백그라운드 스레드 풀 내에서 실행될 수 있음을 의미한다. 구성 가능한 함수가 ViewModel에서 함수를 호출하면 Compose는 동시에 여러 스레드에서 컴포저블 함수를 호출할 수 있다.
컴포저블 함수가 호출될 때 호출자와 다른 스레드에서 호출이 발생할 수 있다. 즉, 컴포저블 람다의 변수를 수정하는 코드는 지양해야 한다. 이러한 코드는 스레드로부터 안전하지 않을 뿐 아니라 구성 가능한 람다의 허용되지 않는 부작용이기 때문이다. 부작용을 피하기 위해서는 상태 관리를 remember, State, MutableState 등 Compose에서 제공하는 매커니즘을 통해 적절하게 처리할 수 있다.
재구성은 가능한 한 많이 건너뜀
UI의 일부가 잘못된 경우 Compose는 업데이트해야 하는 부분만 재구성하기 위해 최선을 다한다. 즉, UI트리에서 위 또는 아래에 있는 컴포저블을 실행하지 않고 단일 버튼의 컴포저블을 다시 실행하는 것을 건너뛸 수 있다. 이는 전체 UI 트리를 다시 그리지 않음으로써 성능을 최적화 할 수 있다.
성능 최적화를 위해서 필요하지 않은 재구성을 가능한 한 많이 건너뛰도록 설계된 것을 선택적 재구성이라고 부르며, 상태 변화가 발생한 부분만 효율적으로 다시 그려진다.
컴포저블 함수는 매우 자주 실행될 수 있음
경우에 다라 컴포저블 함수는 UI 애니메이션의 모든 프레임에서 실행될 수 있다. 특히 애니메이션이나 사용자 상호작용이 빈번한 UI에서, 컴포저블 함수는 프레임마다 다시 호출될 수 있다. 이런 경우, 컴포저블 함수가 비용이 많이드는 작업(ex: 네트워크 호출, 파일 시스템 접근)을 포함하고 있다면 성능에 심각한 영향을 미칠 수 있다.
이를 방지하기 위해서 컴포저블 함수가 반드시 빠르게 실행되고, 비용이 많이 드는 작업은 백그라운드에서 처리하거나, Compose 외부에서 처리한 후 그 결과만 컴포저블 함수로 전달받는 방식으로 설계하는 것이 중요하다. 네트워크 요청이나 데이터베이스 작업은 ViewModel과 같은 상태 관리 클래스를 통해 백그라운드에서 처리하고, 그 결과를 UI에 바인딩하는 방식으로 처리하는 것을 지향해야 할 것이다.
https://developer.android.com/develop/ui/compose/mental-model?hl=ko
Compose 이해 | Jetpack Compose | Android Developers
이 페이지는 Cloud Translation API를 통해 번역되었습니다. Compose 이해 컬렉션을 사용해 정리하기 내 환경설정을 기준으로 콘텐츠를 저장하고 분류하세요. Jetpack Compose는 Android를 위한 현대적인 선언
developer.android.com
'Android' 카테고리의 다른 글
[Jetpack Compose] Composable 수명 주기 (1) | 2024.09.09 |
---|---|
[Jetpack Compose] Compose에서의 Statelss/Stateful (2) | 2024.09.06 |
[Android] ViewModel의 인스턴스 공유 문제 + hiltNavGraphViewModels (0) | 2024.07.08 |
[Android] ViewPager2에 관련된 속성들 (0) | 2024.05.03 |
[Android] String Resources - <string-array>로 깔끔하게 관리하자 (0) | 2024.04.02 |