RecyclerView를 활용한 대용량 데이터셋 페이징
RecyclerView를 활용한 대용량 데이터셋 페이징
페이징(paging) 시스템은 대용량 데이터셋을 다룰 때, 데이터를 로드하고 화면에 표시하는 방식을 최적화하는 기법입니다. 작고 관리하기 쉬운 단위(chunk)로 나누어 데이터를 가져오기 때문에, 앱의 성능을 안정적으로 유지하면서 더 나은 사용자 경험을 제공할 수 있습니다. 모든 데이터를 한꺼번에 로드하면 메모리 소비가 급격히 증가하고, 초기 로딩 시간이 길어지며, 네트워크 대역폭도 불필요하게 낭비됩니다. 실제 프로덕션 환경에서는 수천, 수만 건의 데이터를 한 번에 가져오는 것이 거의 불가능에 가깝기 때문에, 페이징은 선택이 아닌 필수 패턴이라 할 수 있습니다.
이번 면접 질문을 통하여 아래 내용들을 학습하실 수 있습니다.
- 페이징 없이 대용량 데이터셋을 로드하면 왜 성능 문제가 발생하는지 설명할 수 있습니다.
RecyclerView의 스크롤 리스너를 활용하여 수동 페이징 시스템을 직접 구현할 수 있습니다.ListAdapter와DiffUtil이 어떻게 협력하여 리스트를 효율적으로 업데이트하는지 이해할 수 있습니다.- 수동 페이징과 Jetpack Paging 라이브러리를 각각 어떤 상황에서 사용해야 하는지 판단할 수 있습니다.
페이징이 중요한 이유
데이터를 작은 페이지 단위로 나누어 로드하면, 메모리 사용량이 크게 줄어들어 메모리 부족(Out of Memory) 오류를 사전에 방지할 수 있습니다. 현재 화면에 보이는 영역(viewport)에 필요한 데이터만 먼저 가져와서 렌더링하므로, 초기 로딩 속도도 대폭 빨라집니다. 또한, 추가 데이터는 사용자가 실제로 필요로 할 때만 요청하기 때문에 네트워크 사용량을 최소화할 수 있으며, 이는 네트워크 환경이 좋지 않은 상황에서 특히 큰 이점을 제공합니다.
사용자 경험 관점에서 보면, 페이징 시스템은 사용자가 리스트를 탐색하는 동안 동적으로 데이터를 로드하므로, 부드러운 스크롤을 구현할 수 있습니다. 무한 스크롤(infinite scrolling)이나 대규모 데이터 소스를 다루는 앱에서 이 방식을 적용하면, 디바이스나 네트워크에 과도한 부하를 주지 않으면서도 반응성이 뛰어난 인터페이스를 구현할 수 있습니다. 면접에서 이 주제가 나오면 단순히 "데이터를 나눠서 로드한다"는 수준을 넘어, 메모리, 네트워크, UX 세 가지 측면에서 구체적으로 설명하시면 좋은 인상을 남기실 수 있습니다.
DiffUtil을 활용한 어댑터 구성
첫 번째 단계는 ListAdapter를 사용하여 RecyclerView.Adapter를 만드는 것입니다. ListAdapter는 DiffUtil을 통해 리스트 비교(diff) 연산을 자동으로 처리하므로, 새로운 페이지가 추가될 때 전체 리스트를 새로고침하지 않아도 됩니다.
class PokedexAdapter :
ListAdapter<Pokemon, PokedexAdapter.PokedexViewHolder>(diffUtil) {
override fun onCreateViewHolder(
parent: ViewGroup, viewType: Int
): PokedexViewHolder {
val binding = ItemPokemonBinding.inflate(
LayoutInflater.from(parent.context)
)
return PokedexViewHolder(binding)
}
override fun onBindViewHolder(
holder: PokedexViewHolder, position: Int
) { /* bind data */ }
inner class PokedexViewHolder(
private val binding: ItemPokemonBinding
) : RecyclerView.ViewHolder(binding.root)
companion object {
private val diffUtil =
object : DiffUtil.ItemCallback<Pokemon>() {
// 같은 아이템인지 고유 식별자(name)로 판별
override fun areItemsTheSame(
oldItem: Pokemon, newItem: Pokemon
) = oldItem.name == newItem.name
// 아이템의 실제 내용이 동일한지 비교
override fun areContentsTheSame(
oldItem: Pokemon, newItem: Pokemon
) = oldItem == newItem
}
}
}