안드로이드 시스템·Compose 용어 사전
Compose읽기 지연 · 람다 전달

derivedStateOf

자주 바뀌는 입력에서 드물게 바뀌는 결과를 뽑아 리컴포지션 횟수를 줄이는 API.

자주 바뀌는 입력에서 드물게 바뀌는 결과를 뽑아 리컴포지션을 줄인다.

val listState = rememberLazyListState()

// ❌ 스크롤 1픽셀마다 리컴포지션
val showButton = listState.firstVisibleItemIndex > 0

// ✅ Boolean 이 바뀔 때만
val showButton by remember { derivedStateOf { listState.firstVisibleItemIndex > 0 } }
입력 (Int)0 → 1 → 2 → 3 → 4 → 5 ...초당 수십 번 바뀐다
출력 (Bool)false → true한 번만 바뀐다
  • 출력이 바뀔 때만 구독자를 무효화한다

언제 쓰지 않나

// ❌ 입력과 출력이 같은 빈도로 바뀐다 — 오버헤드만 늘어난다
val fullName by remember { derivedStateOf { "$first $last" } }

// ✅ 그냥 계산하거나 remember(key) 를 쓴다
val fullName = remember(first, last) { "$first $last" }

판별

  • "입력이 100번 바뀔 때 출력이 몇 번 바뀌나?"
    • 비슷하다 → derivedStateOf 는 손해다
    • 훨씬 적다 → 이득이다

읽기 지연 — 람다로 넘긴다

// ❌ 이 컴포저블이 offset 을 읽는다 → 스크롤마다 전체가 다시 그려진다
Box(Modifier.offset(y = scrollOffset.dp))

// ✅ 람다 버전은 그리기 단계에서만 읽는다 → 컴포지션을 건너뛴다
Box(Modifier.offset { IntOffset(0, scrollOffset) })

Compose 는 세 단계로 돈다

- 컴포지션(무엇을) → 레이아웃(어디에) → 드로잉(어떻게)

상태를 어느 단계에서 읽느냐가 비용을 가른다

- 컴포지션에서 읽으면 세 단계 전부 다시
- 드로잉에서 읽으면 그리기만 다시
  • graphicsLayer { alpha = a } · drawBehind { } 도 같은 원리다

상태 읽기 범위를 좁힌다

// ❌ 부모가 상태를 읽어 전체가 무효화된다
@Composable fun Screen(vm: VM) {
    val count = vm.count
    Header(); Body(); Footer(count)
}

// ✅ 필요한 곳만 읽는다
@Composable fun Screen(vm: VM) {
    Header(); Body(); Footer(count = { vm.count })
}

면접 함정

  • derivedStateOfremember 없이 쓴다 → 리컴포지션마다 새 파생 상태가 만들어져 아무 이득이 없다.
  • "모든 계산을 derivedStateOf로 감싸면 빨라진다" → 구독 관리 비용이 붙는다. 빈도 차이가 있을 때만 쓴다.

스크롤 성능 실측 예

1,000개 목록에서 '맨 위로' 버튼 표시 조건을 계산한다

  • 직접 읽기 — 스크롤 1초에 리컴포지션 약 90회 (프레임마다)
  • derivedStateOf — 같은 구간에서 2회 (false→true, true→false)

버튼 하나가 아니라 그 버튼을 감싼 스코프 전체가 다시 그려지므로 체감 차이가 크다

세 단계별 읽기 API

// 컴포지션에서 읽는다 — 가장 비싸다
Box(Modifier.padding(top = offset.dp))

// 레이아웃에서 읽는다
Box(Modifier.offset { IntOffset(0, offset) })

// 드로잉에서 읽는다
Box(Modifier.graphicsLayer { translationY = offset.toFloat(); alpha = a })
Box(Modifier.drawBehind { drawRect(color) })

애니메이션·제스처처럼 값이 프레임마다 바뀌면 반드시 람다 버전을 쓴다 — 이것만으로 잰크가 사라지는 경우가 많다

콜백을 안정적으로 넘긴다

// ❌ 매 리컴포지션마다 새 람다 → 하위가 skip 되지 않는다 (Strong Skipping 이전)
UserList(users, onClick = { id -> viewModel.select(id) })

// ✅ 메서드 참조는 안정적이다
UserList(users, onClick = viewModel::select)

함께 보면 좋은 용어

노트에서 맥락과 함께 보기 — Compose 성능·안정성 — recomposition·@Stable·skippable