본문 바로가기

Android/TroubleShooting

LazyColumn에서 부드러운 스크롤을 위한 트러블슈팅 🛠️

728x90
반응형

“Compose에서 스크롤이 미세하게 끊긴다면, 단순한 성능 문제가 아닐 수도 있다.”

 

 

1. 문제 상황 진단: 왜 LazyColumn이 느려졌을까?

LazyColumn은 RecyclerView와 같이 화면에 보이는 아이템만 Compose하고 Layout하는 지연 로딩(Lazy Loading) 메커니즘을 사용해 리소스 사용을 최소화한다. 그럼에도 불구하고 성능 문제가 발생하는 주된 원인은 다음과 같다.

1.1. 🔑 Key 미사용: 아이템 재활용 실패

LazyColumn은 스크롤 시 화면 밖으로 나간 아이템을 캐시에 저장하고 재활용하여 성능을 최적화한다.

하지만 아이템에 고유한 Key를 지정하지 않으면 리스트의 데이터가 변경될 때마다 Compose가 아이템의 상태 변화를 정확히 파악하지 못하고 불필요하게 전체 아이템을 다시 Recomposition하거나 스크롤 상태를 잃어버릴 수 있다.

 

1.2. 💰 과도한 Recomposition 유발: 비용이 큰 작업의 반복

아이템 내부에서 비용이 많이 드는 연산 (예: 정렬, 필터링, 복잡한 로직)을 수행하거나,

State를 너무 상위 Composable로 호이스팅(Hoisting)하여 리스트 전체를 자주 Recompose하면 성능이 저하된다.

 

1.3. 🧺 중첩 스크롤 문제 (Nested Scrolling)

LazyColumn 안에 같은 방향(수직)으로 스크롤 가능한 Column이나 다른 LazyColumn/LazyRow를 포함하는 경우에는 터치 이벤트 충돌로 인해 스크롤이 멈추거나 오작동을 일으킬 수 있다.

 


 

2. 핵심 트러블슈팅 및 성능 개선 방안

 

발생 가능한 문제에 대한 실질적인 해결책과 함께 필자가 경험한 코드를 제시하고자 한다.

 

2.1. 해결책 1: key 속성을 활용한 아이템 안정화 (Item Stabilization)

고유 식별자(Unique ID)를 Key로 제공하여 Compose Runtime이 데이터 변경 시 아이템을 효율적으로 추적하고 재활용하도록 한다. 이건 아이템의 State 보존에도 필수적이다.

📝 Bad Code (Key 없음)

data class MyItem(val id: String, val title: String)

@Composable
fun MyListScreen(items: List<MyItem>) {
    LazyColumn {
        // Warning: items() without key may cause performance issues
        items(items) { item ->
            MyItemCard(item) // 아이템이 업데이트되면 모든 것이 리컴포지션될 수 있음
        }
    }
}

Good Code (Key 사용)

data class MyItem(val id: String, val title: String) // id는 불변(val)이어야 함

@Composable
fun MyListScreen(items: List<MyItem>) {
    LazyColumn {
        // items()의 key 파라미터에 고유 ID를 제공
        items(
            items = items,
            key = { item -> item.id } // ✨ 각 아이템의 고유 ID를 key로 사용
        ) { item ->
            MyItemCard(item)
        }
    }
}

 

2.2. 해결책 2: rememberderivedStateOf를 사용한 불필요한 Recomposition 방지

LazyList 내부에서 비용이 큰 연산을 직접 수행하지 않고,
rememberderivedStateOf를 사용하여 연산 결과를 캐싱하고, 실제 필요한 경우에만 Recomposition이 발생하도록 제한한다.

📝 Bad Code (LazyColumn 내부에서 정렬 수행)

@Composable
fun SortedList(items: List<MyItem>) {
    LazyColumn {
        // DON'T DO THIS: 스크롤 할 때마다 (새 아이템이 들어올 때마다) 정렬이 반복됨
        items(items.sortedBy { it.name }) { contact -> 
            ItemRow(contact)
        }
    }
}

Good Code (LazyColumn 외부에서 정렬 및 캐싱)

@Composable
fun SortedList(items: List<MyItem>) {
    // 💡 items 리스트가 변경될 때만 정렬을 수행하도록 remember로 캐싱
    val sortedItems = remember(items) {
        items.sortedBy { it.name }
    }
    
    LazyColumn {
        items(sortedItems, key = { it.id }) { item ->
            ItemRow(item)
        }
    }
}

 

💡 팁: derivedStateOf를 사용한 리스트 상태 기반 계산 최적화

스크롤 상태에 따라 버튼 표시 여부 등 리스트의 상태에 의존하는 계산은 derivedStateOf를 사용하여 읽기 시점에만 상태 변화를 확인하도록 한다.

val listState = rememberLazyListState()

val showScrollToTopButton by remember {
    // 🚀 listState.firstVisibleItemIndex가 변경될 때마다 전체 Composable이 아닌 
    // 이 상태를 읽는 부분만 Recomposition을 유발하도록 제한
    derivedStateOf { listState.firstVisibleItemIndex > 0 }
}

LazyColumn(state = listState) { /* ... */ }

// showScrollToTopButton 상태에 따라 버튼 표시
AnimatedVisibility(visible = showScrollToTopButton) {
    ScrollToTopButton { 
        /* CoroutineScope을 사용하여 스크롤 */ 
    }
}

 

2.3. 해결책 3: 중첩 스크롤 문제 해결

LazyColumn 내부에 스크롤이 가능한 Column을 넣으면 안된다.

LazyColumn 자체에 스크롤 기능이 내장되어 있기 때문이다.

📝 Bad Code (중첩 스크롤)

LazyColumn {
    item {
        Column(
            modifier = Modifier
                .fillMaxWidth()
                .verticalScroll(rememberScrollState()) // ❌ LazyColumn 내부에 Scrollable Column
        ) { 
            /* ... Content ... */
        }
    }
    items(data) { /* ... */ }
}

Good Code (Modifier 제거 또는 item/items 블록 활용)

LazyColumn의 item{} 또는 items{} 블록을 활용하여 수직 레이아웃을 구성하면 된다.

만약 내부 컨텐츠의 높이가 고정되어 있다면 verticalScroll Modifier를 사용하지 말고 고정 높이를 지정하는 것이 좋다.

LazyColumn {
    // 1. item{} 블록을 사용하여 헤더 또는 고정 아이템 추가
    item { 
        HeaderComposable(modifier = Modifier.fillMaxWidth().height(100.dp)) 
    }
    
    // 2. items() 블록에 리스트 아이템 추가
    items(data, key = { it.id }) { item ->
        MyListItem(item)
    }
}

 

 


 

3. 결론

Jetpack Compose의 LazyColumn로 효율성을 극대화하기 위해서는 Compose의 동작 원리를 이해하고 key 속성을 필수적으로 사용해야 한다. 그리고 불필요한 Recomposition을 줄이기 위해 rememberderivedStateOf를 적절히 활용하는 것이 중요하다.

 


참고

 

Jetpack Compose의 LazyColumn 랙(Lag)을 제거하는 팁에 대한 튜토리얼 영상.

Top 3 Hacks to Remove LazyColumn Lag in Jetpack Compose

 

 

 

 

구독과 공감♡♡은 블로그 운영에 큰 힘이 됩니다!
긍정적인 댓글 남겨주시면 감사드리며,
보완해야 할 점이 있으면 댓글로 남겨주셔도 좋습니다!

728x90
반응형