(Android) RecyclerView 사용 방법

윤성현·2025년 1월 11일
post-thumbnail

RecyclerView 사용 방법

‼️ 해당 내용은 RecylcerView 톺아보기 에서 RecyclerView 사용 방법을 설명한 부분만을 따로 발췌한 글입니다.
RecyclerView 사용 방법에 초점을 맞춰, 사용하는 방법(단계)과 각 단계의 이유만을 다룬 글입니다.

7가지 단계와 그 이유

각 단계를 기계적으로 수행하다보면, 내가 왜 이런 작업을 하고 있는지 놓치게 됩니다.
적어도 내가 왜 이런 작업을 해야하는지, 각 순서가 왜 존재하는지 생각해봅시다!

1. 레이아웃 파일에서 RecyclerView를 선언한다.

RecyclerView는 위젯으로써 XML 레이아웃 파일에 선언되어야 화면의 구성 요소로 보여질 수 있습니다. RecyclerView를 사용하고자 하는 activity_~~.xml파일에서 RecyclerView가 보여질 위치와 크기(width & height)를 조정해야 합니다.

2. 한 항목이 어떻게 보여질 것인지 정의하는 xml파일을 만든다.

RecyclerView는 결국 "여러 데이터"를 특정 뷰 요소로 보여주는 위젯입니다. 한 덩어리의 데이터를 어떻게 배치할 것인지를 결정함으로써 한 항목이 어떻게 보여질 것인지에 대해서 정의할 수 있게 됩니다. 이는 뷰 요소양식(혹은 포맷)을 만들었다고도 표현할 수 있습니다.

예를 들면, 프로필 이미지는 좌측 상단에, 유저이름은 프로필 이미지 바로 우측에 두는 등의 "배치 방법"을 하나의 양식으로 만드는 과정이라고 생각하시면 됩니다. 한 항목에 대한 xml 파일을 제작하는 것으로 화면의 반복되는 뷰 요소의 양식을 만들게 된 것입니다.

일반적으로 item_~~.xml 이름의 파일을 만들어 각 항목의 레이아웃을 정의합니다.

3. 한 항목에 보여질 데이터를 저장하는 Data Class를 작성한다.

2번의 설명을 다시 보면 뷰 요소에는 어떤 덩어리 단위의 데이터가 필요하다는 것을 알 수 있습니다. 그리고 좀 더 자세히 읽어보면 그 데이터는 하나의 덩어리 형태로 이루어져야 한다는 것을 유추해볼 수 있습니다.

이제 우리는 그 하나의 덩어리 형태의 데이터를 만드는 작업을 할 것입니다. 위에서 만들었던 item_~~.xml 파일에 정의된 데이터 구조를 참고하여 필요한 필드를 가진 Data Class를 작성합니다. 예시로 댓글창에서 댓글 하나의 요소를 담는다면 아래와 비슷할 것입니다.

data class Comment(
	val id: Long,
	val profileUrl: String,
	val username: String,
	val content: String,
)

4. 하나의 항목을 나타내는 XML 파일과 데이터를 연결하는 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
	}
}

5. 리스트 형태의 데이터를 여러 개의 뷰 요소로 렌더링 할 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
    • 새로운 ViewHolder가 필요할 때 호출되는 함수입니다.
    • 화면에 보여질 아이템의 레이아웃을 inflate하고 ViewHolder를 생성합니다.
      • parent는 RecyclerView 자체를 의미하며, 이를 통해 context를 얻어올 수 있습니다.
      • viewType은 여러 종류의 아이템 뷰를 사용할 때 구분하기 위한 파라미터입니다.
    • 이 함수는 RecyclerView가 처음 그려질 때와 추가적인 ViewHolder가 필요하다고 판단되는 지점에서 호출됩니다. 다르게 말하면, 충분한 ViewHolder가 존재한다면 호출되지 않는 함수입니다.
  • onBindViewHolder
    • ViewHolder에 데이터를 바인딩할 때 호출되는 함수입니다.
    • position을 통해 현재 바인딩할 아이템의 위치를 알 수 있습니다.
      • 해당 position의 데이터를 ViewHolder의 bind 함수를 통해 뷰에 설정합니다.
      • 스크롤할 때마다 화면에 보이는 아이템들에 대해 이 함수가 호출됩니다.
  • getItemCount
    • RecyclerView가 표시할 전체 아이템의 개수를 반환하는 함수입니다.
    • 데이터 리스트의 크기를 반환하여 RecyclerView가 몇 개의 아이템을 표시해야 하는지 알려줍니다.
      • 이 값에 따라 RecyclerView는 스크롤의 범위를 결정합니다.
  • submitList (이 부분은 궁금하지 않다면 넘어가도 좋습니다)

    이 함수는 커스텀 함수로, 새로운 데이터 리스트를 설정할 때 사용합니다.
    이 커스텀 함수를 통해 두 가지 알아볼 것이 있습니다.

    1. 새로운 데이터 셋을 어떻게 보여줄 수 있을까요?

      현재 존재하는 데이터 셋에서 무언가 하나가 지워져야 하거나, 새로운 데이터 셋을 보여줘야하는 상황이라고 생각해봅시다. 이때 comments를 clear하고 addAll 함으로써 데이터 셋을 변경했다고 가정해봅시다. 그럼 보여지고 있는 RecyclerView의 화면이 바뀔까요? 아쉽게도 바뀌지 않습니다. 😢

      이는 데이터가 바뀌었다는 것을 RecyclerView에게 알려주지 않았기 때문입니다. RecyclerView는 데이터가 바뀌었다는 사실을 알 수 있는 방법이 없기 때문에, 우리가 직접 알려줘야 합니다.

      이를 위해 RecyclerView.Adapter 클래스는 데이터셋의 변경을 알리는 다양한 notify 함수들을 제공합니다.

      • notifyDataSetChanged(): 전체 데이터셋이 변경되었음을 알립니다. 모든 아이템을 다시 바인딩합니다.
      • notifyItemChanged(position): 특정 위치의 아이템이 변경되었음을 알립니다.
      • notifyItemInserted(position): 특정 위치에 새 아이템이 추가되었음을 알립니다.
      • notifyItemRemoved(position): 특정 위치의 아이템이 제거되었음을 알립니다.

      따라서 데이터셋을 변경한 후에는 반드시 적절한 notify 함수를 호출해야 UI가 업데이트됩니다.

    2. 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
      				    }
      		    }
          }
      }

6. RecyclerView에 만든 Adapter를 넣는다.

이제, 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를 넣어주고 있음을 알 수 있습니다.
		}
}

7. LayoutManager를 설정한다.

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
      )

0개의 댓글