-
[Android] Compose 상태 관리의 기본 개념개발/Android 2025. 1. 30. 23:41
들어가며
이번 포스트는 Compose의 상태에 대한 전반적인 이해와 이를 관리하는 기초적인 방법을 설명합니다.
Composable을 선언하는 방법이나, 더욱 효율적인 상태 관리 방법(상태 호이스팅 등)에 관한 내용은 다른 문서를 참고해주시기 바랍니다.
이번 회차에서는 상태란 무엇이며, Compose에서는 상태 변화에 따라 UI를 어떻게 변경하는지에 대해 알아봅니다.
목차
상태란 무엇인가?
- 상태에 대한 정의와 예시
- 안드로이드에서 상태를 변경하는 방식: xml vs Compose
- xml 방식에서 상태를 변경하는 방법
- Compose에서 상태를 변경하는 방법
- Compose가 상태를 변경하는 원리
상태의 변경을 알려주는 방법
- MutableState와 동작 원리(feat. State, RecomposeScope)
- mutableStateOf()
상태를 유지하는 방법
- remember
- rememberSaveable : 구성 변경에도 대응하자!
상태란 무엇인가?
상태에 관한 Compose 공식문서의 설명
State and Jetpack Compose
State in an app is any value that can change over time. This is a very broad definition and encompasses everything from a Room database to a variable in a class. All Android apps display state to the user. (후략)상태에 대한 정의와 예시
앱에서 상태란 시간이 지남에 따라 변할 수 있는 모든 값들을 의미합니다.
상태가 의미하는 바는 매우 추상적이고, 그 범위가 방대합니다. DB의 데이터부터 네트워크 통신의 결과, 클래스 내부의 변수까지 다양한 데이터를 상태라고 부를 수 있습니다.
조금 더 직관적으로 나타내면, UI에 직접적으로 나타나거나 UI를 나타내는데 활용되며, 언제든 변경될 수 있는 값이라고 말할 수 있습니다. 결국 앱(또는 웹)은 상태에 따라 UI를 다르게 나타내어 사용자에게 적절한 정보를 제공해주어야하기 때문입니다.
아래는 Compose 공식문서에서 설명하는 안드로이드 앱에서 나타날 수 있는 상태의 예시입니다.
- 네트워크 연결을 할 수 없다는 메세지를 보여주는 스낵바
- 블로그 글과 댓글들
- 사용자가 버튼을 클릭할 때 버튼에 나타나는 물결 애니메이션
- 이미지 위에 그릴 수 있는 스티커
이외에도 로그인 여부, 회원가입 시 입력한 아이디의 유효성, 사진 로딩 완료 여부 등이 있습니다.
위 예시에서 나타나는 요소들을 상태라고 정의할 수 있습니다.
안드로이드에서 상태를 변경하는 방식: xml vs Compose
앱에서 상태는 언제든 변경될 수 있는 값이라고 했습니다. 앱 실행 중에 특정 상태가 변경된다면, 상태의 변경이 UI에도 반영되도록 구현해야할 것입니다. 그렇다면 Compose에서는 어떻게 상태를 변경할까요?
이를 이해하기 위해서 xml 방식에서 상태를 변경하는 방식과 비교해보겠습니다.
기존 xml 기반의 안드로이드 구현은 명령형 프로그래밍 방식입니다. xml로 View를 구성하고 선언했지만, 해당 View를 제어하는 View 로직(Activity, Fragment 등)에서 View의 상태를 설정하고 변경했습니다. 아래 예제 코드를 함께 살펴보면 어떤 의미인지 조금 와닿을 것 같습니다.
버튼을 클릭하면 클릭 횟수가 1씩 증가하고, 그 횟수를 화면에 Text로 보여주는 간단한 예시입니다.
- Activity 코드
// Activity에서 View에 나타나는 상태를 명령하여 변경합니다. class CounterActivity : AppCompatActivity() { private var count = 0 // 버튼이 눌린 횟수(상태) override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_counter) val countButton = findViewById<Button>(R.id.btn_count) val countTextView = findViewById<TextView>(R.id.tv_count) countButton.setOnClickListener { count++ // countTextView의 text(상태)를 변경하라고 명령 countTextView.text = "$count" } } // ... }
- xml View 구성
<androidx.constraintlayout.widget.ConstraintLayout android:layout_width="match_parent" android:layout_height="match_parent"> <TextView android:id="@+id/tv_count" android:layout_width="wrap_content" android:layout_height="wrap_content" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" /> <Button android:id="@+id/btn_count" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginTop="10dp" android:text="+" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toBottomOf="@id/tv_count" /> </androidx.constraintlayout.widget.ConstraintLayout>
setOnClickListener 에서 클릭에 대한 이벤트를 설정하여 블럭 안에 정의된 코드가 수행됩니다.
Button이 클릭될 때 마다, 증가된 count 값을 TextView 의 text 에 할당하는 동작이 이루어집니다.
즉, View에게 직접 명령하여 View의 상태를 변경하고 있습니다.
“야, TextView. 너의 text는 이거야. 이걸로 바꿔.”
반면 Compose는 선언형 프로그래밍 방식을 따릅니다. 아래의 예제 코드를 살펴볼까요?
// UI가 어떻게 구성되어있는지를 선언한 Composable 입니다. @Composable fun CounterScreen() { var count by remember { mutableStateOf(0) } // 버튼이 눌린 횟수(상태) // UI 구성을 선언 Column( modifier = Modifier.fillMaxSize(), verticalArrangement = Arrangement.Center, horizontalAlignment = Alignment.CenterHorizontally, ) { Text(text = "$count") Spacer(modifier = Modifier.height(10.dp)) Button( onClick = { count++ } ) { Text(text = "+") } } }
앞의 xml 예시와 동일한 동작을 수행하는 화면입니다. View가 어떻게 구성되어있는지를 Composable 함수로 나타냈습니다.
xml에 정의한 View에게 직접 명령하여 상태를 변경했던 방식과 달리, Compose에서는 단지 View가 어떻게 생겼는지만 선언했습니다.
“얘, TextView야. 너는 이렇게 생겼단다~”
상태를 변경하는(또는 변경과 관련이 있는) 부분에 대한 코드만 비교하자면 아래와 같습니다.
// xml에 선언된 View에게 상태를 변경할 것을 명령 private var count = 0 // 버튼이 눌린 횟수(상태) //... // 버튼이 클릭될 때 마다 count 증가, text 설정 countButton.setOnClickListener { count++ // Text의 변경을 명령: Text의 text를 "$count"로 설정해라. countTextView.text = "$count" }
// Composable의 형태를 선언 var count by remember { mutableStateOf(0) } // 버튼이 눌린 횟수(상태) //... // Text Composable을 선언: Text의 text는 "$count"이다. Text(text = "$count")
Compose가 상태를 변경하는 원리
어떻게 UI를 그려야할지 선언만 했을 뿐인데, 과연 Compose는 상태의 변화를 어떻게 UI에 반영하는 것일까요?
공식문서를 참고하여 조금 더 자세히 이해해보겠습니다.
State and Composition
Compose is declarative and as such the only way to update it is by calling the same composable with new arguments. These arguments are representations of the UI state. Any time a state is updated a
recomposition takes place. As a result, things like TextField don’t automatically update like they do in imperative XML based views. A composable has to explicitly be told the new state in order for it to update accordingly.공식문서에서 상태에 따른 Composition이 발생하는 원리에 대해 언급한 부분입니다. 우선 해석한 내용을 읽어봅시다!
Compose는 선언적이며, Composable을 업데이트하기 위해서는, 새로운 인수를 사용하여 동일한 Composable을 호출해야 합니다. 이러한 인수는 UI 상태를 나타내며, 상태가 업데이트될 때마다 Composable의 재구성이 이루어집니다. 따라서 TextField와 같은 것은 명령형 xml 기반 뷰에서처럼 자동으로 업데이트되지 않습니다. Composable이 그에 따라 업데이트 되도록 하려면 새 상태를 명시적으로 알려주어야 합니다.
Compose에서 Composable이 업데이트되는 원리를 친절히(?) 설명해주었습니다. 걱정마세요, 이해를 돕기 위해 단계별로 설명드리겠습니다!
- 우선, Compose는 선언적입니다.
위의 Compose 예제를 보면 알 수 있듯, UI의 형태를 선언한 Composable 함수가 호출되어 UI가 그려집니다.(Composition)
한번 호출되어 그려진 Composable은 처음 선언되었던 UI를 그대로 유지합니다.(Initial Composition) - Composable을 업데이트하기 위해서는, 새로운 인수를 사용하여 동일한 Composable을 호출해야 합니다.
여기서 새로운 인수란 것은 새로 변경된 UI의 상태를 의미합니다. 즉, “새로운 상태”를 사용해 동일한 Composable 함수를 다시 호출해야만 Composable의 업데이트를 수행할 수 있습니다. - 상태가 업데이트될 때마다 Composable이 재구성됩니다.(Recomposition)
이것은 Compose가 상태의 변화를 감지한다는 것을 의미합니다. 개발자가 어떠한 방법으로 상태를 명시적으로 설정하면, Compose는 상태의 변화를 감지할 수 있습니다.
상태 값이 변경되면, Compose는 새로운 상태 값을 가지고 Composable을 다시 호출합니다. 그러면 변경될 UI에 대한 Composition이 다시 수행되어 UI가 업데이트됩니다. 이 과정을 Recomposition이라고 부릅니다.
그래서 Compose가 상태 변화에 따라 Composable을 Recomposition하여 UI를 업데이트하기 위해서는 아래 요소들이 필요합니다.
- Compose에게 상태 변경을 알려주어야 합니다. ( State , MutableState )
- Recomposition이 일어나도, 이전의 상태를 유지해야 합니다. ( remember , rememberSaveable )
저희는 Compose에서 위 두 가지를 구현하면 됩니다! 간단하죠?
이제부터 위의 두 요소를 가지고서 Compose가 Recomposition을 할 수 있도록 구현하는 방법을 알아봅시다.
그전에 아래 코드를 먼저 살펴볼까요? 닉네임을 입력받는 Composable이 있습니다. 하지만 아래의 코드를 실행하고 텍스트를 입력하면, 아무런 변화도 일어나지 않습니다.
@Composable private fun NicknameInputLayout() { Column( modifier = Modifier.padding(16.dp), ) { Text( text = "닉네임을 입력하세요.", modifier = Modifier.padding(bottom = 8.dp), style = MaterialTheme.typography.bodyMedium, ) OutlinedTextField( value = "", onValueChange = { }, label = { Text("닉네임") }, ) } }
왜 그럴까요? 앞에 나왔던 올바른 예제와 비교하면 어떤 부분이 다르고 잘못되었는지 알아차릴 수는 있습니다! 하지만 저희는 아직 Compose에게 상태 변화를 알려주고, 변경된 상태를 유지하는 방법을 자세히 모릅니다.
사람은 실수로부터 성장하는 법이라고 합니다. 이제 이 예제의 잘못된 부분을 하나씩 개선하면서, Compose가 상태의 변화를 감지하고 Recomposition을 수행하는 방법을 터득해보겠습니다!
상태의 변경을 알려주는 방법
앞서 Compose는 상태의 변화를 감지하여 Composable을 Recomposition한다고 했습니다.
“상태의 변화를 감지한다”, 어디서 많이 들어보지 않으셨나요? 혹시 LiveData 와 같은 Observer 패턴을 떠올리셨다면, 이미 완벽히 이해하신 겁니다!
Compose는 상태를 관찰하여 상태의 변화를 감지할 수 있습니다.
MutableState와 동작 원리(feat. State, RecomposeScope)
Compose가 상태를 관찰하도록 하려면 MutableState 를 사용해야 합니다. 아래는 MutableState에 관한 공식문서의 설명입니다.
MutableState
A mutable value holder where reads to the value property during the execution of a Composable function, the current RecomposeScope will be subscribed to changes of that value. When the value property is written to and changed, a recomposition of any subscribed RecomposeScopes will be scheduled. If value is written to with the same value, no recompositions will be scheduled.흠… 낯선 표현이 많아서 단번에 이해가 되지 않습니다. 공식 문서에 정의된 코드와 함께 살펴보며 천천히 이해 해봅시다!
@Stable interface MutableState<T> : State<T> { // 상태를 저장하는 value override var value: T // 문법적 설탕: 코드 작성을 편리하게 하기 위한 구조분해 연산자 operator fun component1(): T operator fun component2(): (T) -> Unit }
MutableState 는 수정될 수 있는 값, 즉 상태를 감싸는 Holder 입니다. 코드를 살펴보면 State 를 상속한 interface로 구현되어있고, value 프로퍼티를 재정의하고 있습니다.
@Stable interface State<out T> { val value: T }
참고로 State 역시 interface이며, 여기에 value 프로퍼티가 정의되어 있습니다.
State 의 value 는 val 로 읽기 전용이지만, MutableState 의 value 는 var 로 재정의하여 읽기, 쓰기가 가능합니다. State 는 관찰자가 상태 값을 읽을 때 접근하고, MutableState 는 소유자가 상태 값을 변경할 때 접근한다는 것을 알 수 있습니다.
Composable 함수가 실행되는 동안 MutableState 내부의 value 프로퍼티를 읽고, 현재 Composable 함수의 RecomposeScope가 value 값의 변경을 구독합니다.
value에 새 값이 할당되어 변경될 때 해당 값을 구독하고 있던 모든 RecomposeScope의 Recomposition이 이루어집니다. 만약 value가 동일한 값으로 할당되면 Recomposition은 일어나지 않습니다.
잠깐, RecomposeScope 는 무엇인가요?
Represents a recomposable scope or section of the composition hierarchy. Can be used to manually invalidate the scope to schedule it for recomposition.
Composition 계층 중에서 Recomposition이 이루어지는 범위를 나타냅니다. 즉, Recomposition이 일어날 수 있는 UI 라고 이해하시면 됩니다.
/** * Represents a recomposable scope or section of the composition hierarchy. Can be used to manually * invalidate the scope to schedule it for recomposition. */ interface RecomposeScope { /** * Invalidate the corresponding scope, requesting the composer recompose this scope. * * This method is thread safe. */ fun invalidate() }
RecomposeScope 의 구현입니다. interface로 구현되어있으며, 해당 UI를 무효화 처리하여 다시 그려내는 invalidate() 메서드가 정의되어있습니다. (안드로이드 xml View에서의 invalidate() 와 비슷합니다!)
RecomposeScopeImpl 에 대한 설명
A RecomposeScope is created for a region of the composition that can be recomposed independently of the rest of the composition. The composer will position the slot table to the location stored in [anchor] and call [block] when recomposition is requested. It is created by [Composer.startRestartGroup] and is used to track how to restart the group.
해당 RecomposeScope의 구현체가 Compose 내부에서 활용됩니다. Composition을 생성하는 Composer 가 RecomposeScope 를 생성하고, UI의 업데이트가 요청되면 해당 RecomposeScope 의 invalidate() 를 호출하여 UI를 Recompose하는 작업이 이루어집니다!
정리하자면, Compose에서는 내부적으로 상태를 관찰합니다. 상태의 변화가 일어나면 자동으로 Recomposition이 수행됩니다. Compose가 상태를 관찰할 수 있도록 구현하기 위해서는 MutableState를 사용합니다. 그러면 Compose는 Composable을 실행할 때 해당 Composable이 재구성될 수 있는 UI 임을 기억하고, 상태 변화를 감지하여 자동으로 UI를 업데이트 해줍니다.
mutableStateOf()
코드에서 MutableState 객체를 생성할 때는 mutableStateOf() 메서드를 사용합니다.
Return a new MutableState initialized with the passed in value
@StateFactoryMarker fun <T> mutableStateOf( value: T, policy: SnapshotMutationPolicy<T> = structuralEqualityPolicy() ): MutableState<T> = createSnapshotMutableState(value, policy)
위 코드는 MutableState<T> 객체를 만들어주는 mutableStateOf() 메서드의 코드 원문입니다.
SnapshotMutableState 가 무엇인지 모르겠지만, 저희는 MutableState를 구현하는 구현체임을 알 수 있습니다. 조금만 살펴보자면, Snapshot 이라는 이름에서 Git의 commit 시스템과 비슷한 동작으로 구현되었다는 것을 유추할 수 있을 것 같습니다.
(SnapshotMutableState 가 무엇인지에 대한 자세한 딥다이브는 다른 포스트에서 다뤄보겠습니다.)
파라미터로 value 와 policy 를 설정할 수 있습니다.
- value : MutableState 의 초기값을 지정합니다.
- policy : value 의 값이 변경되는 정책(규칙)을 의미합니다. (이 역시도 지금은 깊게 다루지 않습니다.)
그렇다면 MutableState 를 사용하면 Compose가 상태의 변화에 따라 자동으로 UI를 업데이트 해준다고 하니, 아래와 같이 코드를 수정하면 될 것 같습니다!
@Composable private fun NicknameInputLayout() { Column( modifier = Modifier.padding(16.dp), ) { // mutableStateOf 로 초기값을 지정하여 MutableState 생성 val nickname: MutableState<String> = mutableStateOf("") Text( text = "닉네임을 입력하세요.", modifier = Modifier.padding(bottom = 8.dp), style = MaterialTheme.typography.bodyMedium, ) OutlinedTextField( value = nickname.value, // TextField에 변경된 값을 나타냄 onValueChange = { nickname.value = it }, // TextField의 입력 값을 State에 지정 label = { Text("닉네임") }, ) } }
영화 '혹성탈출'의 시저가 안된다고 합니다. 안타깝게도 위 코드는 잘못된 구현입니다. 코드를 실행하고 텍스트를 입력해도 아무런 변화가 일어나지 않습니다. 왜 그럴까요?
그 이유는 Recomposition 에 있습니다.
앞서 상태의 변화가 일어나면 Compose가 해당 Composable을 다시 호출하여 UI를 그린다고 했습니다.
Composable이 다시 호출될 때마다, 입력 값 상태로 생성한 nickname 인스턴스가 계속해서 생성될 것입니다.
그러면 nickname State의 value 값은 항상 빈 문자열(“”)로 초기화됩니다.
// Recomposition으로 Composable이 다시 호출될 때마다 nickname이 매번 빈 문자열로 초기화됨 val nickname: MutableState<String> = mutableStateOf("")
즉, Recomposition이 발생할 때마다 상태의 변화가 초기화되기 때문에, 텍스트의 입력에도 아무런 변화가 일어나지 않는 것입니다.
이제 우리는 또 다른 요소를 구현해야 할 차례입니다. 바로 Recomposition이 일어나도, 이전 상태의 값을 유지시켜야 합니다!
상태를 유지하는 방법
그렇다면 Compose에서 Composable의 재구성이 일어나도 이전의 상태를 유지시키는 방법은 무엇일까요?
이를 위해서는 remember 와 rememberSaveable API를 활용하면 됩니다.
remember
remember
Composable functions can use the remember API to store an object in memory. A value computed by remember is stored in the Composition during initial composition, and the stored value is returned during recomposition. remember can be used to store both mutable and immutable objects.
Composable 함수에서 사용할 수 있는, 메모리에 객체를 저장하는 API라고 합니다.
자세한 내부 구현은 아래와 같이 되어 있습니다.
@Composable inline fun <T> remember(crossinline calculation: @DisallowComposableCalls () -> T): T = currentComposer.cache(false, calculation)
파라미터 람다인 calculation 은 저장하고자 하는 객체를 반환해주는 람다 함수입니다.
currentComposer 라고 하는 Composer 객체로부터 calculation 의 값을 캐싱하는 동작을 합니다.
Composer 의 cache API도 한번 살펴볼까요?
/** * A Compose compiler plugin API. DO NOT call directly. * * Cache, that is remember, a value in the composition data of a composition. This is used to * implement [remember] and used by the compiler plugin to generate more efficient calls to * [remember] when it determines these optimizations are safe. */ @ComposeCompilerApi inline fun <T> Composer.cache(invalid: Boolean, block: @DisallowComposableCalls () -> T): T { @Suppress("UNCHECKED_CAST") return rememberedValue().let { if (invalid || it === Composer.Empty) { val value = block() updateRememberedValue(value) value } else it } as T }
너무 복잡할지도 모르니 핵심 코드만 살펴보자면,
- rememberedValue() 에서 저장된 상태 값을 가져옵니다. 이름에서 유추할 수 있듯, 메모리(캐시)에 저장된 상태 값을 의미합니다.
- invalid 의 값과 메모리에서 불러온 상태 값이 비어있는지 확인합니다. invalid 는 무효화 처리가 필요한지를 나타냅니다.
- 만약 무효화 처리를 해야하거나(invalid == true) 저장된 상태가 없다면(it === Composer.Empty), “상태 값을 새로 수정해야한다”를 의미하므로 새로운 값을 저장해야 합니다. 그래서 block() 람다로부터 객체를 반환받아(val value = block()), rememberedValue() 에 저장하여 캐싱합니다.(updateRememberedValue(value))
- 위의 두 경우가 아니라면, rememberedValue() 에 저장된 값을 반환해줍니다.
remember API는 이런 방식으로 상태 값을 내부적으로 저장하고 있습니다.
참고 사항
remember stores objects in the Composition, and forgets the object when the composable that called remember is removed from the Composition.
remember 로 저장된 객체가 제거되는 시점 : remember 가 호출한 Composable이 Composition에서 제거될 때remember API는 아래와 같은 방법으로 사용할 수 있습니다. 작성하는 방법과 사용하는 방법은 조금 다르지만, 모두 동일한 동작을 합니다.
- val mutableState = remember { mutableStateOf(default) }
- MutableState 인스턴스를 반환받습니다.
- 상태 값에 접근하기 위해서는 mutableState.value 로 접근해야합니다.
- val (value, setValue) = remember { mutableStateOf(default) }
- sugar syntax로 value, setValue() 에 대한 참조를 반환받습니다.
- 상태 값을 읽을 때는 value, 수정할 때는 setValue()를 사용합니다.
- var value by remember { mutableStateOf(default) }
- by 위임 키워드를 이용하여 value 를 반환받을 수 있습니다.
- 상태 접근, 수정 모두 value 를 사용합니다.
- by 키워드를 사용하기 위해서는 아래 두 가지를 import 해야합니다.
import androidx.compose.runtime.getValue import androidx.compose.runtime.setValue
그래서 remember API를 사용하여 아래와 같이 코드를 변경하면, 드디어 우리가 원하는 동작을 구현할 수 있습니다!
@Composable private fun NicknameInputLayout() { Column( modifier = Modifier.padding(16.dp), ) { // remember API로 이전 상태의 값을 유지 var nickname: MutableState<String> by remember { mutableStateOf("") } Text( text = "닉네임을 입력하세요.", modifier = Modifier.padding(bottom = 8.dp), style = MaterialTheme.typography.bodyMedium, ) OutlinedTextField( value = nickname, onValueChange = { nickname = it }, label = { Text("닉네임") }, ) } }
rememberSaveable: 구성 변경에도 대응하자!
안드로이드에서는 구성 변경(ex. 다크모드, 화면 회전 등)이 일어나면, Activity가 제거되었다가 다시 생성되면서 이전의 상태 값들이 모두 사라집니다.
이는 Compose도 마찬가지입니다. 기존의 구현에서는 savedInstanceState: Bundle 를 활용하여 구성 변경에 대응할 수 있었습니다. Compose에서는 어떻게 처리할 수 있을까요?
구성 변경에도 데이터가 사라지지 않도록 하려면, rememberSaveable 을 사용할 수 있습니다!
rememberSaveable
It behaves similarly to remember, but the stored value will survive the activity or process recreation using the saved instance state mechanism (for example it happens when the screen is rotated in the Android application).
rememberSaveable automatically saves any value that can be saved in a Bundle. For other values, you can pass in a custom saver object.
remember API와 동일한 동작을 하지만, Activity의 재생성에도 저장한 값이 사라지지 않습니다. 이는 savedInstanceState (Bundle)를 사용하는 메커니즘과 같습니다.
Bundle에는 저장할 수 있는 타입(Int, String, Long 등 직렬화가 가능한 타입)이 제한되어 있습니다. 만일 직접 생성한 객체를 저장하고자 한다면, Saver 객체를 직접 생성하여 활용할 수 있습니다. (Saver 객체의 생성 방법은 공식 문서를 참고하시기 바랍니다.)
지금 구현에서는 간단하게 rememberSaveable 을 사용하는 것으로 바꿔주기만 해도, 구성 변경에 대응할 수 있는 Composable을 사용할 수 있습니다!
@Composable private fun NicknameInputLayout() { Column( modifier = Modifier.padding(16.dp), ) { // rememberSaveable API로 구성 변경에 대응 var nickname: MutableState<String> by rememberSaveable { mutableStateOf("") } Text( text = "닉네임을 입력하세요.", modifier = Modifier.padding(bottom = 8.dp), style = MaterialTheme.typography.bodyMedium, ) OutlinedTextField( value = nickname, onValueChange = { nickname = it }, label = { Text("닉네임") }, ) } }
마치며
지금까지 Compose에서 상태를 관리하는 기본적인 방법에 대해 알아보았습니다.
이제 Compose에서 상태가 어떻게 변화하고, UI가 그에 따라 어떻게 바뀌는지 알게되었습니다.
하지만, 현재의 구현은 몇 가지 문제점이 남아있습니다. 그 중 하나는 Composable의 재사용과 테스트가 어렵다는 것입니다.
Compose의 제일 큰 장점 중 하나는 View의 재사용이 편리한 점입니다.
여러 Composable의 조합으로 View를 구성할 수 있어, 필요한 속성을 잘 주입한다면 동일한 Composable을 여러 화면에서 사용할 수 있습니다.
하지만 지금 저희가 구현한 Composable은 nickname이라는 상태를 직접 들고 있어, 다른 UI에서의 재사용이 어렵습니다.
그렇게 되면 비슷한 구현을 하는 다른 Composable을 여럿 만들어야하고, 이는 서비스의 유지보수를 어렵게 만듭니다.
또한 UI가 상태의 변화에 따라 적절하게 변화되는지 확인하기 위해서는 테스트를 활용할 수 있습니다. 그러나 Composable이 직접 상태를 들고 있다면, 상태의 변화를 테스트하기 어렵습니다.
다음 시리즈에서는 이런 문제점을 해결하기 위해서, 상태를 더욱 효과적으로 관리하는 방법을 알아보겠습니다.
참고 레퍼런스
'개발 > Android' 카테고리의 다른 글
[Android] Github Actions로 CI 적용하기 - 간단 가이드 (0) 2025.04.02 [Android] Compose 상태 관리 심화 (0) 2025.02.15 [Android] 우리가 RecyclerView를 사용하는 이유 (feat. ListView) (0) 2025.01.10