Jetpack Compose의 선언적(Declarative) UI
Jetpack Compose의 선언적(Declarative) UI
Jetpack Compose는 선언적(declarative) UI 프레임워크로, 개발자가 UI를 단계별 명령으로 직접 조작하는 대신 주어진 상태(state)에 대해 UI가 어떤 모습이어야 하는지를 기술합니다. 프레임워크가 상태 변경을 관찰하고, UI 트리에서 영향을 받은 부분만 자동으로 리컴포지션(Recomposition)하기 때문에, 기존 XML 및 뷰 시스템에서 필수적이었던 수동 뷰 조작이 더 이상 필요하지 않습니다. Compose가 왜 선언적인지, 명령형(imperative) UI 패턴과 어떻게 다른지를 이해하는 것은 면접에서 매우 자주 다뤄지는 핵심 주제입니다. 특히 면접관 분들이 단순한 개념 설명 수준을 넘어, 실제로 리컴포지션이 어떻게 동작하고 왜 효율적인지까지 물어보는 경우가 많으므로 깊이 있게 학습해 두시는 것이 좋습니다.
이번 면접 질문을 통하여 아래 내용들을 학습하실 수 있습니다.
- 선언적(declarative) UI 프로그래밍 모델과 명령형(imperative) UI 프로그래밍 모델의 차이점을 설명할 수 있습니다.
- 상태(state)가 Compose에서 리컴포지션을 어떻게 주도하는지 이해할 수 있습니다.
@Composable함수가 선언적 컴포넌트의 특성을 어떻게 충족하는지 파악할 수 있습니다.- 컴포넌트 멱등성(idempotence)이 무엇이며, 왜 정확성 확보에 중요한지 설명할 수 있습니다.
- Compose 모델과 XML 레이아웃 및 명령형 뷰 업데이트 방식을 비교할 수 있습니다.
선언형 UI vs 명령형 UI
명령형(imperative) UI 모델에서는 개발자가 뷰 객체에 대한 참조를 직접 유지하면서 메서드를 호출하여 프로퍼티를 변경합니다. 기반 데이터가 변경될 때 어떤 뷰를 업데이트해야 하는지 추적하는 책임이 전적으로 개발자에게 있으며, 이로 인해 업데이트 경로 하나라도 누락되면 UI와 데이터가 불일치하는 동기화 문제가 발생할 수 있습니다. 실무에서도 이런 유형의 버그는 상당히 빈번하게 발생하며, 디버깅이 까다로운 경우가 많습니다.
// 명령형: 뷰를 수동으로 업데이트
var counter = 0
binding.button.setOnClickListener {
counter++
binding.button.text = "Clicked: $counter"
}
반면 선언적(declarative) UI 모델에서는 개발자가 현재 상태를 입력으로 받아 UI에 대한 기술(description)을 반환하는 함수를 정의합니다. 프레임워크가 상태 변경 시마다 이 함수를 호출하고 필요한 업데이트를 스스로 결정하므로, 개발자가 직접 뷰를 변경할 필요가 없습니다.
// 선언형: UI는 상태의 함수
@Composable
fun CounterButton() {
var count by remember { mutableStateOf(0) }
Button(onClick = { count++ }) {
Text("Clicked: $count")
}
}
선언적 접근 방식은 UI 상태 불일치와 관련된 버그를 근본적으로 차단합니다. 상태가 변경될 때마다 UI가 현재 상태로부터 새로 구성되기 때문에, 텍스트 필드 업데이트를 빠뜨리거나 잘못된 visibility 플래그를 토글하는 실수가 발생할 여지가 없습니다. 이 점이 기존 명령형 방식과 비교했을 때 가장 큰 장점이라고 할 수 있습니다.
상태 기반 리컴포지션
Compose 런타임은 각 @Composable 함수가 어떤 상태(state) 객체를 읽는지 추적합니다. 상태 값이 변경되면 해당 상태에 의존하는 컴포저블 함수 중 최소 집합만 식별하여 재실행합니다. 이 과정을 리컴포지션(Recomposition)이라고 합니다.
@Composable
fun UserProfile(user: User) {
Column {
// user.name이 변경될 때만 리컴포지션 발생
Text(text = user.name)
// user.email이 변경될 때만 리컴포지션 발생
Text(text = user.email)
// user 변경과 무관하게 리컴포지션되지 않음
StaticHeader()
}
}