RecyclerView는 안드로이드 개발자가 리스트 형태의 UI를 만들 때 자주 사용하는 컴포넌트입니다. 이름에 적혀있듯 재활용을 한다고 언뜻 알고는 있지만, 어떤 원리로 재활용이 되고 있는지 조금 더 깊게 파보면서 RecyclerView에 대해 알아가봅시다!
글에 들어가기에 앞서, 저와 RecyclerView는 꽤나 연이 깊습니다. 제가 안드로이드에 관심을 가지고 개발을 시작했을 때 처음 장벽을 느꼈던 부분이 RecyclerView였죠. 그 때는 혼자 학습을 했었는데, 혼자여서 그랬는지 이 부분을 학습하는 데 꽤나 어려웠던 기억이 납니다. 바로 다음학기에 모바일 프로그래밍 수업을 수강했었는데, 코로나 이슈로 인한 온라인 수업 + RecyclerView의 높은 난이도 두 가지의 여파로 많은 학우들이 수업을 드랍하겠다는 사람이 많았습니다.
저는 제가 처음 장벽을 느꼈던 부분에서 학우들이 포기하게 되는 상황이 아쉽다고 생각했고, 이에 RecyclerView를 설명하는 영상을 만들었습니다. (덧붙여 모바일 개발이라는 분야가 꽤나 재밌는데 학우들이 포기하지 않고 저처럼 흥미를 느끼면 좋겠다고 생각했었습니다. 😁) 처음에는 과제를 그대로 풀어버리는 영상을 만들어서 물의를 약간 빚었지만… 이후에는 유사한 과제를 따로 만들어서 설명하는 영상을 만들었고, 적지 않은 학생들에게 도움을 주었습니다. 영상 뿐만 아니라 질의응답방을 만들어서 잠시나마 질문을 올리는 카톡방도 따로 운영했었죠.
어떻게 보면 제가 안드로이드에 흥미를 가지게 된, 그리고 어떻게 보면 개발 분야에서의 첫번째 딥다이브 경험이 RecyclerView가 아니었을까 싶습니다. 이번에는 전반적인 원리를 살펴보며 조금 더 깊게 딥다이브 해보려는 취지에서 이번 글을 작성하게 되었습니다.
안드로이드 환경에서 여러개의 아이템을 스크롤하는 화면을 만들고 싶을 때 어떻게 구성하면 좋을지 알아봅시다!
RecyclerView는 안드로이드에서 리스트나 그리드와 같은 스크롤 가능한 UI를 구현하기 위해 사용되는 뷰 그룹(ViewGroup)입니다.
RecyclerView는 Android 5.0(Lollipop)에서 도입되었으며, 이전에 사용되던 ListView와 GridView를 대체하는 더 성능이 뛰어나고 확장 가능한 구성 요소입니다. 이를 통해 대량의 데이터를 효율적으로 표시하고 관리할 수 있습니다.
Android 초창기에는 목록을 구현하기 위해 ListView를 사용했지만, ListView는 다음과 같은 단점들이 있었습니다.
convertView를 사용해 뷰를 재활용했지만, 이 메커니즘은 뷰타입의 다양성을 제대로 처리하지 못했습니다.notifyDataSetChanged() 메서드를 호출해 전체 목록을 다시 갱신해야 했습니다.ObjectAnimator나 AnimatorSet과 같은 도구를 사용해 커스텀 애니메이션을 직접 구현해야 했습니다.RecyclerView는 이러한 문제를 해결하기 위해 만들어졌습니다. RecyclerView는 재활용 메커니즘을 더 체계화하고, 모듈화된 구성 요소(예: Adapter, LayoutManager)를 도입하여 확장성을 높였습니다.
재활용 메커니즘
RecyclerView는 화면에 표시되는 뷰(View)만 생성하고, 화면에서 사라지면 해당 뷰를 재활용합니다. 이를 통해 메모리 사용을 줄이고 성능을 최적화합니다. 스크롤 속도가 빠르고, 대량의 데이터를 처리하는 애플리케이션에서도 안정적으로 작동합니다.
모듈화된 구성 요소
RecyclerView는 핵심 기능을 모듈화된 구성 요소로 나누어 개발자가 필요에 따라 조합하고 확장할 수 있게 설계되었습니다. 이 구조는 확장성과 유지보수를 용이하게 만듭니다.
findViewById() 호출을 최소화하고, 아이템 뷰가 재활용될 때 데이터만 업데이트하도록 설계되었습니다.유연한 애니메이션 및 전환 지원
RecyclerView는 ItemAnimator를 통해 기본 애니메이션(아이템 추가, 삭제, 이동)을 지원합니다. 또한, 커스텀 애니메이션을 쉽게 추가할 수 있습니다.
데코레이터(Decorator) 지원
RecyclerView는 ItemDecoration 클래스를 통해 아이템 간의 구분선, 여백, 배경 등을 손쉽게 추가할 수 있습니다. 이러한 기능은 코드를 간소화하고, UI 디자인 작업을 더 직관적으로 만듭니다.
안드로이드 초기부터 사용되었던 ListView와 안드로이드 5.0에서 등장한 RecyclerView는 어떤 차이가 있는지 비교해봅시다!
ListView vs RecyclerView| 특징 | ListView | RecyclerView |
|---|---|---|
| 재사용 메커니즘 | 제한적이며 뷰타입 관리가 어렵다 | ViewHolder를 사용한 체계적 재활용 |
| 레이아웃 관리 | 단순한 세로 레이아웃만 지원 | LayoutManager를 통한 유연성 |
| 애니메이션 지원 | 제공하지 않음 | 기본 애니메이션 및 커스텀 지원 |
| UI 전환 | 지원하지 않음 | LayoutManager 변경으로 전환 가능 |
| 데이터 변경 처리 | 전체 갱신 필요 | 특정 항목만 갱신 가능 |
| 확장성 | 제한적 | 높은 확장성 |
| 성능 | 대량 데이터 처리에 비효율적 | 최적화된 성능 |
ListView에 대한 안드로이드 공식문서에서는 다음과 같이 말하고 있습니다.
For a more modern, flexible, and performant approach to displaying lists, use
RecyclerView.
리스트를 표시하는 데 있어 더욱 현대적이고 유연하며 성능 좋은 방식을 원하시면RecyclerView를 사용하세요.
각 단계를 기계적으로 수행하다보면, 내가 왜 이런 작업을 하고 있는지 놓치게 됩니다.
적어도 내가 왜 이런 작업을 해야하는지, 각 순서가 왜 존재하는지 생각해봅시다!
RecyclerView를 선언한다.RecyclerView는 위젯으로써 XML 레이아웃 파일에 선언되어야 화면의 구성 요소로 보여질 수 있습니다. RecyclerView를 사용하고자 하는 activity_~~.xml파일에서 RecyclerView가 보여질 위치와 크기(width & height)를 조정해야 합니다.
RecyclerView는 결국 "여러 데이터"를 특정 뷰 요소로 보여주는 위젯입니다. 한 덩어리의 데이터를 어떻게 배치할 것인지를 결정함으로써 한 항목이 어떻게 보여질 것인지에 대해서 정의할 수 있게 됩니다. 이는 뷰 요소의 양식(혹은 포맷)을 만들었다고도 표현할 수 있습니다.
예를 들면, 프로필 이미지는 좌측 상단에, 유저이름은 프로필 이미지 바로 우측에 두는 등의 "배치 방법"을 하나의 양식으로 만드는 과정이라고 생각하시면 됩니다. 한 항목에 대한 xml 파일을 제작하는 것으로 화면의 반복되는 뷰 요소의 양식을 만들게 된 것입니다.
일반적으로 item_~~.xml 이름의 파일을 만들어 각 항목의 레이아웃을 정의합니다.
2번의 설명을 다시 보면 뷰 요소에는 어떤 덩어리 단위의 데이터가 필요하다는 것을 알 수 있습니다. 그리고 좀 더 자세히 읽어보면 그 데이터는 하나의 덩어리 형태로 이루어져야 한다는 것을 유추해볼 수 있습니다.
이제 우리는 그 하나의 덩어리 형태의 데이터를 만드는 작업을 할 것입니다. 위에서 만들었던 item_~~.xml 파일에 정의된 데이터 구조를 참고하여 필요한 필드를 가진 Data Class를 작성합니다. 예시로 댓글창에서 댓글 하나의 요소를 담는다면 아래와 비슷할 것입니다.
data class Comment(
val id: Long,
val profileUrl: String,
val username: String,
val content: String,
)
ViewHolder를 작성한다.‘갑자기 뷰홀더가 왜 필요하지?’라는 궁금증이 생길 수 있습니다. 하지만 잘 생각해보면 우리는 지금 1~3번의 과정에서 한 항목의 뷰 요소와 데이터 덩어리만 만들었습니다. 뷰 요소와 데이터 덩어리 두 가지가 연결되지 않는다면, 서로 관계 없는 데이터 조각들에 불과합니다.
뷰홀더는 하나의 뷰 요소에 대해서 데이터를 어떻게 넣어줄 것인지 정의하는 곳입니다. 자세하게 설명하면 어떤 TextView에 어떤 데이터 값을 넣을 것인지, 어떤 ImageView에는 어떤 값을 넣을 것인지 결정함으로써 데이터와 뷰 요소를 연결하게 됩니다.
그렇기 때문에 ViewHolder는 일반적으로 bind 함수만 가지고 있습니다. 위에서 보여드렸던 것처럼 댓글창을 예시로 한다면 다음과 비슷한 코드가 나올 것입니다.
// ViewBinding 사용시
class CommentViewHolder(
private val binding: ItemCommentBindning,
) : RecyclerView.ViewHolder(binding.root) {
fun bind(comment: Comment) {
binding.tvUsername.text = comment.username
binding.tvContent.text = comment.content
// 이미지는 생략합니다.
}
}
// ViewBinding 미 사용시
class CommentViewHolder(
itemView: View,
) : RecyclerView.ViewHolder(itemView) {
private val tvUsername: TextView = itemView.findViewById(R.id.tvUsername)
private val tvContent: TextView = itemView.findViewById(R.id.tvContent)
fun bind(comment: Comment) {
tvUsername.text = comment.username
tvContent.text = comment.content
}
}
Adapter를 작성한다.RecyclerView를 사용하려고 했다면, 당신은 아마도 하나의 데이터를 보여주는 것보다는 컬렉션 형태(높은 확률로 List)의 데이터를 보여주고 싶었을 것입니다. 4번까지의 과정에서 우리는 하나의 뷰요소에 하나의 데이터를 연결했는데, 아직 여러개의 데이터를 연결하지는 못했습니다. Adapter를 통해서 컬렉션 형태의 데이터가 어떻게 여러개의 뷰 요소로 보여지는지 살펴보도록 하겠습니다.
Adapter에서 필수적으로 구현해야하는 세 가지의 override 함수와 필수는 아니지만 하나의 추가적인 일반 함수를 살펴보면서 어댑터가 어떤 일을 하고, 왜 필요한지 알아보도록 하겠습니다. 위에서 예시를 들었던 것과 같이 계속해서 댓글창에 대한 예시로 진행해보겠습니다.
class CommentAdapter(
private val comments: MutableList<Comment>
// 추가적인 설명을 위해 MutableList를 사용하지만, 가급적 List를 사용하세요.
): RecyclerView.Adapter<CommentViewHolder>() {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): CommentViewHolder {
val binding = ItemCommentBinding.inflate(LayoutInflater.from(parent.context), parent, false)
return CommentViewHolder(binding)
}
override fun onBindViewHolder(holder: CommentViewHolder, position: Int) {
holder.bind(comments[position])
}
override fun getItemCount(): Int = comments.size
// 데이터를 변경하는 커스텀 함수
fun submitList(newComments: List<Comment>) {
comments.clear()
comments.addAll(newComments)
notifyDataSetChanged()
}
}
onCreateViewHolder
onBindViewHolder
getItemCount
submitList (이 부분은 궁금하지 않다면 넘어가도 좋습니다)
이 함수는 커스텀 함수로, 새로운 데이터 리스트를 설정할 때 사용합니다.
이 커스텀 함수를 통해 두 가지 알아볼 것이 있습니다.
새로운 데이터 셋을 어떻게 보여줄 수 있을까요?
현재 존재하는 데이터 셋에서 무언가 하나가 지워져야 하거나, 새로운 데이터 셋을 보여줘야하는 상황이라고 생각해봅시다. 이때 comments를 clear하고 addAll 함으로써 데이터 셋을 변경했다고 가정해봅시다. 그럼 보여지고 있는 RecyclerView의 화면이 바뀔까요? 아쉽게도 바뀌지 않습니다. 😢
이는 데이터가 바뀌었다는 것을 RecyclerView에게 알려주지 않았기 때문입니다. RecyclerView는 데이터가 바뀌었다는 사실을 알 수 있는 방법이 없기 때문에, 우리가 직접 알려줘야 합니다.
이를 위해 RecyclerView.Adapter 클래스는 데이터셋의 변경을 알리는 다양한 notify 함수들을 제공합니다.
따라서 데이터셋을 변경한 후에는 반드시 적절한 notify 함수를 호출해야 UI가 업데이트됩니다.
ListAdapter를 활용하면 좋습니다.
사실 submitList는 ListAdapter 클래스의 메서드입니다. 따라서 ListAdapter를 사용하면 submitList 함수를 override해서 구현할 수 있습니다. (자세한 사항은 AsyncListDiffer.java의 submitList를 참고해보시면 좋습니다.)
데이터 셋이 변경되는 일은 꽤 자주 일어납니다. 그럴때마다 notifyDataSetChanged()를 사용하면 전체 뷰를 다시 그리기 때문에 성능상으로도 좋지 않으며, 다시 그려짐으로 인해서 화면 또한 한 번 깜빡거리게 됩니다. 이를 최적화하기 위해 데이터를 비교하는 기준인 DiffUtil을 만들고 ListAdapter의 인자로 넣어주면, submitList를 호출할 때마다 이전 리스트와 새로운 리스트의 차이를 자동으로 계산하여 효율적으로 업데이트를 하게 됩니다.
덤으로 notify 함수에서 position 값을 넣어주면서 변경 지점까지 알려줬던 것과 다르게, 그저 리스트를 전달함으로써 변경점을 신경쓰지 않고도 업데이트를 할 수 있는 편리성까지 얻을 수 있게 됩니다.
ListAdapter를 사용한 예시 코드는 다음과 같습니다.
class CommentListAdapter : ListAdapter<Comment, CommentViewHolder>(DiffUtil()) {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): CommentViewHolder {
// ViewHolder 생성 로직
}
override fun onBindViewHolder(holder: CommentViewHolder, position: Int) {
holder.bind(getItem(position))
}
override fun submitList(list: List<Musical>) {
super.submitList(list)
}
companion object {
val DiffUtil = object: DiffUtil.ItemCallback<Comment>() {
override fun areItemsTheSame(oldItem: Comment, newItem: Comment): Boolean {
return oldItem.id == newItem.id
}
override fun areContentsTheSame(oldItem: Comment, newItem: Comment): Boolean {
return oldItem == newItem
}
}
}
}
이제, RecyclerView를 사용할 준비가 끝났으며, 사용하기만 하면 됩니다. RecyclerView를 보여줄 Activity 클래스로 이동해서, 해당 RecyclerView의 adapter 속성에 지금까지 만든 Adapter를 넣어주면 됩니다. (이때, 직접 만든 Adapter가 입력 파라미터를 가지고 있다면 선언할 때 같이 넣어주면 됩니다.)
RecyclerView.java파일을 보면 setAdapter라는 함수를 통해서 어댑터를 설정할 수 있다는 것을 알 수 있습니다. 이 함수를 통해 직접 만든 adapter를 넣어주면 됩니다.
class CommentActivity: AppCompatActivity() {
private val binding by lazy { ActivityMainBinding.inflate(layoutInflater) }
private val comments = listOf<Comment> ( ... )
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(binding.root)
binding.rvComments.adapter = CommentAdapter(comments)
// RecyclerView 의 mAdapter에 직접 접근하는 것처럼 보이지만,
// 코드를 타고 들어가거나 역컴파일을 해보면
// RecyclerView의 setAdapter 함수를 통해서 adapter를 넣어주고 있음을 알 수 있습니다.
}
}
6번까지의 과정을 따라왔는데 RecyclerView가 제대로 보이지 않는다면, LayoutManager 속성을 설정하지 않았을 확률이 높습니다. LayoutManager는 RecyclerView의 아이템들을 어떤 방식으로 배치하고 스크롤할지를 결정하는 핵심 컴포넌트입니다. 이는 각 항목의 위치 지정, 레이아웃의 방향, 재사용될 뷰의 정책 등을 관리합니다.
LayoutManager 없이는 RecyclerView가 어떻게 아이템을 배치해야 할지 알 수 없기 때문에, RecyclerView 를 사용할 때는 반드시 (RecyclerView를 선언한) xml 파일이나 Activity 클래스 파일 중 한 곳에서 LayoutManager 속성을 설정해줘야 합니다. (일반적인 수직 스크롤에서는 LinearLayoutManager를 넣어주면 됩니다.)
// activity_comment.xml
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/rv_comment"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
.../>
// CommentActivity
override fun onCreate(savedInstanceState: Bundle?) {
// 이외 코드
binding.rvComments.adapter = CommentAdapter()
binding.rvComments.layoutManager = androidx.recyclerview.widget.LinearLayoutManager(this)
}
LinearLayoutManager: 가장 기본적인 레이아웃 매니저로, 아이템을 수직 또는 수평으로 배치
수직 스크롤: 일반적인 리스트 형태 (기본값)
수평 스크롤: LinearLayoutManager(context, LinearLayoutManager.HORIZONTAL, false)
GridLayoutManager: 아이템을 그리드 형태로 배치
spanCount로 한 행/열에 들어갈 아이템 수를 지정
갤러리나 상품 목록 등에 적합
binding.recyclerView.layoutManager = GridLayoutManager(context, spanCount = 2)
StaggeredGridLayoutManager: 다양한 크기의 아이템을 그리드 형태로 배치
아이템의 크기가 불규칙할 때 공간을 효율적으로 활용
binding.recyclerView.layoutManager = StaggeredGridLayoutManager(
spanCount = 2,
StaggeredGridLayoutManager.VERTICAL
)

