함수형 UI vs 선언형 UI
컴포즈를 이해하려면 선언형 UI가 무엇인지, 그 탄생 배경이 어떠한지 부터 이해하는 것이 편하다.
기존 안드로이드 뷰 계층 구조는 함수형 UI로, UI 위젯의 트리로 표시할 수 있었다. 사용자 상호작용 등의 이유로 인해 앱의 상태가 변경되면, 현재 데이터를 표시하기 위해 UI 계층 구조를 업데이트해야 했다. UI를 업데이트하는 가장 일반적인 방법은 findViewById()와 같은 함수를 사용하여 트리를 탐색하고 button.setText(String), container.addChild(View) 또는 img.setImageBitmap(Bitmap)와 같은 메서드를 호출하여 노드를 변경해 위젯의 내부 상태를 변경시켰다.
이때, 데이터를 여러 위치에서 렌더링하고 뷰를 업데이트하면, 즉 수동적으로 뷰를 조작하면 오류가 발생할 위험이 커진다.
따라서 이를 개선하기 위해 선언형 UI는 처음부터 화면 전체를 개념적으로 재생성한 후 필요한 변경사항만 적용하는 방식으로 작동한다. 이러한 접근 방식은 스테이트풀(Stateful) 뷰 계층 구조를 수동으로 업데이트할 때의 복잡성을 방지할 수 있다. 이때, 화면 전체를 재성성하는 것은 큰 비용이 필요하므로 이때 컴포즈는 특정 시점에 어떤 UI를 다시 그려야 하는지 알아서 똑똑하게 선택한다. 이에 대해서는 추후 더 정리해보자 !
Recomposition
Compose에서 UI를 업데이트하려면 해당 Composable 함수를 다시 호출해야 한다. 이를 Recomposition, 즉 재구성이라고 한다. 리컴포지션은 변경된 데이터에 따라 필요한 UI 요소만 다시 그려 성능을 최적화한다.
아래 예시와 같은 경우, 해당 컴포저블 함수를 통해 클릭할때마다 clicks 값이 업데이트 되고, 컴포즈는 이 값을 사용해 텍스트를 다시 그린다. 이때, 버튼은 재구성되지 않고 텍스트만 갱신된다.
Recomposition is the process of calling your composable functions again when inputs change. This happens when the function's inputs change. When Compose recomposes based on new inputs, it only calls the functions or lambdas that might have changed, and skips the rest. By skipping all functions or lambdas that don't have changed parameters, Compose can recompose efficiently.
리컴포지션은 입력값이 변경될 때 컴포저블 함수를 다시 호출하는 과정이다. 이때 함수의 입력값이 변경되었을 때만 리컴포지션이 발생한다. Compose는 새로운 입력값을 기준으로 리컴포지션할 때 변경될 가능성이 있는 함수나 람다만 호출하고, 나머지는 건너뛴다. 이렇게 변경되지 않은 함수나 람다를 모두 건너뛰기 때문에 컴포즈는 전체 UI 를 재구성하는 것이 큰 비용이 드는데도, 효율적인 재구성이 가능하다.
@Composable
fun ClickCounter(clicks: Int, onClick: () -> Unit) {
Button(onClick = onClick) {
Text("I've been clicked $clicks times")
}
}
Recomposition 시 주의점
Side Effect 피하기
Composable 함수는 언제든지, 어떤 순서로든 호출될 수 있으므로 다음과 같은 Side Effect는 피하는 것이 좋다.
- 공유 객체의 속성 변경
- ViewModel의 Observable 업데이트
- SharedPreferences 업데이트
Composable 함수는 항상 같은 입력에 대해 같은 결과를 반환해야 한다.
비용이 큰 작업은 백그라운드에서
Composable 함수에서 SharedPreferences 읽기 같은 비용이 큰 작업을 하면, 매 프레임마다 불필요한 호출이 발생해 UI 성능 저하가 발생한다. 아래 예시처럼 ViewModel에서 비동기로 데이터를 가져오고 Composable 함수에는 결과만 전달해야 한다.
@Composable
fun SharedPrefsToggle(
text: String,
value: Boolean,
onValueChanged: (Boolean) -> Unit
) {
Row {
Text(text)
Checkbox(checked = value, onCheckedChange = onValueChanged)
}
}
기타 알아두어야 할 컴포즈의 성질
- 리컴포지션은 가능한 많은 컴포저블 함수와 람다를 건너뛴다.
- 즉, 모든 컴포저블 함수와 람다는 개별적으로 리컴포지션될 수 있다
- 리컴포지션은 낙관적으로 진행되며, 취소될 수 있다.
- 리컴포지션은 변경 사항이 감지되면 즉시 시작되지만, 만약 리컴포지션 중에 파라미터가 다시 변경되면 이전 작업을 취소하고 새 파라미터로 다시 시작한다. 이를 Optimistic(낙관적) Recomposition이라 한다.
- 컴포저블 함수는 매우 자주 실행될 수 있으며, 애니메이션의 매 프레임마다 실행될 수 있다.
- 따라서 컴포저블 함수는 가벼워야 하고, 위처럼 시간 및 비용이 크게 드는 작업은 백그라운드에서 처리하고 결과만 전달하는 것이 좋다.
- 컴포즈는 멀티스레딩을 고려하여 설계되었다. 따라서 멀티스레드로 실행될 수 있다고 가정하고 코드를 짜야한다.
- 따라서 이에 따라 발생할 만한 혹시 모를 사이드 이펙트는 무조건 피하자.
- 컴포저블 함수는 병렬로 실행될 수 있다.
- 따라서 컴포저블 람다 내부에서 변수를 수정하는 코드는 피해야한다.
- 컴포저블 함수는 순서에 상관없이 실행될 수 있다.
- 따라서 각 함수는 독립적이어야 하며, 전역 변수를 수정하거나 순서에 의존되서는 안된다.
참고자료
Compose 이해 | Jetpack Compose | Android Developers
이 페이지는 Cloud Translation API를 통해 번역되었습니다. Compose 이해 컬렉션을 사용해 정리하기 내 환경설정을 기준으로 콘텐츠를 저장하고 분류하세요. Jetpack Compose는 Android를 위한 현대적인 선언
developer.android.com
'안드로이드 > 컴포즈' 카테고리의 다른 글
[Android][Compose] 컴포즈의 stateful / stateless, remember, mutableStateOf, remeberSaveable, 상태 호이스팅 (0) | 2025.02.28 |
---|---|
[Android][Compose] 컴포즈의 3단계, 상태, 컴포지션의 생명주기, 콜 사이트 (0) | 2025.02.27 |
[Android][Compose] 네비게이션 구현하기 (0) | 2024.07.18 |
[Android][Compose] State와 상태 호이스팅 (0) | 2024.07.05 |
[Android][Compose] Composable과 Recomposition (0) | 2024.07.05 |