RecyclerView의 이벤트 처리 방식 개선

송규빈·2024년 2월 15일
1
post-thumbnail

개요

컴포즈를 도입하지 않은 상태에서 리스트형 UI를 개발하다보면 보통 RecyclerView를 사용하게 됩니다.

리스트 UI를 업데이트할 때는 Adapter를 통해 요청하고, Adapter 내의 각 항목에서 이벤트가 발생하면 ViewHolder - Adapter - Activity/Fragment - ViewModel 순으로 전달하는 것이 일반적인 방법이죠.

저희 팀에서는 기존에 클릭 이벤트를 처리할 때 리스너를 전달하여 처리하거나, 람다를 전달하여 사용해 왔습니다.
그러나 문제점이 존재했습니다.

문제점

Adapter

class ItemAdapter(
    followingClickListener: FollowingClickListener,
    wishClickListener: WishClickListener,
    storeClickListener: StoreClickListener,
) : ListAdapter<Item, ViewHolder>(ItemDiffUtilCallback()) {

	private val itemViewClickBinder = ItemViewClickBinder(
        followingClickListener = followingClickListener,
        wishClickListener = wishClickListener,
        storeClickListener = storeClickListener,
    )

   private val viewHolderFactory = ItemViewHolderFactory(
        itemViewClickBinder
    ) 
    
    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
        return viewHolderFactory.createViewHolder(parent, viewType)
    }
}

Prop Drilling

중첩 리싸이클러뷰 등 컴포넌트 계층이 깊어질수록 Prop drilling을 피할 수 없다는 점이었습니다.

물론 ViewClickBinder를 둠으로써 어느정도 상쇄를 시켰지만, 그럼에도 ViewClickBinder를 매 계층마다 전달을 해야하는 문제가 있었습니다.

이런 현상이 지속되다보면 이벤트 관련 코드가 수정될 때마다 모든 계층의 코드에 영향이 가기 때문에 유지보수가 힘들어지고, 다른 개발자가 파악을 하려고 할 때 코드를 계속해서 따라가야하는 불편함이 있습니다.

또한, 중간 계층의 컴포넌트가 실제로 사용하지 않는 리스너 및 람다 등을 단순히 하위 컴포넌트로 전달하기 위해 가지고 있어야 하는 경우가 생김으로 메모리 사용의 비효율도 초래할 수 있습니다.

결합도 증가

위와 같은 형식으로 했을 때 보통 액티비티/프래그먼트에서 구현을 담당하게 되는데, 이로 인해 액티비티/프래그먼트와 어댑터의 커플링이 심해지게 되어 유지보수가 어려워집니다.

위의 문제를 해결하기 위해 여러 문서를 찾아보던 중 RecyclerView의 사용자 이벤트, RecyclerView로 전달하는 상태 객체에 lambda로 이벤트 처리 시 주의사항이라는 글을 보고 아이디어를 얻었습니다.

해당 글에서는 어댑터를 통해 리스너나 클릭 이벤트를 주입받는 방식이 아닌 상태 객체 즉, 리싸이클러뷰에 바인딩 될 아이템 객체로 람다를 전달하여 클릭 이벤트를 처리하는 방식에 대해 설명하고 있었습니다.

개선

data class ItemUiModel(
    val itemIdx: Long = 0L,
    val title: String = "",
    val price: Long = 0L,
    var isWish: Boolean = false,
) : WishEvent {
    override var onWishClicked: (Long, Boolean) -> Unit = { _, _ -> }
}

이렇게 UIModel을 활용하여 클릭 이벤트를 전달하도록 수정하였습니다.
생성자 매개변수가 아닌 클래스 본문에서 작성한 의도는 위 링크를 한 번 보시길 바랍니다.

위에서 제시한 상태 객체를 통한 이벤트 전달 방법에서 더 나아가 저희 팀에 좀 더 구색을 맞출 수 있게 WishEvent 등의 인터페이스를 활용하였는데요.

찜 클릭 이벤트와 같이 거의 모든 화면에서 공통으로 사용되는 이벤트들은 인터페이스를 두어서 각 UI Model들의 이벤트 전달 방식에 대한 통일성을 높였고, 이벤트 처리 매커니즘이 변경될 시를 대비하여 유연성과 확장성을 높였습니다.

private fun setWishClick(view: AppCompatImageView) {
        view.setOnClickListener {
            val item = adapter?.currentList?.getOrNull(bindingAdapterPosition)
                ?: return@setOnClickListener

            viewClickBinder.setWishClick(
                item.itemIdx,
                item.isWish,
                item.onWishClicked
            )
        }
    }

액티비티/프래그먼트에서 어댑터를 거쳐 뷰홀더까지 내려와 전달되는 클릭 이벤트를 등록하는 것이 아닌, 단순히 UI Model에서의 메서드를 등록하면 되는 것이죠.

(viewClickBinder는 다른 뷰홀더들과의 재사용성을 고려하기 위해 사용되었습니다.)

profile
🚀 상상을 좋아하는 개발자

0개의 댓글