
derivedStateOf는 Compose 성능 이야기에서 자주 등장합니다.
그래서 처음 배우면 마치 붙이기만 하면 빨라지는 마법처럼 보일 수 있습니다.

하지만 결론부터 말하면 derivedStateOf는 빠르게 바뀌는 원본 state를, UI가 실제로 필요로 하는 더 거친 파생 state로 바꿔 줄 때 의미가 큽니다.
즉 핵심은 계산 자체보다 어떤 변화가 recomposition을 일으켜야 하는가입니다.
왜 그냥 계산하면 아쉬울까
예를 들어 리스트를 스크롤할 때 맨 위로 가기 버튼을 보여줄지 말지만 결정하고 싶다고 해보겠습니다.
val listState = rememberLazyListState()
val showScrollTop = listState.firstVisibleItemIndex > 0문장은 단순합니다.
문제는 listState가 매우 자주 바뀐다는 점입니다.
사용자가 손가락을 조금만 움직여도 스크롤 관련 state는 계속 변합니다.
그런데 우리가 UI에서 정말 궁금한 것은 매 순간의 세밀한 스크롤 값이 아니라, 버튼을 보일지 말지입니다.
즉 원본 변화 단위와 UI 관심 단위가 다릅니다.
derivedStateOf가 하는 일
이럴 때 derivedStateOf는 파생 상태를 만듭니다.
val listState = rememberLazyListState()
val showScrollTop by remember {
derivedStateOf {
listState.firstVisibleItemIndex > 0
}
}이제 하위 UI는 스크롤 원본 전체를 직접 읽지 않고 showScrollTop만 봅니다.
그래서 관심 있는 변화가 더 분명해집니다.
firstVisibleItemIndex가 0일 때는 버튼 숨김- 0보다 커질 때만 버튼 표시
핵심은 모든 미세한 변화를 다 전달하는 것이 아니라, UI가 실제로 반응해야 하는 더 작은 결과만 남기는 것입니다.
이걸 memoization으로 이해하면 자꾸 헷갈린다
많은 분이 derivedStateOf를 remember와 비슷한 캐시 도구로만 이해합니다.
완전히 틀린 말은 아니지만, 그렇게만 보면 실전 판단이 어려워집니다.
차이를 단순하게 정리하면 이렇습니다.
remember: 계산을 다시 하지 않게 돕는 쪽derivedStateOf: 어떤 상태 변화가 recomposition을 일으킬지 더 거칠게 다듬는 쪽
물론 둘을 같이 쓰는 경우가 많습니다.
하지만 판단 기준은 다릅니다.
대표 예시: 스크롤 버튼
아래 예시는 공식 문서 맥락과도 잘 맞습니다.
@Composable
fun MessageList() {
val listState = rememberLazyListState()
val showScrollTop by remember {
derivedStateOf {
listState.firstVisibleItemIndex > 0
}
}
Box {
LazyColumn(state = listState) {
// ...
}
AnimatedVisibility(visible = showScrollTop) {
ScrollToTopButton()
}
}
}여기서 중요한 점은 showScrollTop이 원본 스크롤 state 전체를 그대로 노출하지 않는다는 점입니다.
버튼 입장에서는 Boolean 하나면 충분합니다.
그래서 파생 상태가 실제 UI 요구와 잘 맞습니다.
언제 특히 잘 맞을까
다음 패턴이면 검토할 가치가 큽니다.
- 원본 state가 자주 바뀐다
- UI는 그중 일부 변화에만 반응하면 된다
- 파생 결과가 원본보다 더 단순하다
예를 들면 이런 경우입니다.
- 스크롤 위치 전체 대신 상단 여부만 필요
- 텍스트 전체 대신 입력이 유효한지만 필요
- 복잡한 리스트 상태 대신 empty/loading/error 구분만 필요
언제 굳이 쓸 필요가 없을까
반대로 아래 경우에는 실익이 작을 수 있습니다.
- 값이 자주 바뀌지 않는다
- 파생 결과가 원본과 거의 같은 빈도로 바뀐다
- 단순한 계산일 뿐 recomposition 범위를 줄여 주지 못한다
예를 들어 이런 코드는 대개 굳이 derivedStateOf가 필요하지 않습니다.
val fullName = "${user.firstName} ${user.lastName}"이건 자주 바뀌는 스크롤 state를 거친 신호로 줄이는 상황이 아닙니다.
단순 계산이라면 오히려 derivedStateOf를 습관적으로 넣는 편이 더 읽기 어렵습니다.
remember만으로는 부족한 이유
가끔 이렇게 쓰면 되지 않나 생각할 수 있습니다.
val showScrollTop = remember(listState.firstVisibleItemIndex) {
listState.firstVisibleItemIndex > 0
}이 코드도 계산 결과는 만들 수 있습니다.
하지만 derivedStateOf가 주로 빛나는 지점은 Compose state를 읽는 파생 상태를 다루는 맥락입니다.
즉 스크롤처럼 자주 변하는 상태를 읽고, 그 결과를 다른 composable에 더 안정적인 형태로 전달할 때 의도가 더 잘 드러납니다.
흔한 실수
1) 성능 팁이라면 일단 붙인다
이건 가장 흔한 오해입니다.
병목이 없는 곳에 붙이면 체감 이득이 거의 없고, 코드만 더 복잡해질 수 있습니다.
2) 모든 계산을 derivedStateOf로 감싼다
파생 상태가 필요한 것과, 그냥 계산식 하나가 있는 것은 다릅니다.
3) 원본 변화와 UI 관심 변화가 같은데도 쓴다
예를 들어 원본 값이 바뀔 때마다 UI도 그대로 달라져야 하면, 굳이 중간 파생 상태를 둘 이유가 크지 않습니다.
빠른 판단 기준
아래 질문에 예가 많으면 derivedStateOf를 검토해 볼 만합니다.
- 원본 state가 매우 자주 바뀌는가?
- UI는 그 원본 전체가 아니라 더 단순한 결과만 필요로 하는가?
- 하위 composable에 더 안정적인 신호를 전달하고 싶은가?
반대로 아래에 가깝다면 먼저 단순한 코드가 낫습니다.
- 계산이 가볍다
- 변화 빈도가 낮다
- 파생 결과도 거의 같은 빈도로 바뀐다
정리
derivedStateOf는 “계산값을 만들어 주는 API”라고만 외우면 금방 남용하게 됩니다.
더 실전적인 이해는 이쪽입니다.
자주 바뀌는 원본 state를, UI가 실제로 반응해야 하는 더 작은 파생 state로 줄이는 도구입니다.
이 기준으로 보면 언제 써야 하고 언제 굳이 안 써도 되는지가 훨씬 선명해집니다.
관련해서 같이 보면 좋은 글은 Jetpack Compose 성능은 어디서 느려질까, remember와 rememberSaveable 차이, Modifier 순서는 왜 중요할까입니다.
관련해서는 Jetpack Compose 성능은 어디서 느려질까, remember와 rememberSaveable 차이도 함께 보면 판단 기준이 더 또렷해집니다.
공식 관점은 Android Developers Compose performance best practices 문서의 derivedStateOf 설명과 같이 보면 좋습니다.