안드로이드

선언형 UI와 명령형 UI

Bum_2 2024. 10. 12. 21:30

명령형 UI

UI 요소들의 상태 변화와 동작을 명령하여 사용하겠다는 것이다. UI의 요소에 대해 명시적인 변경을 명령하여 조건에 따라 어떻게 업데이트할지 세밀하게 작성해야한다.

 

  • 명령형 UI의 특징
    • 명시적인 상태 관리 : UI의 상태를 수동으로 추적하고 변경한다.
      예를 들어 버튼 클릭 시 텍스트를 변경하거나, 특정 뷰의 visibility를 변경하는 동작을 직접 제어해야 한다.
    • 단계별 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