안드로이드 아키텍처 용어 사전
상태 관리state hoisting · stateless

상태 호이스팅

상태를 컴포저블 밖으로 끌어올려 재사용·테스트 가능하게 만드는 패턴.

상태를 컴포저블 밖으로 끌어올리는 패턴. 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)

어디까지 올릴까

  • 너무 낮으면 — 형제 컴포저블이 그 값을 못 본다

  • 너무 높으면 — 최상위가 모든 상태를 알게 되고 파라미터가 폭발한다

  • 기준: 그 상태를 읽거나 쓰는 컴포저블들의 '가장 낮은 공통 조상'

    • — 그 이상 올리지 않는다

함께 보면 좋은 용어

노트에서 맥락과 함께 보기 — 상태 관리 — 호이스팅·SavedStateHandle·이벤트