RecyclerView의 뷰 재활용 메커니즘은 다음과 같은 원리로 작동합니다.
onAttachedToRecyclerView : 어댑터가 RecyclerView에 붙었을 때 최초 한 번 호출된다.onCreateViewHolder : 새로운 뷰홀더를 생성할 때 호출된다.onBindViewHolder : 뷰홀더에 데이터 바인딩 시 호출된다.onViewAttachedToWindow : 뷰홀더가 화면에 보일 때 호출된다.onViewDetachedFromWindow : 뷰홀더가 화면에서 완전히 사라질 때 호출된다.onViewRecycled : 해당 뷰홀더를 재활용하기 직전에 호출된다.onDetachedFromRecyclerView : 어댑터가 RecyclerView에서 제거되었을 때 마지막 한 번 호출된다.위에서 살펴본 오버라이드 함수에 다음과 같이 코드를 추가하면 로그를 살펴볼 수 있습니다!
‼️ 아래와 같이 로그를 찍게 되면, “함수가 호출된 횟수”를 보게 됩니다. 정확히 몇 번째의 아이템의 생명주기를 보는 것과는 조금 다릅니다. 대신, 위로만 스크롤한다면
onBindViewHolder의 로그에 찍힌 숫자와 가리키는 아이템의 위치(몇 번째 아이템인지)는 동일하게 됩니다.
class CommentAdapter : RecyclerView.Adapter<CommentViewHolder>() {
val counts = MutableList(7) { 0 } // 각 함수 호출 횟수를 저장
override fun onAttachedToRecyclerView(recyclerView: RecyclerView) {
super.onAttachedToRecyclerView(recyclerView)
println("onAttachedToRecyclerView: ${++counts[0]}")
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): CommentViewHolder {
println("onAttachedToRecyclerView: ${++counts[1]}")
return CommentViewHolder(/* ... */)
}
override fun onBindViewHolder(holder: CommentViewHolder, position: Int) {
println("onBindViewHolder: ${++counts[2]}")
}
// 기타 함수들도 동일하게 사용
// Logcat에서 System.out으로 필터를 설정하면 println으로 찍은 로그들만 따로 모아볼 수 있습니다.
}
RecyclerView가 초기화된 화면에서 이루어지는 동작들을 살펴보자!

