#1 개요
Jetpack Compose의 State 개념에 대해 살펴본다.
#2 일반적인 개념으로서의 State
먼저, State라는 용어 자체에 대한 이해가 필요하다. Jetpack Compose에서 쓰이는 State가 아니라 일반적인 개념으로서의 State 말이다. 상태라는 말로 번역되는 State는 말 그대로 상태, 즉 시간이 흐름에 따라 변할 가능성이 있는 값으로 다름아닌 아래와 같은 코드를 말하는 것이다.
// 일반적인 State를 나타내는 클래스
class Counter {
private var count = 0
// 상태를 읽는 함수
fun getCount(): Int {
return count
}
// 상태를 변경하는 함수
fun increment() {
count++
}
fun decrement() {
count--
}
}
프로퍼티 count를 State로 볼 수 도 있고, Counter 클래스를 State로 볼 수도 있다. State라는 용어 자체는 굉장히 추상적이기 때문이다.
#3 Jetpack Compose에서의 State
#3-1 조건 - UI에 표시되는 변수
그렇다면 이 Jetpack Compose에서 쓰이는 State란 무엇인가? 딱 봐도 일반적인 개념으로서의 State보다는 더 구체적인 의미로 쓰일 것이다. UI와 관련된 라이브러리인 Jetpack Compose 답게, Jetpack Compose에서의 State는 UI에 표시되는 수치, 더 정확히는 UI에 표시되는 변수다. #2에서 State는 시간이 흐름에 변할 가능성이 있는 값이라고 했다. 따라서 값이 하드 코딩되어 있을 리는 없으니 '수치' 보다는 '변수'라고 해야 더 정확한 표현이다.
하지만, Jetpack Compose의 State는 UI에 표시되는 변수라는 말은 불충분한 설명이다. 아래 코드를 보자.
var count = 0 // count는 Int형
@Composable
fun ButtonExample(modifierParam: Modifier = Modifier) {
Button(
onClick = {
Log.i("interfacer_han", "Current count value: ${++count}")
},
modifier = modifierParam
) {
Text(
text = "Count: $count",
fontSize = 40.sp
)
}
}
count가 Text()에서 참조되고 있다. 즉, count는 UI에 표시되는 변수다. 이 때 count는 'State'인가? 아니다. 일반적인 개념으로서의 State는 맞지만, Jetpack Compose에서 말하는 State가 되려면 한 가지 조건이 더 필요하다.
#3-2 조건 - Observable 패턴 구현
바로 Jetpack Compose의 State는 반드시 Observable 패턴을 구현해야만 한다는 조건 때문이다. #2의 count는 평범한 Int형 변수로 Observable 패턴과는 관계가 없다. 따라서 State라고 할 수 없다. 그렇다면 count를 Int형이 아니라 Observable 패턴을 구현한 데이터형으로 바꾸면 그만일 것이다. 그 데이터형으로는 MutableState, LiveData, Flow 또는 RxJava가 있다. count를 이러한 데이터형으로 바꿔보면,
val count = mutableStateOf(0) // count는 MutableState<Int>형
@Composable
fun ButtonExample(modifierParam: Modifier = Modifier) {
Button(
onClick = {
Log.i("interfacer_han", "Current count value: ${++count.value}")
},
modifier = modifierParam
) {
Text(
text = "Count: ${count.value}",
fontSize = 40.sp
)
}
}
비로소 count는 (Jetpack Compose의) State가 된다.
#4 State의 특권
#4-1 힘들게(?) State를 만들어 두는 이유
어떻게 보면 빡빡하다고도 볼 수 있는 2가지 조건을 만족해야만 얻어지는 Jetpack Compose의 State에게는 특권이 주어지는데, 바로 해당 State 값의 변화가 Jetpack Compose 런타임에 의해 자동으로 추적되고 UI 업데이트까지 암시적으로 진행된다는 점이다 (LiveData.observe()의 암시적 수행을 다룬 이 게시글과 같은 맥락의 동작을 한다). 즉, State를 만들어만 두면 프로그래머는 그 뒤의 일을 신경 끌 수 있다.
State의 특권을 체감하기 위해서, 실제 앱을 실행시켜 그 동작을 살펴보겠다. 위에서 다뤘던 count 변수가, State인 경우와 State가 아닌 경우 각각을 안드로이드 에뮬레이터로 실행시킨다.
#4-2 변수 count가 State가 아닌 경우의 동작
count가 State가 아니었던 #3-1를 실제로 실행시킨 다음 버튼을 클릭했을 때 일어나는 일이다. Log 메시지를 보면 count는 버튼을 누를 때마다 분명히 증가하고 있지만, UI에서 업데이트가 이뤄지지 않아 count가 계속 0인 걸로 보인다. 물론 애초에 Button의 클릭리스너에서 Text 업데이트에 관한 코드를 써 넣지 않았기 때문이지만 말이다 (여담이지만 사실, Jetpack Compose는 선언적 프로그래밍이라서 Button() 클릭 시 그 내부의 Text()를 업데이트하는 코드를 짜는 것 자체가 설계상 불가능에 가깝다 따라서, 어찌보면 Jetpack Compose에서의 State 사용은 특권이라기보단 의무라고 봐아할 지도 모른다).
#4-3 변수 count가 State인 경우의 동작
반면, count가 State였던 #3-2의 코드를 실행시키면 UI 업데이트가 이뤄진다. count가 State이기 때문이다. Compose 런타임은 새로운 State의 데이터로 Composable을 다시 실행해 업데이트된 UI를 화면에 출력한다. 이를 재구성(Recomposition)이라고 한다.
#5 LiveData와의 비교 우위
#5-1 간결함 (선언적)
LiveData를 사용하는 경우, 프로그래머가 명시적으로 구현해야 하는 코드가 상대적으로 더 많다. 예를 들어, LiveData는 데이터가 변경되었을 때 이를 관찰(observe)하고, 그에 따라 UI를 업데이트하기 위해 XML 레이아웃이나 ViewModel에 별도의 코드를 작성해야 한다. 이 과정에서 생명주기를 고려한 Observable 패턴을 설정하고 관리해야 하며, 데이터 변경에 따른 UI 업데이트를 명시적으로 처리해야 한다. 이로 인해 코드가 길어지고 복잡해질 수 있다. 물론 LiveData도 LiveData 출범 전의 코드들에 비하면 충분히 선언적이지만, Jetpack Compose 환경에서의 State 문법만큼 선언적이지는 않다.
#5-2 Jetpack Compose와의 통합성
State는 Jetpack Compose와의 통합을 고려하여 설계되었다. Compose와 자연스럽게 연동되어 추가적인 Observer 설정이나 생명주기 관리 없이 State.value의 변화에 따라 UI가 자동으로 재구성(Recomposition)된다.
#6 요약
UI에 표시되는 변수면서, 동시에 Observable 패턴까지 충족해야만 비로소 Jetpack Compose의 State다. State.value가 변하면 Recomposition이 암시적으로 수행된다.
#7 완성된 앱
#3-1 ~ #3-2의 코드가 담긴 안드로이드 프로젝트다.
'깨알 개념 > Android' 카테고리의 다른 글
[Android] Jetpack Compose - State Hoisting (0) | 2024.07.23 |
---|---|
[Android] Jetpack Compose - State Remembering (0) | 2024.07.23 |
[Android] Jetpack Compose - Lazy Column (0) | 2024.07.22 |
[Android] Jetpack Compose - Button (0) | 2024.07.22 |
[Android] Jetpack Compose - Layout, Arrangement, Alignment (0) | 2024.07.22 |