상태를 컴포저블 밖으로 끌어올리는 패턴. Compose 상태 관리의 기본기다.
// ❌ 상태를 안에 가지면 (stateful)
@Composable
fun SearchBar() {
var text by remember { mutableStateOf("") }
TextField(value = text, onValueChange = { text = it })
}
// → 바깥에서 값을 알 수도, 바꿀 수도 없다. 테스트도 어렵다
// ✅ 끌어올리면 (stateless)
@Composable
fun SearchBar(text: String, onTextChange: (String) -> Unit) {
TextField(value = text, onValueChange = onTextChange)
}
-
규칙: 상태는 그것을 읽는 모든 컴포저블의 '가장 낮은 공통 조상' 에 둔다
-
value 는 내려보내고 (state down)
-
onValueChange 는 올려보낸다 (events up)
- 이것이 UDF 다
무엇을 얻나
- 재사용 — 같은 UI 를 다른 상태로 쓸 수 있다
- 테스트 — 상태를 주입해 모든 경우를 그려 볼 수 있다
- 단일 출처 — 상태가 한 곳에만 있다
- 미리보기 — @Preview 에서 원하는 상태를 그대로 넣는다
상태의 두 종류
-
UI 요소 상태 — 스크롤 위치 · 펼침/접힘 · 텍스트 필드 포커스
- 컴포저블 안에 remember 로 두면 된다
- ViewModel 로 올리면 오히려 번거롭다
-
화면 상태 — 화면이 그려야 할 데이터 · 로딩 · 에러
- ViewModel 에 둔다
-
판별: "화면 회전 후에도 서버에서 다시 받아야 하나?" → 화면 상태
두 종류의 죽음에서 살아남기
remember { } // 리컴포지션은 넘지만 구성 변경(회전)에서 사라진다
rememberSaveable { } // 구성 변경과 프로세스 종료까지 넘는다 (Bundle 저장)
ViewModel // 구성 변경은 넘지만 프로세스 종료는 못 넘는다
SavedStateHandle // 프로세스 종료까지 넘는다
리컴포지션 구성 변경 프로세스 종료
remember ✓ ✗ ✗
rememberSaveable ✓ ✓ ✓
ViewModel ✓ ✓ ✗
ViewModel + SavedStateHandle ✓ ✓ ✓
rememberSaveable은 Bundle에 저장되므로 작은 값만 넣는다 — 큰 리스트를 넣으면 TransactionTooLargeException이 난다.
// 커스텀 타입은 Saver 가 필요하다
val state = rememberSaveable(stateSaver = MySaver) { mutableStateOf(MyType()) }
// 또는 @Parcelize 를 붙인다
면접 함정
- ❌ "모든 상태를 ViewModel에 올려야 한다" → UI 요소 상태까지 올리면 불필요한 결합과 코드가 는다.
- ❌ "rememberSaveable이 remember의 상위 호환" → 직렬화 비용과 크기 제한이 있다. 필요한 것만 쓴다.
derivedStateOf — 계산 결과를 상태로
val listState = rememberLazyListState()
val showButton by remember {
derivedStateOf { listState.firstVisibleItemIndex > 0 }
}
derivedStateOf 없이 쓰면 스크롤 한 픽셀마다 firstVisibleItemIndex 를 읽어 리컴포지션이 돈다
derivedStateOf 를 쓰면
- 결과(Boolean) 가 바뀔 때만 리컴포지션이 돈다
- false → false 는 무시된다
조건: 자주 바뀌는 입력 → 드물게 바뀌는 출력, 일 때만 쓴다
콜백을 올릴 때 주의
// ❌ 람다가 매번 새로 만들어져 하위 컴포저블이 계속 다시 그려진다
UserList(users = state.users, onClick = { id -> viewModel.select(id) })
// ✅ 메서드 참조는 안정적이다
UserList(users = state.users, onClick = viewModel::select)
어디까지 올릴까
-
너무 낮으면 — 형제 컴포저블이 그 값을 못 본다
-
너무 높으면 — 최상위가 모든 상태를 알게 되고 파라미터가 폭발한다
-
기준: 그 상태를 읽거나 쓰는 컴포저블들의 '가장 낮은 공통 조상'
- — 그 이상 올리지 않는다