
이제 Recomposition 시에도 remember { mutableStateOf<T> } 의 계산식을 재실행하기 위해서는 key 를 설정해줘야 한다는 것을 알았다.
다만, 한 가지 가정을 해보자.
이전처럼 단순한 데이터라면 상관 없겠지만, 계산식에 의해 새로운 상태가 파생되어야 하는 경우는 어떻게 할 것인가?
다음과 같은 코드가 있다고 해보자.
class MainActivity() : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
enableEdgeToEdge()
MaterialTheme {
Scaffold {
Column(modifier = Modifier
.fillMaxSize()
.padding(it)
) { DerivedStateOfTest() }
}
}
}
}
}
@Composable
fun DerivedStateOfTest(){
val listState = rememberLazyListState()
val page = remember(listState.firstVisibleItemIndex){
listState.firstVisibleItemIndex / 5
}
Text("현재 페이지 - ${page}")
LazyColumn(
modifier = Modifier.fillMaxSize(),
state = listState
) {
itemsIndexed(List(500){ it }){ i, _ ->
Text(text = "item $i")
}
}
}
이 코드는 LazyColumn 을 이용하여 하단 스크롤을 하는 경우 현재 보이는 첫 번째 Composable Index / 5 로 페이지를 나타낸다.
우리는 이전 시간에 UI 를 업데이트 하기 위해서는 remember 에 key 를 지정해줘야한다는 것을 알았다.
이제 이것을 실행해보자.

값이 잘 변경되는 것을 확인할 수 있다.
그렇다면 리컴포지션 횟수는 얼마나 될까?

Running Device - 오른쪽 상단의 네모 및 돋보기 모양 을 클릭하자.

이를 클릭하면 위와 같이 해당 화면에서 발생하는 Recomposition, 자식의 Recomposition 과 Skipped 을 확인할 수 있다.
Skipped 의 경우 Compose Runtime 이 UI 가 의존하는 데이터가 이전과 다름이 없다 판단하여 Recomposition 하지 않고 넘기는 기능이 존재하는데, 이것의 Count 이다.
이제 다시 화면을 스크롤하여 firstVisibleItemIndex 를 업데이트 해보자.

실제 계산은 listState.firstVisibleItemIndex / 5 로, 계산 결과가 바뀐 것은 2번만 발생하였음에도 DerivedStateOfTest Composable 은 15번의 불필요한 리컴포지션 과정을 거쳤다.
만약 이것이 수 천, 수 만번의 과정을 더 거친다면 앱이 버벅거리거나 심하면 크래쉬가 발생할 수 있는데, 이것이 발생한 이유는 다음과 같다.
@Composable
fun DerivedStateOfTest(){
val listState = rememberLazyListState()
val page = remember(listState.firstVisibleItemIndex) {
listState.firstVisibleItemIndex / 5
}
Text("현재 페이지 - ${page}")
LazyColumn(
modifier = Modifier.fillMaxSize(),
state = listState
) {
itemsIndexed(List(500){ it }){ i, _ ->
Text(text = "item $i")
}
}
}
위의 코드를 살펴보면 page 는 key 로 listState.firstVisibleItemIndex 를 읽어들이고 있다.
이것은 내부적으로 scrollPosition.index 를 가져오는데, 이 index 가 var index by mutableIntStateOf(initialIndex) 처럼 state 로 구현되어 있기 때문에 이를 읽는 가장 가까운 restartable (재시작가능한) Composable 에서 Recompostion 이 발생하는 것이다.
- remember 의 경우
inline function으로 이루어져 있기 때문에 컴파일 시 key 는 가장 가까운 스코프에서 선언한 것처럼 된다.- 이와 비슷하게
@NonRestartableComposable이라는 어노테이션이 있는데 비슷한 기능을 한다.
자, 그렇다면 우리는 이 불필요한 리컴포지션 과정을 어떻게 줄일 수 있을까?
이럴 때 사용하는 것이 DerivedStateOf 이다.
이를 간단히 설명하자면 다음과 같다.
다른 상태를 참조하여 새로운 상태를 파생하는 함수
내부 계산식에 State.value 가 존재하다면 이를 자동으로 추적한다.
단, 일반 파라미터로 받는 State.value 인 경우 이를 추적하지 못한다.!
A 라는 상태를 이용하여 B 라는 상태로 계산을 진행해야하는 경우, key 를 사용하게 된다면 불필요한 리컴포지션이 발생하거나 변수에 의도치 않은 데이터가 들어갈 수 있다.
기능을 알았으니 이를 한 번 사용해보도록 하자.
@Composable
fun DerivedStateOfTest(){
val listState = rememberLazyListState()
val page by remember {
derivedStateOf { listState.firstVisibleItemIndex / 5 }
}
Text("현재 페이지 - ${page}")
LazyColumn(
modifier = Modifier.fillMaxSize(),
state = listState
) {
itemsIndexed(List(500){ it }){ i, _ ->
Text(text = "item $i")
}
}
}

리컴포지션이 확실하게 줄어든 것을 확인할 수 있다 !