RecylcerView가 보여지는 순간 맨 처음에는 onAttachedToRecyclerView가 호출됩니다. 그 이유는, 처음으로 어댑터가 RecyclerView에 붙은 시점이기 때문입니다.
각 뷰홀더는 “생성” → “바인딩” → “뷰 그룹에 붙임” 순서를 거쳐 화면에 보이게 됩니다.
그렇기 때문에 onCreateViewHolder → onBindViewHolder → onViewAttachedToWindow 순서로 메서드가 호출되는 것을 볼 수 있습니다.
추가적으로 살펴볼 수 있는 것은, 한 번에 여러 개의 뷰홀더를 생성하거나 한 번에 여러 개를 바인딩하는 것이 아니라 순서대로 생성하고 바인딩하는 것을 볼 수 있습니다. 이를 통해 RecyclerView는 뷰 요소(항목)가 필요한 시점에 대해서 순차적으로 뷰홀더를 채워나가는 방식을 사용하고 있음을 알 수 있습니다.
스크롤을 조금 내려보면서 함수 호출을 들여다보자!

7번째 뷰홀더가 생성되고 attached 된 후에, 가장 상단에 있던 아이템(뷰홀더)가 화면에서 완전히 사라지면서 onViewDetachedFromWindow함수가 호출된 것을 확인할 수 있습니다.
11번째 onBindViewHolder를 보면, 11번째 onCreateViewHolder가 호출되지 않았음에도 bind가 진행되고 있음을 볼 수 있습니다. 하지만, onCreateViewHolder 대신 onViewRecycled가 처음 호출된 것을 같이 확인할 수 있는데, 이를 통해 더 이상 뷰홀더가 직접 생성되는 것이 아니라 만들어진 뷰홀더가 재활용되고 있음을 알 수 있습니다.
9번째 뷰부터 발생하는데, onCreateViewHolder와 onBindViewHolder 이후에 바로 onViewAttachedToWindow가 오지 않을 수 있다는 것을 볼 수 있습니다. 이는 미리 뷰홀더를 준비해두고 필요한 시점에 attach하는 “프리페칭(Prefetching)”을 하고 있음을 볼 수 있습니다. 마찬가지로 onCreateViewHolder가 호출되었다고 바로 onBindViewHolder가 호출되는 것 역시 아닙니다. 2번에서 소개했던 세 종류의 메서드는 각자의 역할에 따라서 호출되기 때문에, 순서는 유지되지만 항상 함께 호출되는 것은 아닙니다.
RecyclerView의 동작 원리를 이해하려면 위에서 말했던 LayoutManager, Adapter 그리고 이제부터 설명할 Cache영역과 Recycled Pool에 대해서 알아야합니다. 뷰홀더의 생성과 재활용은 이 다섯 가지 컴포넌트들의 상호작용으로 이루어집니다.

