선언형 UI와 명령형 UI
명령형 UI
UI 요소들의 상태 변화와 동작을 명령하여 사용하겠다는 것이다. UI의 요소에 대해 명시적인 변경을 명령하여 조건에 따라 어떻게 업데이트할지 세밀하게 작성해야한다.
- 명령형 UI의 특징
- 명시적인 상태 관리 : UI의 상태를 수동으로 추적하고 변경한다.
예를 들어 버튼 클릭 시 텍스트를 변경하거나, 특정 뷰의 visibility를 변경하는 동작을 직접 제어해야 한다. - 단계별 UI 갱신 : UI가 변화할 때 단계별로 어떤 요소가 어떻게 변해야하는지 직접 정의해야한다.
예를 들어 리스트의 아이템이 추가되거나 제거될 때, 리스트 전체를 다시 그리거나 개별 아이템에 대해 수동으로 추가/삭제 작업을 처리한다. - 코드의 복잡성 : UI의 상태와 UI 구성 요소들이 강하게 결합되어있어서, UI가 복잡해질수록 명령형으로 상태를 관리하고 뷰를 업데이트하는 코드가 길어지고, 유지보수가 어려워진다.
- 명시적인 상태 관리 : UI의 상태를 수동으로 추적하고 변경한다.
선언형 UI
UI의 최종 상태를 기술하는 방식으로, UI가 어떤 모습이어야 하는지를 선언하겠다는 것이다. 선언형 UI에서도 상태 관리는 개발자가 제어하지만 상태가 변화했을 때 UI의 업데이트를, Compose 런타임이 상태 변화를 감지하고 컴포저블 함수들을 다시 호출하여 UI를 업데이트하는 것이다.
- 선언형 UI의 특징
- 상태 기반 UI : 상태를 기반으로 UI를 기술한다. 상태가 변경될 때마다 새로운 UI 트리를 렌더링하고, Compose 런타임은 이전 상태와 비교하여 필요한 부분만 업데이트한다.
- 간결하고 읽기 쉬운 코드 : 어떤 UI가 보여져야 하는지를 표현하는 코드이므로, 코드가 간결하고 읽기 쉽다. UI 구성 요소의 현재 상태만 기술하기 때문에, 세밀한 단계를 기술할 필요가 없다.
- 재사용이 가능한 컴포넌트 : UI를 컴포넌트 단위로 작성하여 UI의 모양이 동일하다면 재사용할 수 있다.
예시 코드와 함께 이해해보자.
명령형 UI 코드
class MainActivity : AppCompatActivity() {
// 상태 변수
private var count = 0
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
// 명령형 UI의 특징으로, button, countTextView라는 UI 요소를 직접 참조
val button = findViewById<Button>(R.id.button)
val countTextView = findViewById<TextView>(R.id.countTextView)
// 초기 상태 설정
countTextView.visibility = View.GONE
button.text = "Click me"
// 동작과 상태 변화를 기술
button.setOnClickListener {
count++
// 상태에 따라 UI를 직접 업데이트
countTextView.text = "Count: $count"
if (count > 0) {
countTextView.visibility = View.VISIBLE
}
}
}
}
XML 코드
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android">
<Button
android:id="@+id/btn_basic"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Click me"/>
</layout>
코드를 보면 명령형 UI에 대한 특징이 뚜렷하게 나타난다. 세밀하게 단계가 나뉘며, 상태 변화에 대한 UI 업데이트를 개발자가 직접 명령해야한다. 이것이 선언형 UI와의 가장 큰 차이점이라고 말할 수 있으며, 아래의 동일한 기능을 하는 선언형 UI를 보면 간단한 기능을 구현하는 것에서도 코드의 길이가 차이나는 것을 볼 수 있다.
선언형 UI 코드
@Composable
fun Counter() {
// 상태 관리 변수
var count by remember { mutableStateOf(0) }
Column(
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center
) {
// 버튼 클릭 이벤트에 따라 상태(count)를 변경
Button(onClick = { count++ }) {
Text(text = "Click me")
}
if (count > 0) {
// 상태 변화에 따른 UI의 최종 모습을 정의
Text(text = "Count: $count")
}
}
}
앞서 말했지만, 선언형 UI와 명령형 UI 모두 상태 관리는 개발자의 몫이다. 하지만 선언형 UI에서는 mutableStateOf, remember, rememberSaveable과 같은 Compose UI 툴이 존재하며 상태가 변화할 때마다 Compose 런타임이 알아서 UI를 업데이트하는 것이다.
❓초반에 들었던 궁금증
초기 생각으로는 명령형 UI와 명령형 UI가 "Count: $count"라는 텍스트를 업데이트하는 것에서 별다를 것이 없어보였다. 하지만, 선언형 UI에서는 "Count: $count"라는 최종 상태를 텍스트로 보여달라고 선언하며 UI 업데이트를 Compose 런타임이 맡는 것이고 명령형 UI에서는 버튼을 클릭할 때마다 개발자가 직접 "Count: $count"을 업데이트하라고 명령한 것이다.
"상태가 변화하면 UI를 업데이트한다"는 점에서는 개발자가 신경 쓰지 않아도 비슷하게 동작하는 것처럼 보일 수 있겠지만, 명령형 프로그래밍과 선언형 프로그래밍의 가장 중요한 차이는 "어떻게" 그 작업을 처리하는지와 "어떤 사고 방식"으로 코드를 작성하는지에 달렸다고 생각한다.
- 참고
Understanding Jetpack Compose — part 1 of 2