
최근 개발을 하면서 다른 사람들의 GitHub을 뒤져보다가, TextField를 너무나도 간단하게 사용하는 사례를 발견했습니다.
제가 기존에 사용하던 방식과 비교했을 때 너무 간단해져서 사용을 고려했지만, 결국 어떠한 이유로 사용하지 않게 되었기에, 이 경험을 공유해보고자 합니다.
기존 방식
제가 이전에 사용했던 방식은 ViewModel에서 상태를 관리하는 형태였습니다.
@HiltViewModel
class NameViewModel @Inject constructor() : ViewModel() {
private val _name = MutableStateFlow("")
val name: StateFlow<String> get() = _name
fun setName(newName: String) {
viewModelScope.launch {
_name.value = newName
}
}
}
@Composable
fun NameScreen(
viewModel: NameViewModel = viewModel(),
) {
val name by viewModel.name.collectAsStateWithLifecycle()
BasicTextField(
value = name,
onValueChange = {
viewModel.setName(it)
},
)
}
이렇게 단순한 화면에서는 큰 문제가 없지만, TextField가 3 ~ 4개를 넘어가는 화면에서는 이를 처리하는 귀찮은 상태 관리 코드들이 TextField의 개수만큼 생겨나야 했습니다.
새로운 방식
이런 귀찮은 방식을 조금이라고 줄여줄 수 있는 방법은 TextFieldState를 사용하는 방법입니다.
사용법은 너무나도 간단합니다. String 타입 대신 TextFieldState를 상태로 관리하고 넘겨주기만 하면 됩니다.
@HiltViewModel
class NameViewModel @Inject constructor() : ViewModel() {
private val _name = MutableStateFlow(TextFieldState(""))
val name: StateFlow<TextFieldState> get() = _name
}
@Composable
fun NameScreen(
viewModel: NameViewModel = viewModel(),
) {
val name by viewModel.name.collectAsStateWithLifecycle()
BasicTextField(
state = name,
)
}
TextFieldState를 사용함으로 onValueChange 메서드에 대한 처리를 해주지 않아도 됩니다. 이는 한 화면에 많은 TextField가 필요할 경우에 큰 차이를 보입니다.
TextFieldState의 원리
TextFieldState의 원리는 사실 간단합니다.
- TextFieldState는 Class입니다.
- 객체를 생성하고, 내부의 프로퍼티로 텍스트를 관리합니다.
- 내부에서는 text 프로퍼티와 버퍼를 사용해 텍스트를 업데이트 합니다.
TextFieldState의 내부를 보면 text 프로퍼티로 텍스트을 저장하고, Buffer를 사용해서 그 값을 업데이트하고 있습니다.
@Stable
class TextFieldState
internal constructor(
initialText: String,
initialSelection: TextRange,
initialTextUndoManager: TextUndoManager,
) {
internal var mainBuffer: TextFieldBuffer =
TextFieldBuffer(
initialValue =
TextFieldCharSequence(
text = initialText,
selection = initialSelection.coerceIn(0, initialText.length),
)
)
val text: CharSequence
get() = value.text
}
BasicTextField는 이 내부 버퍼에 접근하여, 텍스트를 업데이트해주고 있기에 별도의 처리가 없어도 텍스트의 변경이 이뤄집니다.
TextFieldState의 단점
코딩은 편해졌지만, 생각보다 TextFieldState는 단점이 꽤 있습니다.
1. TextFieldState는 Class다
클래스는 기본적으로 참조 기반이기 때문에 내용이 아닌 주소를 비교합니다.
이는 StateFlow나 Compose Recomposition에서 변경을 감지할 때, 변경을 감지하지 못한다는 것을 의미합니다.
예를 들어 아래의 코드같이 TextFieldState의 text 변경을 감지하는 것이 불가능합니다.
private val _name = MutableStateFlow(TextFieldState(""))
val name: StateFlow<TextFieldState> get() = _name
init {
viewModelScope.launch {
name.collect {
println("Text changed: ${it.text}") // 변경을 감지하지 못함
}
}
}
// 이런식으로 사용해야 함
init {
viewModelScope.launch {
snapshotFlow { _name.value.text }
.collect { newText ->
println("Text changed: $newText")
}
}
}
만약 TextFieldState의 text 변경에 따라 특정 이벤트를 실행해야 하는 경우가 있다면, snapShotFlow를 통해 값을 감지해야 합니다. (다만 BasicTextField의 경우에는 값이 바뀔때마다 Recomposition이 일어나기 때문에 매번 새 값을 가져올 수 있습니다)
2. TextFieldState의 값은 Mutable하다
TextFieldState는 기본적으로 Mutable 하기에, 아래와 같이 사용하는것이 아무 의미가 없습니다.
private val _name = MutableStateFlow(TextFieldState(""))
val name: StateFlow<TextFieldState> get() = _name
fun clearText() {
// 둘 다 가능
_name.value.clearText()
name.value.clearText()
}
또한 TextFieldState에 접근할 수 있는 모든 곳에서, text의 변경이 가능하기 때문에 예측 불가능한 상태 변경이 발생할 수 있습니다.
결론
TextFieldState의 장점은 사용이 너무나도 간편해진다는 것입니다. 여기서 말하진 않았지만 다양한 메서드들이 있으며 사용법 또한 매우 간단합니다.
하지만, 만약 텍스트의 상태 변경을 감지해야하는 경우에는 추가적인 조치가 필요하고 Mutable하지 않다는 치명적인 문제가 있습니다.
이런 문제점들이 중요하지 않으시다면, 사용을 고려하셔도 좋지만 저는 뭔가 마음 한편이 찝찝하기에 사용을 보류하게 되었습니다.
'Android > Compose' 카테고리의 다른 글
| Recomposition 최적화와 Stable 타입의 관계 (0) | 2026.01.07 |
|---|---|
| Recomposition과 Compose Navigation를 사용할 때 주의할 점 (1) | 2025.11.20 |
| ViewModel 톺아보기 (1) - ViewModel의 생성과 관리 (0) | 2024.11.18 |
| SavedStateHandle을 통해 Compose Navigation간 데이터 전달하기 (3) | 2024.09.23 |
| Compose의 Side-effects (0) | 2024.08.17 |