RecyclerView가 처음 화면에 그려질 때는 다음과 같은 순서로 진행됩니다.
getItemViewType()을 통해 각 position의 뷰 타입을 결정합니다.새로운 ViewHolder가 필요할 때는 다음과 같은 단계를 거칩니다. 각 단계에서 필요한 ViewHolder를 찾았다면 더 찾지 않고 해당 단계를 마무리합니다. (return 한다고 생각하시면 됩니다.)
Cache 확인
RecycledViewPool 확인
getItemViewType(position)의 반환값을 기준으로 검색합니다.새로운 ViewHolder 생성
onCreateViewHolder()를 호출하여 새로 생성합니다.
ViewHolder가 화면에서 사라질 때 어떻게 작동하는지 과정과 원리를 살펴봅시다!
사용자가 RecyclerView를 스크롤하면, 이미 화면에 있던 아이템 일부가 사라지고 새로운 아이템이 화면에 등장합니다. 이때 사라지는 ViewHolder는 아래 순서로 처리됩니다.
onViewDetachedFromWindow() 콜백 호출
mCachedViews(캐시)에 우선 저장
recyclerView.setItemViewCacheSize(size))mCachedViews가 가득 차면 RecycledViewPool(풀)로 이동
RecyclerView 내부에서 ViewHolder는 크게 세 가지 상태를 거치며 이동합니다.


