성장기록지
Compose State와 State hoisting 본문
State hoisting?
Hoisting은 번역하자면 "끌어올림"이라는 뜻이다.
그럼 상태를 끌어올린다는 뜻인데, 상태는 무엇인지, 어떻게 끌어올린다는건지 감이 안 올 수 있을것이다.
그럼 우선 상태에 대해 알아보고 가자.
State란?
상태를 이해하기 위해선 다음 예시코드를 읽어보자.
앱을 실행시킨 후, 이 코드의 텍스트필드에 타이핑을 한다면 어떻게 될까?
정답은 아무 일도 일어나지 않는다. 텍스트필드에는 아무런 텍스트가 표시되지 않는다.
@Composable
private fun HelloContent() {
Column(modifier = Modifier.padding(16.dp)) {
Text(
text = "Hello!",
modifier = Modifier.padding(bottom = 8.dp),
style = MaterialTheme.typography.bodyMedium
)
OutlinedTextField(
value = "",
onValueChange = { },
label = { Text("Name") }
)
}
}
xml의 EditText와 다르게 왜 이런일이 발생하는지 짐작해본다면,
아래 코드의 OutLinedTextField는 내부적으로 입력값을 처리하는 로직이 없다고 볼 수 있다.
그렇다면 우리는 사용자의 입력값을 관리하고, 이를 OutLinedTextField에 전달해 줄 필요가 있다.
이러한 근거를 토대로 코드를 아래와 같이 변경해보았다.
@Composable
fun HelloContent() {
Column(modifier = Modifier.padding(16.dp)) {
var name by remember { mutableStateOf("") } // 추가
if (name.isNotEmpty()) {
Text(
text = "Hello, $name!",
modifier = Modifier.padding(bottom = 8.dp),
style = MaterialTheme.typography.bodyMedium
)
}
OutlinedTextField(
value = name,
onValueChange = { name = it },
label = { Text("Name") }
)
}
}
by 키워드를 통해 remember { mutableStateOf("") } 에 name의 초기화를 위임하는
이 코드가 대체 무엇이길래 이러한 일이 가능한 것일까?
우선 공식문서를 통해 remember가 어떻게 정의되었는지 알아보겠다.
Remember
영어 설명을 해석하자면 다음과 같다.
- remember 함수는 calculation 람다가 생성한 value를 기억한다.
- 이는 컴포저블이 recomposition 되더라도 해당 value를 유지한다는 의미이다
- calculation 람다는 composition 시점에 한 번만 평가된다.
- Recomposition 시, remember는 처음 컴포지션에서 계산된 값을 반환한다.
- Recomposition 시 기존에 저장된 value를 재사용한다.
- MutableState를 예를 들면 Recompositon 시 내부값은 갱신되지만 새로운 MutableState 객체를 만들지는 않는다는 의미이다.
요약하자면, composition 시점에 값이나 객체를 저장하고,
값이 recomposition 동안에 반환된다고 볼 수 있다.
또한 이 객체는 Recomposition이 일어나도 새로운 값이나 객체가 아닌
기존에 저장된 것을 사용한다는 것이다.
그렇다면 이제 MutableStateOf의 역할만 제대로 알면 어느정도 의문이 풀릴 것이다!
MutableState
mutableStateOf의 설명은 다음과 같이 작성되어있다.
그렇다면 MutableState는 무엇일까?
State의 하위 타입으로, 제네릭으로 타입이 정해져있으므로 어떠한 타입도 var형식의 value로 넣을 수 있다.
설명을 요약하여 해석해보자면,
- Composable 함수가 실행되는 동안, value를 읽을 수 있는 가변 value holder이다.
- value 가 변경되면 Recomposition이 일어난다. (같은 value로 작성되면 일어나지 않는다)
정도로 볼 수 있다 .
참고로 State는 다음과 같이 불변으로 정의되어 있다.
한줄로 간단히 설명이 가능 할 것이다.
- Composable 함수가 실행되는 동안, value를 읽을 수 있는 불변 value holder이다.
설명 요약
그렇다면 이제 State와 아까 추가로 작성하였던 name 변수에 대해 설명해보겠다!
- State는 Composable 함수가 실행되는 동안 제네릭 타입의 value를 읽을 수 있는 value holder이다.
- MutableState는 value를 Mutable하게 들고 있고, value 변경 시 Recomposition이 일어난다.
- remember는 composition 과정에서 이 MutableState를 저장하고 있고, recompostion 과정에서 반환해준다.
- 이를 통해 불필요한 계산을 피하고 상태를 유지하도록 돕는다!!
Stateful과 StateLess
State hoisting을 설명하려면, Stateful과 StateLess을 같이 설명하고 가야한다.
공식문서에는 아래와 같이 서술되어 있다.
요약하자면,
- 위의 예시처럼 remember를 사용하여 상태를 관리하는 컴포저블은 stateful 한 것이고,
- 상태를 갖지 않는 컴포저블은 Stateless 하다고 볼 수 있다.
- Stateless는 상태 호이스팅 (State hoisting) 을 사용해서 가능하다.
글의 처음 예시 코드에서는 상태가 없었을때 데이터의 갱신이 제한되었었는데,
State hoisting이 뭐길래 상태 없이도 사용가능한 것인지 알아보자!
State hoisting
공식문서에선 아래와 같이 State hoistion의 개념이 작성되어있다.
- Composable의 caller(호출자)로 state를 옮겨 stateless composable로 만드는 패턴이다.
- value와 onValueChange 두개의 파라미터를 통해 state를 caller로 옮길 수 있다.
- onValueChange로만 제한되지는 않고 컴포저블에 더 구체적인 이벤트가 어울리는 경우 람다를 사용하여 그 이벤트를 정의해서 사용할 수 있다.
상당히 추상적인 설명이라 이해가 어려울 수 있다.
이해를 돕기 위해 예시 코드와 함께 보도록 하겠다.
@Composable
fun HelloScreen() {
var name by rememberSaveable { mutableStateOf("") }
HelloContent(name = name, onNameChange = { name = it })
}
@Composable
fun HelloContent(name: String, onNameChange: (String) -> Unit) {
Column(modifier = Modifier.padding(16.dp)) {
Text(
text = "Hello, $name",
modifier = Modifier.padding(bottom = 8.dp),
style = MaterialTheme.typography.bodyMedium
)
OutlinedTextField(value = name, onValueChange = onNameChange, label = { Text("Name") })
}
}
HelloScreen의 변수 name에 집중하여 보자. remember와 mutableStateOf를 활용하고 있다.
(rememberSavable은 화면 회전, 앱 종료 후에도 상태를 저장하고 복원할 수 있지만
직렬화 가능한 데이터만 저장 가능한 것이다)
이 name을 HelloContent의 파라미터인 name과 onNameChange로 전달하고 있는 것을 볼 수 있다.
HelloContent에선 결과적으로 HelloScreen의 name을 사용하게 되고, Stateless가 되는 것이다!!
그림으로 보자면 아래와 같다.
state가 내려가고, event가 올라가므로 단방향 데이터 흐름(UDF)라 볼 수 있다.
State hoisting의 장점
State hoisting을 통해 Stateless가 된 컴포저블은 재사용하기 유용하다!
개발을 진행하며 공통된 사항이 있다거나 할 때, Stateless 컴포저블로 구성하여
원하는 State를 보내주며 재사용 할 수 있는 장점이 있다.
또한 테스트 용이성이 향상되며 UI 관리도 단순해질 것이다.
꼭 State hoisting을 해야하는가?
결론부터 말하자면 아니다.
공식문서에도 다음과 같이 나와있다.
간단히 설명을 추가하자면, 변수 showDetails는 이 UI 요소의 내부 상태이고, 그저 텍스트만 보여주는 단순한 로직이다.
또한 ChatBubble이라는 컴포저블 안에서만 활용되므로, 상태를 호이스팅하여 얻는 이익이 크게 없으므로 내부에 유지해도 된다는 것이다!
마치며
부스트캠프를 통해 진행한 프로젝트 리팩토링을 하며
state hoisting을 명확히 이해하기 위해 해당 글을 작성해 보았다.
다음 글에선 재사용성을 높이는 법과, 실제 프로젝트 적용 사례를 통해
글을 작성해보도록 하겠다.
'안드로이드' 카테고리의 다른 글
Android의 Clean Architecture란? (0) | 2025.01.07 |
---|---|
android 권장 아키텍쳐란? (0) | 2025.01.02 |
Fragment Manager란? (0) | 2024.12.30 |
사용하기 전에 알아보는 Jetpack Room (0) | 2024.12.21 |
안드로이드 Life Cycle (with Activity,Fragment) (1) | 2024.12.20 |