컴포즈를 도입하지 않은 상태에서 리스트형 UI를 개발하다보면 보통 RecyclerView
를 사용하게 됩니다.
리스트 UI를 업데이트할 때는 Adapter
를 통해 요청하고, Adapter
내의 각 항목에서 이벤트가 발생하면 ViewHolder
- Adapter
- Activity/Fragment
- ViewModel
순으로 전달하는 것이 일반적인 방법이죠.
저희 팀에서는 기존에 클릭 이벤트를 처리할 때 리스너를 전달하여 처리하거나, 람다를 전달하여 사용해 왔습니다.
그러나 문제점이 존재했습니다.
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
을 피할 수 없다는 점이었습니다.
물론 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
는 다른 뷰홀더들과의 재사용성을 고려하기 위해 사용되었습니다.)