재활용이 어떻게 되고 있는지, 성능이 정말 최적화 되는지 그 방법과 결과를 확인해봅시다. RecyclerView가 재활용이 되었을 때와 재활용이 되지 않을 때를 비교해봅시다.
‼️ 편의상 ListView와 비교하며 성능을 보는데, ListView도 convertView를 활용하면 뷰를 재사용할 수 있습니다. 이 챕터에서는 “재활용의 성능”에 초점을 맞추고자 재활용하지 않는 ListView와 비교하고 있습니다.
재활용이 되고 있는지 간단하게 확인할 수 있는 방법은 로그를 찍어보는 방법입니다. RecyclerView는 자동적으로 뷰를 재활용하기 때문에 ListView와 비교해보겠습니다.
1. ListView의 경우
class CommentListViewAdapter(...): BaseAdapter() {
var counts = 0
override fun getView(
position: Int,
convertView: View?,
viewGroup: ViewGroup?,
): View {
val view: View = LayoutInflater.from(context).inflate(R.layout.item_comment, null).also {
println("ListView - getView: ${counts++}") // Layout이 inflate 될 때마다 출력
}
// ...
}
}
ListView의 어댑터는 일반적으로 BaseAdapter를 사용하는데, 뷰를 생성하는 로직이 있는 getView()에서 뷰 생성시마다 counts와 함께 한줄의 출력을 작성합니다. 그리고 스크롤을 계속 아래로 내리면 아래와 같이 계속해서 뷰가 생성된다는 것을 볼 수 있습니다.

