들어가기에 앞서
본문은 Compose 프로젝트에 대한 ReComposition 최적화 학습 중 https://medium.com/@anandgaur2207/jetpack-compose-chapter-9-performance-optimization-in-compose-0a32b3733f44 을 번역해 정리한 글입니다. 의역이 다수 포함되어 있음
네이티브 안드로이드 UI를 위한 현대적인 툴킷인 Jetpack 컴포즈는 선언형 접근법으로 UI 개발을 단순화 할 수 있는 기능을 제공합니다.
그러나 다른 UI 툴킷과 같이, 컴포즈를 이용해 좋은 성능을 달성하기 위해선 내부 구조와 최적화 기술에 대한 이해가 필요합니다.
성능 최적화는 앱이 원활히 실행되고, 시스템 자원을 효율적으로 사용하며, 훌륭한 유저경험을 제공하도록 보장합니다.
만약 당신의 UI가 느리거나 랙이 걸린다면, 유저는 재빨리 그것을 알아챌 것이고 이는 좌절과 잠재적인 설치 삭제로 이어질 수 있습니다.
recompostion은 Jetpack Compose UI에서 상태가 변경되면 일부를 다시 그리는 프로세스입니다.
recompostion은 컴포즈에서 효율적이지만, 불필요한 Recompostion은 성능 병목을 유발할 수 있습니다.
컴포즈는 UI상태를 추적하며 상태의 변화가 일어날 시 자동적으로 recompose를 수행합니다.
하지만 recomposition은 실제 필요한 정도보다 더 자주 일어날 수 있습니다.
이것을 제어하기 위해선, 자주 변경되는 @Composable 함수에 너무 많은 로직을 넣지 마세요.
@Composable
fun Counter(count: Int) {
Text(text = "Count: $count")
}
@Composable
fun CounterScreen() {
var count by remember { mutableStateOf(0) }
// This button does not need to be recomposed every time the count changes
Button(onClick = { count++ }) {
Text("Increment")
}
// Only this text should recompose when count changes
Counter(count = count)
}
여기선 Button 함수와 Counter 함수가 분리되어 있으므로
counter라는 상태가 변경될 때마다 Counter 함수만이 recompose 되어 성능이 최적화 됩니다.
remember 키워드는 recomposition시 값을 다시 계산하지 않고도 이를 저장할 수 있도록 도와 성능을 향상 시킬 수 있습니다.
@Composable
fun ExpensiveOperation() {
// This computation is expensive
val result = remember { performExpensiveComputation() }
Text(text = result)
}
remember가 없다면, ExpensiveOperation 함수가 recompose 될때마다 고비용의 계산(performExpensiveComputation)이 일어날 것 입니다.
크기가 큰 리스트를 처리할때 LazyColumn 혹은 LazyRow를 사용하세요.
이 컴포넌트들은 눈에 보이는 아이템들만을 화면에 그려 메모리와 CPU 사용량을 최적화 합니다.
@Composable
fun UserList(users: List<String>) {
LazyColumn {
items(users) { user ->
Text(text = user)
}
}
}
위와 같이 크기가 큰 users 리스트를 처리해야 할때, LazyColumn은 오직 화면에 표시되는 user 아이템만이 구성되도록 보장하여 스크롤 성능을 향상시킵니다.
Modifier 역시 성능에 영향을 미칠 수 있습니다.
만약 Composable에 너무 많은 불필요한 modifier를 적용한다면, 더 복잡한 레이아웃 작업을 발생시킬 수 있습니다.
@Composable
fun OptimizedBox() {
Box(
modifier = Modifier
.fillMaxSize() // Avoid stacking modifiers unnecessarily
.background(Color.Blue)
) {
Text(text = "Hello, World!", color = Color.White)
}
}
크기, 패딩, 배경 등 레이아웃에 영향을 미치는 여러 Modifier를 필요와 관계없이 추가하지 마세요.
불필요한 레이아웃 계산이 발생할 수 있습니다.
Column, Row, Box와 같은 레이아웃은 강력한 기능을 제공합니다.
그러나 너무 많은 레이아웃을 nested하게 사용한다면 성능 문제가 발생할 수 있습니다.
레이아웃 계층을 가능한 평면화하세요
@Composable
fun SimpleLayout() {
Column(modifier = Modifier.fillMaxSize()) {
Text("Title")
Spacer(modifier = Modifier.height(8.dp))
Text("Description")
}
}
간단한 레이아웃은 컴포즈의 레이아웃 엔진의 작업을 줄여 렌더링 성능을 향상시킬 수 있습니다.
특정 상태에 따라 고비용의 계산이 발생하는 경우가 있다면,
derivedStateOf 키워드를 사용해 계산결과를 메모이제이션 함으로써 recomposition을 최적화할 수 있습니다.
@Composable
fun ExpensiveCalculation(input: Int) {
val result by remember {
derivedStateOf { performExpensiveCalculation(input) }
}
Text("Result: $result")
}
derivedStateOf 는 input이 변경될때만 performExpensiveCalculation(input) 계산이 실행되도록 보장합니다.
리스트나 함수 같이 stable하지 않은 (역주: 컴포즈에서의 stable/unstable 상태를 가리킵니다) 오브젝트를 Composable 함수에 넘길때 불필요한 recompostion이 발생할 수 있습니다.
이 때 remember 키워드를 사용해 recompostion 사이에 오브젝트를 stable 하게 만들 수 있습니다.
@Composable
fun StableList() {
val names = remember { listOf("Alice", "Bob", "Charlie") }
LazyColumn {
items(names) { name ->
Text(name)
}
}
}
remember가 없다면, 리스트는 매 recompostion 마다 재생성되어 성능 감소를 야기할 수 있습니다.
Jetpack Compose에서 최적의 성능을 보장하기 위한 몇 가지 모범 사례는 다음과 같습니다.
불필요한 recompostion을 방지하기 위해 자주 변화하는 상태를 부모 컴포저블에 배치하지 마세요.
변경이 발생하는 상태를 분리해야 합니다.
recompostion 중에 재계산을 방지하기 위해 고비용의 작업이나 UI 구성 요소를 remember를 이용해 기억하세요
derivedStateOf를 사용하여 종속 상태를 최적화합니다.
Composable에 필요한 수정자만 적용합니다.
패딩, 크기 또는 배경과 같은 수정자를 과도하게 사용하면 레이아웃 계층구조가 불필요하게 복잡해질 수 있습니다.
레이아웃 계층 구조를 얕게 유지합니다.
과도하게 계층화된 Column, Row, Box는 렌더링 속도를 저하시킵니다.
큰 데이터셋을 다룰 땐 LazyColumn, LazyRow, LazyGrid 을 사용하세요.
긴 리스트에 대해 Column이나 Row를 사용하는 것은 자제하세요. 아이템 수가 많을 때 성능 문제를 일으킬 수 있습니다.
Jetpack Compose는 효율적인 UI 툴킷입니다.
그러나 다른 UI 프레임워크와 마찬가지로 성능 최적화를 위해선 내부 구조를 잘 이해해야 합니다.
Recompostion을 제어하고, 큰 리스트에 LazyColumn을 사용하고, remember와 derivedStateOf 키워드를 잘 활용하고, 레이아웃을 간소화한다면 앱이 원할하게 실행되도록 할 수 있습니다.