Compose의 State
선언형 UI 프레임워크인 Compose는 Stateless함이 가장 큰 장점이다. UI에 대한 UI상태의 상호 의존성을 끊을 수 있다면 UI의 재사용성이 생기고, UI에 대한 테스트 또한 가능해지기 때문이다.
Compose Stateless의 장점
1. UI 재사용성
2. UI 테스트 가능성
하지만 가끔 상태가 UI에 저장되어야만 하는 경우가 있다. 대표적으로 TextField가 있다. TextField는 어떤 텍스트가 입력되었는지를 저장해야 하기 때문에 상태를 가져야 한다. 아래에서 제목을 가지도록 Custom한 TextField인 TextFieldWithTitle을 보자.
@Composable
fun TextFieldWithTitle(title: String) {
var text by rememberSaveable { mutableStateOf("") }
Column(modifier = Modifier.padding(16.dp)) {
Text(
text = title,
modifier = Modifier.padding(bottom = 12.dp),
style = MaterialTheme.typography.h6
)
OutlinedTextField(
value = text,
onValueChange = { text = it }
)
}
}
위의 컴포저블은 TextField의 상태를 가지고 있는 Composable이다. 이 Composable 내부에는 text가 있고 text는 TextField의 상태를 저장하기 때문이다. 이 방식의 한계점은 Stateless한 Composable이 Stateful하도록 바뀐다는 점이다. 이렇게 Composable을 Stateful하게 만든다면, 재사용성이 줄어들고, 테스트 가능성이 없어진다. 우리는 이러한 한계점을 극복하기 위해 State Hoisting을 사용한다.
State Hoisting이란?
State Hoisting은 Stateful한 Composable을 Stateless하도록 만들기 위한 디자인 패턴이다. State Hoisting를 직역해보면, "상태(State)를 끌어올리기(Hoisting)" 라는 뜻을 가진다. State Hoisting을 통해 자식 Composable의 State를 해당 Composable을 호출하는 Composable 쪽으로 끌어올림으로써 자식 Composable을 Stateless하게 만드는 것이다. 요약하면 State Hoisting은 자식 Composable의 State를 호출부로 끌어올리는 것을 뜻한다.
그렇다면 어떻게 State를 끌어올릴까? State Hoisting에서는 State를 두 변수로 나누는 방식으로 State를 끌어올린다.
- value: T : 값
- onValueChange: (T) -> Unit : 새 값이 들어왔을 때 값을 변경하도록 요청하는 함수(이벤트)
예를 들어 위의 TextFieldWithTitle을 KotlinWorldScreen에서 호출한다고 하자. TextFieldWithTitle의 상태를 없애기 위해 TextFieldWithTitle에 text: String와 onTextValueChange: (String) -> Unit)를 추가한다.
@Preview(showBackground = true, widthDp = 300, heightDp = 200)
@Composable
fun KotlinWorldScreen() {
var text by rememberSaveable { mutableStateOf("") }
TextFieldWithTitle(title = "Title", text = text, onTextValueChange = { text = it })
}
@Composable
fun TextFieldWithTitle(title: String, text: String, onTextValueChange: (String) -> Unit) {
Column(modifier = Modifier.padding(16.dp)) {
Text(
text = title,
modifier = Modifier.padding(bottom = 12.dp),
style = MaterialTheme.typography.h6
)
OutlinedTextField(
value = text,
onValueChange = onTextValueChange
)
}
}
이렇게 하면 TextFieldWithTitle Composable은 Stateless해지고, Screen으로부터 상태를 전달 받는다.
추가적으로 ViewModel에만 State를 저장하도록 패턴을 수정하면 Screen Composable또한 Stateless하도록 변경할 수 있다.
@Preview(showBackground = true, widthDp = 300, heightDp = 200)
@Composable
fun KotlinWorldScreen() {
TextFieldWithTitle(title = "Title", text = viewModel.text, onTextValueChange = { viewModel.text = it })
}
State Hoisting이 사용되는 곳
State Hoisting은 compose 내부에서 상태를 저장해야 할 때 많이 사용된다. 실제로 TextField그 자체 또한 State Hoisting 패턴을 이용해 Stateless해진 것을 내부를 보면 확인할 수 있다.
@Composable
fun TextField(
value: String,
onValueChange: (String) -> Unit,
..
)