개요
Compose의 Stateless
선언형 UI인 Compose의 장점은 Stateless함에 있다.
UI와 State의 상호 의존성을 끊을 수 있다면, UI요소를 재사용하여 더 효율적으로 코드를 작성할 수 있다.
또한 UI를 개별적으로 테스트 할 수 있어, 유닛 테스트에 용이하다.
요약하자면 Compose Stateless의 장점은 아래의 두 개가 된다.
- UI 재사용성
- UI 테스트로 Unit Test
하지만 State와 종속되는 요소도 있다
그러나 Stateless를 유지하고 싶지만, State를 UI에 저장을 해야만 할 때가 있다.
Compose 요소 자체에 State가 저장되게 설계가 되어 나온 것으로 `TextField`가 있다.
아래와 같은 식으로 `remember` 키워드를 이용하여 상태를 저장해줘야한다.
만약 State를 넣어주지 않으면 텍스트를 아무리 입력해도 바뀌지 않게 된다.
매 순간 입력한 값을 기억하지 못해 기본 값으로 초기화가 되어 값이 그대로 유지된다. ▼
val text = remember {
mutableStateOf("")
}
BasicTextField(
value = text.value,
onValueChange = (){},
)
State Hoisting, Stateful을 Stateless하게 바꾸는 것
"그러면 결국 State에 의존하는 요소가 생기는 거잖아?"
이를 해결하는 것이 State Hoisting이다.
State Hoisting은 직역하면 상태 끌어올리기로, 상태를 상위 레이아웃으로 끌어올려 해당 요소와 상태를 분리하는 것을 말한다.
State Hoisting을 통해 Stateless를 유지하며 그 장점을 살릴 수 있게 된다.
State Hoisting
상태 의존성이 생기는 부분
아까의 `BasicTextField`의 예시를 보자.
`BasicTextField`의 `value`에 상태가 유지되는 `remember`변수를 넣어줘야했다. ▼
val text = remember {
mutableStateOf("")
}
BasicTextField(
value = text.value,
onValueChange = (){},
)
이를 같은 `@Composable` 함수에 넣게 되면, 한 곳에 묶여 State가 해당 `@Composable`에 종속되게 된다.
또한 `onValueChange`의 내용도 채우게 되면 해당 `@Composable`은 재사용이 어려워진다.
특별한 행동을 수행하기 때문이다.
우리는 여기서 상태 의존성 혹은 의존성이 생기는 부분이 `value`와 `onValueChange`임을 알 수 있다.
상태 끌어올리기
어느 부분에서 의존성이 생기는 지를 알았으니 해당 부분을 빼내어 상위 레이아웃으로 끌어올려주면 된다.
"끌어올린다는 표현이 너무 추상적인데..."
상태를 끌어올린다는 것은 의존성이 생기는 부분을 매개변수로 빼낸다고 생각하면 쉽다.
예를 들어 아래와 같은 구조가 있다고 해보자. ▼
해당 구조는 `BasicTextField` 안에 State가 저장되고, `onValueChange`도 내부에 작성이 되어 재사용이 불가능해진다.
공통적인 부분이 사라지고, 고유한 요소가 되어버린다. ▼
하지만 위의 구조를 아래와 같이 바꾸면 상태를 `InputScreen`으로 '올릴' 수 있다. ▼
상태를 위로(Input Screen으로) 올리는 State Hoisting을 하게되면 상태와 이벤트(onValueChange)는 아래와 같은 구조로 전달이 된다. ▼
`InputTextField`, `BasicTextField`는 상태와의 의존성이 사라지게 되면서 재사용이 가능해진다. ▼
ViewModel까지 끌어올리기
"그런데 그럼 결국 Screen Composable은 State에서 자유로울 수 없네?"
AAC의 구성요소인(이는 다음에 좀 더 자세히 다룰 예정이다) ViewModel까지 끌어올리게 되면 Screen도 State에서 자유로워진다.
상태와 관련된 것들을 ViewModel에서 관리하여 더 유연하게 재사용이 가능해진다. ▼
이렇게 BasicTextField에서부터 상태와 이벤트와 관련된 요인들을 매개변수로 빼내어 가장 상위 레이아웃에 까지 전달하여 상태로부터 독립할 수 있게 된다.
State Hoisting을 무조건 해야하는 것은 아니다
State Hoisting을 하지 않아도 될 때
"와 그럼 State Hoisting은 완전 만능이네? 무조건 써야겠네?"
상태를 항상 위로 끌어올릴 필요는 없다.
상태를 제어해야할 다른 Composable이 없는 경우 상태를 내부에 유지하는 것이 좋을 수 있다.
아래의 예제는 터치하면 추가 정보를 보여주는 채팅 메시지다. ▼
@Composable
fun ChatBubble(
message: Message
) {
var showDetails by rememberSaveable { mutableStateOf(false) } // Define the UI element expanded state
ClickableText(
text = AnnotatedString(message.content),
onClick = { showDetails = !showDetails } // Apply simple UI logic
)
if (showDetails) {
Text(message.timestamp)
}
}
추가 정보를 보여주는 `showDetails`라는 상태는 해당 Composable에서만 사용되고 관리되기 때문에 State Hoisting을 하지 않아도 된다.
오히려 따로 빼서 관리하게 되면 개별적으로 적용되는게 아니라 일괄적으로 적용이 될 수 있다.
그렇기에 항상 적용하는 것이 아니라 필요에 따라 적용을 해야한다.
State Hoisting의 단점들
앞에서도 봤지만, State Hoisting이 항상 좋은 것은 아니다.
아래의 단점들도 존재한다.
1. 코드의 복잡성 증가
상태가 하위 컴포저블에서 상위 컴포저블로 이동하면서 상태를 전달하고 관리하는 코드가 늘어난다.
이는 코드 복잡도를 증가시키고, 상태를 전달하기 위한 추가적인 객체 생성이 발생할 수 있다.
특히, 상태를 여러 레벨의 컴포저블 계층 구조를 통해 전달해야 하는 경우, 이러한 오버헤드가 더 커질 수 있다.
또한 상태가 많아지고, 상태들 사이에 의존성이 있다면 코드는 더더욱 복잡해진다.
2. 빈번한 UI 재구성으로 인한 객체 생성 비용 증가
Android Compose에서 state hoisting을 하면 상태가 상위 컴포저블로 이동하게 되고, 상태가 변경될 때마다 상위 컴포저블과 그 하위 컴포저블들이 모두 다시 구성된다.
이 과정에서 새롭게 객체들이 생성되거나 기존 객체들이 재사용되지 않고 버려지는 일이 발생할 수 있다.
특히, 상태 변경이 자주 발생하는 경우 재구성 빈도가 높아져 객체 생성 비용이 증가한다.
게다가 앞의 코드의 복잡성 증가로 인해 추가적인 객체 생성이 발생하면서 객체 생성이 늘어나게 되어 비용이 증가할 수 있다.
이런 문제점들이 존재하기에 State Hoisting이 언제나 옳은 것은 아니며 상황을 보고 적용을 해야한다.
마치며
이번에는 State Hoisting에 대해 알아보았다.
Flutter에도 State 개념이 있고, Stateless와 Stateful 개념이 있어서 크게 생소한 개념은 아니었다.
허나 Flutter는 상태관리 툴에서 상태들을 관리하기에 기본적으로 State Hoisting이 된 것이라 해당 개념에 대해 배우는 일이 없었는데 이번 기회에 알게 되어 Flutter에도 적용할 수 있겠다는 생각이 들었다.
'Develop > AndroidOS' 카테고리의 다른 글
[Android] Compose BOM (0) | 2024.06.25 |
---|---|
[Android] Dependency Injection (DI) (0) | 2024.06.14 |
[Android][Compose] Compose의 레이아웃 (0) | 2024.06.13 |
[Android][Compose] JetPackCompose란? 그리고 Compose의 구성 (0) | 2024.06.12 |