Android/Tech

LazyList 과 RecyclerView 의 메커니즘 알아보기

jyotti 2025. 1. 13. 16:24

Unsplash, Annie Spratt.

동기

'RecyclerView 와 LazyColumn 의 공통점과 차이점에 대해서 얘기해 주세요'

현재 재직 중인 기업의 면접 질문이었습니다. 당시에는 '다량의 데이터 셋을 효율적으로 표현하기 위한 컴포넌트이며, Android View SystemJetpack Compose 각각 나름의 최적화가 이루어져있어 비교적 적은 리소스를 소모한다는 공통점이 있습니다. 차이점은 잘 모르겠습니다.' 라고 대답했던 기억이 있습니다.

당시에 힌트를 주셨는데, 최적화 부분에 있어서 사소한 차이점이 있다고 하셨습니다. 결국 대답은 못했고요.

입사한지 한 달 정도 지난 시점에서, 이 질문에 대한 답변을 생각해보게 되었습니다.


RecyclerView

RecyclerView 는 Anndroid View System 기반의 UI 구현 시 높은 빈도로 사용되는 컴포넌트입니다. ViewHolder 를 통한 View 의 재활용이 적은 메모리로도 다량의 데이터 셋을 효과적으로 보여줄 수 있게 해줍니다.

 

RecyclerView 가 View 를 추가 및 표시하는 과정은 꽤나 복잡합니다. 그도 그럴 것이, RecyclerView 하나를 구현하기 위해 Adapter, ViewHolder, LayoutManager 에 항목 XML 까지 추가로 필요하니까요.

간단하게 도식화한 RecyclerView 의 메커니즘은 다음과 같습니다.

 

RecyclerView 항목 표시 메커니즘

 

간단하게 표현하고 싶었으나, 과정이 워낙 복잡하고 길었습니다. LayoutManager 를 통해 필요한 Position 을 찾고 이에 대한 ViewHolder 를 획득한 뒤, Adapter 에 획득한 Position ViewHolder 를 전달하여 항목을 바인딩, 바인딩된 ItemView 를 다시 LayoutManager 에게 전달하여 RecyclerView 에 표시하게 됩니다.

 

RecyclerView 최적화의 핵심은 Cache 와 RecycledPool 이라고 할 수 있습니다. 임시 보관된 ItemView 및 

ViewHolder 를 불러와서 별도의 과정 없이 빠르게 항목을 표시하도록 합니다.


LazyList

LazyList 는 RecyclerView 와 같은 UI를 Jetpack Compose 로 구현해야 하는 경우에 사용하기 적합한 Composable 입니다. RecyclerView 를 구현할 때와는 확연히 다른 부분이 있는데요. 별도로 Adapter 와 같은 것들이 더 이상 필요하지 않게 되었습니다. Jetpack Compose 를 처음 적용할 때, LazyList 를 사용하면서 '확실히 Jetpack Compose 가 Android View System 보다 생산성이 월등히 높구나' 를 느낄 수 있습니다.

 

Jetpack Compose 를 어느 정도 사용해 보신 분들은 직감적으로 알아차릴 수 있는데, LazyList 의 메커니즘은 Recomposition 에 기반하여 진행됩니다.

 

LazyList 사용 시, LazyListState 객체가 필수적으로 요구되는데, 이는 ScrollableState 의 서브 클래스입니다. 즉, LazyList 의 스크롤 상태를 관리하는 객체입니다.

 

@OptIn(ExperimentalFoundationApi::class)
@Stable
class LazyListState constructor(
    firstVisibleItemIndex: Int = 0,
    firstVisibleItemScrollOffset: Int = 0
) : ScrollableState {
	...
}

 

이는 LazyList 와 상호 작용 시, 유휴 시간동안 어떤 인덱스에 해당하는 Composable 을 미리 측정 및 구성할지를 제어합니다. 그 인덱스에 맞게 LazyList 가 구성되고, 각 아이템이 Recomposition 되는 메커니즘으로 동작합니다.

 

LazyListState 의 핵심은 layoutInfo 라는 이름의 LazyListLayoutInfo 객체입니다. 현재 표시되는 항목을 계산하는 데에 사용되며, 스크롤 또는 LazyList Composable 을 재측정할 때마다 업데이트 됩니다. LazyListLayoutInfo 객체는 다양한 필드를 가지고 있는데, 현재 표시되는 항목 정보, 뷰 포트 정보, 전체 항목 수, 방향, 레이아웃 반전 여부 등입니다.

 

layoutInfo 가 중요한 이유는, 유저의 스크롤에 따라 onScroll() 메서드가 호출된 뒤 해당 메서드 내에서 

notifyPrefetch() 메서드가 호출되는데, notifyPrefetch() 메서드에는 Prefetch 할 항목의 Index 가 참조되기 때문입니다.

 

private fun notifyPrefetch(delta: Float, layoutInfo: LazyListLayoutInfo = this.layoutInfo) {
    if (!prefetchingEnabled) {
        return
    }
    val info = layoutInfo
    if (info.visibleItemsInfo.isNotEmpty()) {
        val scrollingForward = delta < 0
        val indexToPrefetch = if (scrollingForward) {
            info.visibleItemsInfo.last().index + 1
        } else {
            info.visibleItemsInfo.first().index - 1
        }
        if (indexToPrefetch != this.indexToPrefetch &&
            indexToPrefetch in 0 until info.totalItemsCount
        ) {
            if (wasScrollingForward != scrollingForward) {
                // the scrolling direction has been changed which means the last prefetched
                // is not going to be reached anytime soon so it is safer to dispose it.
                // if this item is already visible it is safe to call the method anyway
                // as it will be no-op
                currentPrefetchHandle?.cancel()
            }
            this.wasScrollingForward = scrollingForward
            this.indexToPrefetch = indexToPrefetch
            currentPrefetchHandle = prefetchState.schedulePrefetch(
                indexToPrefetch, premeasureConstraints
            )
        }
    }
}

 

구한 Index 는 곧 LazyLayoutPrefetchState 의 schedulePrefetch 메서드 호출 시 사용되고, 해당 메서드에 의해 추후 추가될 가능성이 있는 항목에 대한 PremeasurePrecomposition 이 진행됩니다. 이후 화면에 보여질 때에는 Recomposition 이 진행되고요. 

 


정리하자면, RecyclerView 는 ViewHolder 를 통해 View 객체 자체를 재활용하고, LazyList 는 필요한 항목에 대해 Premeasure, Precompsition 이 진행된 뒤, 화면에 보일 때 필요한 부분만 Recomposition 되는 방식으로 진행됩니다.