RecyclerView의 경우
class CommentRecyclerAdapter(...) : RecyclerView.Adapter<CommentViewHolder>() {
val counts = 0
override fun onCreateViewHolder(...): CommentViewHolder {
println("onCreateViewHolder: ${counts++}")
// binding 후 뷰 return 로직
}
}
RecyclerView는 위에서 설명한 대로 어댑터의 onCreateViewHolder가 호출되며 뷰가 생성됩니다. 따라서 onCreateViewHolder메서드에서 뷰가 생성될 때마다 로그를 찍어보면 뷰가 계속 생성되는지 여부를 알 수 있습니다. 아래 이미지를 보면, ListView와는 달리 초기에 스크롤 할 때 몇 개의 뷰가 더 생성되고, 이후에는 스크롤을 해도 새로운 뷰를 생성하지 않고 기존 뷰를 재활용하는 것을 확인할 수 있습니다.

Profiler를 활용하면, 현재 내가 개발하고 있는 앱이 메모리를 얼마나 사용하고 있는지 등의 상태를 실시간으로 확인해볼 수 있으며, 그래프로 보여주기 때문에 변화의 추이도 확인할 수 있습니다.
ListView의 경우

ListView의 경우 초기에는 102MB 정도의 메모리를 사용하고 있지만, 스크롤을 계속해서 내리다보면 메모리 사용량이 약 172MB까지 지속적으로 증가하는 것을 확인할 수 있습니다. 이는 새로운 뷰가 계속해서 생성되면서 메모리를 더 많이 소비하게 되기 때문입니다. 스크롤을 더 내릴수록 메모리 사용량은 그만큼 증가하게 됩니다.
RecyclerView의 경우

