[Android] Ch2-3. 상태 호이스팅 (State Hoisting)
1. 개요 및 기본 형식
// 상태를 위로 끌어올림
@Composable
fun ParentComposable() {
var text by remember { mutableStateOf("") } // 부모가 State 소유
// State와 변경 함수를 자식에게 전달
ChildComposable(
text = text, // 현재 값 전달
onTextChange = { newText -> text = newText } // 변경 방법 전달
)
}
@Composable
fun ChildComposable(
text: String, // State 값만 받음 (읽기 전용)
onTextChange: (String) -> Unit // State 변경 함수
) {
TextField(
value = text,
onValueChange = onTextChange // 부모의 State 변경
)
}
- State를 사용하는 Composable에서 상위 Composable로 State를 올리는 패턴입니다.
- 자식 컴포넌트는 State 값과 변경 함수만 매개변수로 받아 동작합니다.
2. 주요 구성 요소
1) Stateful Composable (상태 보유)
내부에 자체적인 State를 직접 선언하고 가지고 있습니다.
특정 상태 로직에 묶여 있으므로 재사용 및 모의(Mock) 테스트가 어렵습니다.
@Composable fun StatefulCounter() { var count by remember { mutableStateOf(0) } // 내부 State 존재 Button(onClick = { count++ }) { Text("Count: $count") } }
2) Stateless Composable (상태 비보유)
- State 객체를 직접 소유하지 않고 상위에서 매개변수를 통해 데이터 및 이벤트를 주입받습니다.
- 구조가 단순하여 재사용과 테스트가 용이합니다.
@Composable fun StatelessCounter( count: Int, // 값만 받음 onIncrement: () -> Unit // 변경 로직만 받음 ) { Button(onClick = onIncrement) { Text("Count: $count") } }
3) 호이스팅의 장점
- 단일 진실 공급원 (Single Source of Truth)
- 상태 통제 체계가 한 곳에 집중되어 데이터 불일치 및 동기화 에러를 방지합니다.
- 재사용성
- Stateless 디자인은 서로 다른 UI 및 데이터를 주입받아 여러 곳에서 유연하게 재사용할 수 있습니다.
- 테스트 용이성
- 내부 상태 갱신 대신, 매개변수로 가상의 상태와 Mock 이벤트 핸들러를 주입할 수 있어 단위 테스트가 쉬워집니다.
- State 공유
- 부모의 단일 State를 여러 하위 자식 Composable들이 나누어 사용할 수 있습니다.
3. 활용 방식
1) 구조 개선 비교 (Before & After)
호이스팅 적용 전 (Stateful)
내부에 자체 State가 귀속되어 외부 동작 제어나 값 공유가 불가합니다.
@Composable
fun TodoInput() {
var text by remember { mutableStateOf("") }
TextField(
value = text,
onValueChange = { text = it }
)
Button(onClick = {
// 외부에서 해당 상태나 변수에 접근할 수 없습니다.
}) {
Text("추가")
}
}
호이스팅 적용 후 (Stateless 설계 및 병합)
부모 스코프에서 모든 상태를 관리하며 컴포넌트 간 정보 교환이 수행됩니다.
@Composable
fun TodoInputScreen() {
var text by remember { mutableStateOf("") } // 통합 State 1
var todos by remember { mutableStateOf(listOf<String>()) } // 통합 State 2
Column {
TodoInput(
text = text,
onTextChange = { text = it },
onAddClick = {
if (text.isNotEmpty()) {
todos = todos + text // 부모의 리스트 상태를 갱신
text = "" // 기존 입력 폼 상태의 동기적 초기화
}
}
)
TodoList(todos = todos) // 동일한 리스트 상태를 다른 컴포넌트와 공유
}
}
@Composable
fun TodoInput(
text: String,
onTextChange: (String) -> Unit,
onAddClick: () -> Unit
) {
Row {
TextField(
value = text,
onValueChange = onTextChange
)
Button(onClick = onAddClick) {
Text("추가")
}
}
}
2) 여러 하위 컴포넌트 간 State 공유
@Composable
fun SharedStateExample() {
var count by remember { mutableStateOf(0) } // 루트 공유 State
Column {
// 동일한 1개의 State를 두 자식이 셰어함
CounterDisplay(count = count) // 렌더링(뷰) 권한만 위임
CounterButtons(
count = count,
onIncrement = { count++ },
onDecrement = { count-- }
) // 값 수정 권한 위임
}
}
@Composable
fun CounterDisplay(count: Int) {
Text("현재 카운트: $count", fontSize = 24.sp)
}
@Composable
fun CounterButtons(
count: Int,
onIncrement: () -> Unit,
onDecrement: () -> Unit
) {
Row {
Button(onClick = onDecrement) { Text("-") }
Text("$count", modifier = Modifier.padding(horizontal = 16.dp))
Button(onClick = onIncrement) { Text("+") }
}
}
3) 복합 State 호이스팅 제어
@Composable
fun LoginScreen() {
var email by remember { mutableStateOf("") }
var password by remember { mutableStateOf("") }
var isLoading by remember { mutableStateOf(false) }
LoginForm(
email = email,
onEmailChange = { email = it },
password = password,
onPasswordChange = { password = it },
isLoading = isLoading,
onLoginClick = {
isLoading = true
// 실제 로그인 비즈니스 로직 처리 진행
}
)
}
@Composable
fun LoginForm(
email: String,
onEmailChange: (String) -> Unit,
password: String,
onPasswordChange: (String) -> Unit,
isLoading: Boolean,
onLoginClick: () -> Unit
) {
Column {
TextField(
value = email,
onValueChange = onEmailChange,
label = { Text("이메일") }
)
TextField(
value = password,
onValueChange = onPasswordChange,
label = { Text("비밀번호") },
visualTransformation = PasswordVisualTransformation()
)
Button(
onClick = onLoginClick,
enabled = !isLoading // 로딩 상태에 연동된 버튼 비활성화
) {
if (isLoading) {
Text("로그인 중...")
} else {
Text("로그인")
}
}
}
}
email,password,isLoading세 가지의 다채로운 UI 데이터를 한 곳(부모)에서 명확하게 파악하고 제어할 수 있습니다.
4. 호이스팅 적용 가이드라인 (규칙)
1) State의 적절한 위치 설정
- 원칙: 두 컴포넌트 이상이 공유해야 하는 정보라면, 해당 정보를 사용하는 가장 가까운 공통 부모(최저 공통 조상) 에 위치시켜야 합니다.
- 단일 컴포넌트 내부에서만 파편화되어 사용되는 상태의 경우 호이스팅 시 구조가 무의미하게 비대해지므로 내부 State 형태로 유지하십시오.
2) 파라미터 분할 방식
- 단순 렌더링 목적의 뷰 영역: 갱신 없이 읽기 전용으로 값(
value)만 하향 전달합니다. - 트리거 및 이벤트 수신 영역: 동작을 통제하는 변경 함수(
onValueChange)도 함께 전달합니다.
5. 부가 설명
네이밍 컨벤션
- 상태 변경 함수는 보통 파스칼케이스의 이벤트 기반 네이밍 룰(
onXxxChange,onXxxClick)을 따릅니다. - 예시:
onTextChange,onCountChange,onAddClick
'Tech > App-Android' 카테고리의 다른 글
| [Android].Ch2-5. Material Design 3 (0) | 2026.03.24 |
|---|---|
| [Android.]Ch2-4. remember vs rememberSaveable (0) | 2026.03.24 |
| [Android].Ch2-2.TextField (0) | 2026.03.23 |
| [Android].Ch2-1.LazyColumn & LazyRow (0) | 2026.03.23 |
| [Android].Ch1-5.이벤트 처리 (0) | 2026.03.20 |