Compose 의 선언형 UI와 상태기반 렌더링
Compose는 선언형 UI 프레임워크이다. 이 말은 곧 State(상태)를 기준으로 UI를 자동으로 다시 그려주는 방식임을 의미한다.
@Composable
fun Greeting(name : String){
Text(text = "Hello, $name")
}
→ 해당 코드에서 name 이라는 상태 값이 변경되면, name을 사용하는 Text는 자동으로 recomposition된다.
이 때, 상태를 어디서 보관하고, 어떻게 UI에게 전달할 것인가? 라는 것이 StateHoisting, StateHolder의 핵심이다.
State Hoisting이란?
상태를 UI 구성요소 내부에서 외부로 끌어올리는 패턴이다.
즉 컴포저블 내부에서 상태를 가지지 않고, 상태를 파라미터로 받아서, 상태를 바꾸는 calback도 외부에서 주입을 받는다.
State Hoisting 예시
먼저 잘못된 예시부터 보겠다.
잘못된 예시
@Composable
fun Counter(){
var count by remember { mutableStateOf(0) }
Button (onClick = {count++}){
Text ("Count : $count")
}
}
현재 위 예시는 상태값이 내부에 존재하는 State Hoisting하지 않은 예시이다.
올바른 예시
@Composable
fun Counter(
count : Int,
onCountChange : (Int)-> Unit
) {
Button (onClick = { conCountChange(count+1)}) {
Text ("Count : $count")
}
}
→ 위 방법은, 상태가 외부에서 주입되고, 컴포저블이 UI에만 집중할 수 있도록 구성된다.
따라서 State Hoisting하게 작성된 예시라고 할 수 있다.
그렇다면, StateHoisting은 왜 필요할까?
왜 State Hoisting이 왜 필요할까?
State Hoisting의 장점
1. 재사용성에 좋다.
- UI 상태를 외부로 분리하면 같은 UI 컴포넌트를 다른 상태와 함께 재사용 가능하다.
- 만약에 SearchBar Component를 만들 경우, text와 onTextChange를 외부에서 주입 받을 때, 해당 컴포넌트는 검색 화면, 필터 검색, 채팅 검색 등.. 다양한 화면에서 UI로 활용 가능해 진다.
- ⇒ 즉 재사용성이 좋다.
2. 테스트 용이
- UI를 상태 없이 단순히 렌더링 테스트에 가능하게 만든다.
- ComposeTestRule.setContent를 활용해서 Compose UI Test를 할 때, 상태를 외부에서 주입 가능하게 만들기 때문에, 테스트를 명확하게 할 수 있다.
3. 단방향 데이터 흐름
- 상태는 위(부모)에서 아래(자식)으로만 흐르고, 변경은 콜백(이벤트)를 통해 위로 전달된다.
시각화된 구조
[ViewModel or StateHolder] -> [Compose UI] -> User Interaction -> [EventC Callback] -> [State Update] -> [Recomposition]
단방향 흐름이 중요한 이유
- 상태의 출처가 명확해져 디버깅이 쉬워진다.
- 양방향 데이터 바인딩보다 충돌이나 무한 루프의 가능성이 낮다 → 버그 감소
- UI와 상태 책임의 분리로 테스트가 간결해진다 → 테스트 용이
- 어떤 이벤트가 어떤 상태를 바꿨는지 추적이 가능하다 → 유지보수 쉬움
4. 상태 집중 관리
- 여러 개의 상태가 여러 컴포저블에 흩어져 있으면, 유지보수가 어렵다 → 중앙 집중화가 필요하다!
- ViewModel 등 외부의 한 곳에서 상태 관리가 가능해진다.
- 만약 ViewModel에서 상태를 모두 관리할 경우, ViewModel을 통해 상태에 접근하여 일관성있게 공유할 수 있다.
5. Compose의 철학과 부합하다.
- 선언형 UI의 원칙
- UI = f(State)
- UI는 현재 상태에 기반하여 그려지는 함수라고 한다.
- 즉 상태가 바뀌면, Compose는 recomposition을 자동으로 트리거해, UI를 새롭게 그린다.
명령형과 비교
fun imperativeMethod() {
textView.text = "Hello"
}
- 명령형은 우리가 직접 UI 업데이트 코드를 작성해야 한다.
선언형
fun Greeting(name :String){
Text("hello, $name")
}
- 선언형 UI에서는 상태가 바뀌면 자동 갱신되며, 명령없이 우리는 선언하면 된다.
- ⇒ 이 철학을 따르기 위해서는, UI외부에서 상태가 관리되어야 하며
- UI는 상태에만 반응하고, 자체적으로 상태를 들고 있어서는 안된다 !!
- ⇒ 이것이 즉 State Hoisting 이 필요한 이유.
State Holder 패턴이란?
앞서 살펴본 State Hoisting은 상태를 끌어올린다.라는 개념이라면,
StateHolder는 끌어올린 상태를 담는 별도의 클래스이다.
보통은 ViewModel 또는 @Composable 이 아닌 class에 mutableStateOf, derivedStateOf 등을담아서 관리한다.
State Holder 예시
class CounterState {
var count by mutableStateOf(0)
private set
fun increment(){
count++
}
}
이렇게 State를 선언한 클래스를 이후 컴포저블에서 아래와같이 사용한다.
@Composable
fun rememberCounterState(): CounterState {
return remember { CounterState() }
}
@Composable
fun CounterScreen() {
val state = rememberCounterState()
Counter(
count = state.count,
onCountChange = {state.increment() }
)
}
이렇게 되면, UI 상태와 로직이 분리되고, State는 StateHolder 클래스에 의해 관리된다.
StateHoisting + StateHolder
그렇다면 hoisitng 전략과 holder클래스를 결합한 예제는 이렇게 된다.
// State Holder Class
class LoginState {
var userName by mutableStateOf("")
var password by mutableStateOf("")
fun onUserNameChange(new : String){
userName = new
}
fun onPasswordChange(new : String) {
password = new
}
}
// State Holder Factory
@Composable
fun rememberLoginState() : LoginState {
return remember { LoginState() }
}
// State Hoisting, => Stateless UI
@Composable
fun LoginForm(
userName : String,
onUserNameChange : (String) -> Unit,
password : String,
onPasswordChange : (String) -> Unit,
onLoginClick() : () -> Unit,
) {
Column {
TextFeild(value = name, onValueChange = onUserNameChange, label = {Text("userName")})
}
}
@Composable
fun LoginScreen(){
val state = rememberLoginState()
LoginForm(
userName = state.userName,
onUserNameChange = state::onUserNameChange,
password = state.passwrod,
onPasswordChange = state::onPasswordChange
onLoginClick = {
Log.d("Login", "Login Click")
}
)
}
기존 Android 기술과의 비교
MVVM
- 상태 흐름 : ViewModel → View
- UI 연결 방식 : dataBinding / Observer
- 상태 변경 방식 : LiveData.postValue ..
Compose
- 상태흐름 : 단방향 (State → UI)
- UI 연결 방식 : 함수 파라미터로 전달
- 상태 변경 방식 : state update → recomposition
⇒ Compose는 가장 예측 가능하고, 함수형 패턴에 가까운 구조를 따른다.
State Holder 와 MVI …
각각을 이해하고 보니 그렇다면, MVI 패턴 역시 State Holder 개념을 적용한 아키텍처 방식인가?라는 생각이 들었다.
정답은 맞다고 한다.
MVI 패턴을 살펴보면 = State Hoisting + State Holder + Intent/Reducer 가 합쳐진 개념이다.
쉽게 비유로 이해
- State Hoisting : 부품을 재사용 가능하게 만드는 방법
- State Holder : 부품을 조립할 때 상태를 담아두는 케이스
- MVI : 전체 기계를 만드는 설계도 (부품 + 조립 방식 + 흐름 설계까지 모두 포함)
정리
State Holder, State Hoisting 은 UI 상태 관리를 위한 원칙 또는 구현 방식이고,
MVI는 이를 포함하여 앱 구조 전반을 설계한 아키텍처 패턴이라고 할 수 있다.
'Android > 공부' 카테고리의 다른 글
[Android] immutable 구조 State 알아보자 (2) | 2025.07.08 |
---|---|
[Android/공부] NDK(네이티브 개발 키트) 사용과 ABI 빌드 환경 톺아보기 (1) | 2024.11.14 |
[Android/공부] Compose로 UI 적용기 -1 (0) | 2024.10.14 |
[Android] MockWebServer란? : okhttp mockwebserver (0) | 2024.09.25 |
[Android] MockWebServer로 Mock API 활용하기 (2) | 2024.09.25 |