RecyclerView의 경우에는 초기 메모리 사용량이 약 75MB에서 시작하여, 스크롤을 계속 내려도 메모리 사용량이 크게 증가하지 않고 초기와 비슷한 정도로 유지되는 것을 확인할 수 있습니다. 이는 새로운 뷰를 생성하지 않고 기존 뷰를 재활용함으로써 메모리를 효율적으로 사용하기 때문입니다. 이를 통해 RecyclerView의 재활용 메커니즘이 실제로 메모리 효율성 측면에서 큰 이점을 제공한다는 것을 확인할 수 있습니다. 스크롤을 더 내려도 메모리 사용량에는 큰 변화가 없으며, 빠른 스크롤 시에는 잠깐 메모리 사용량이 올라갈 수 있지만 곧 다시 초기와 비슷해집니다.
RecyclerView를 사용할 때, 종종 “자동으로 성능이 최적화된다”는 막연한 믿음만으로 재활용의 원리에 대해서는 큰 신경을 쓰지 않는 경우가 많습니다.(제가 지금까지 그래왔습니다.. 😅) 그러나 실제로는 RecyclerView 내부에서 지금까지 살펴본 여러 원리가 정교하게 맞물려 작동하고 있으며, 이로 인해 개발자가 직접 신경 쓰지 않아도 재활용이 매끄럽게 이루어지는 것이었습니다.
이 글을 통해 RecyclerView의 내부 동작 원리를 조금이나마 이해하셨기를 바라며, 다음 글에서 새로운 주제로 다시 찾아뵙겠습니다!
어떤 동작 원리에 대해서 깊게 파고 들어가본 적은 처음인데, 여전히 파고 들어갈 수록 알아야할 내용은 끝이 없다는 것을 알게 되었습니다. 다만, 이번의 글쓰기 덕분에 처음으로 java 파일까지 파헤쳐보며 동작 원리를 살펴보며 꽤 즐거운 시간이 되었습니다. 덤으로 자주 보던(혹은 보였던) 글의 작성자를 다시 보며 더 많은 주제에 관심을 가져보게 된 것 같습니다. 처음이라 미숙한 글이지만, 누군가에게는 도움이 되면 좋겠습니다 😄
RecyclerView Deep Dive - 1. RecyclerView 정의와 동작원리 및 생명주기
RecyclerView Deep Dive with Google I/O 2016
RecyclerView: Highly Optimized Collections for Android Apps