RecyclerView가 NestedSrolledView 속에 존재하면 발생하는 문제점

재원·2023년 7월 10일
6

Android

목록 보기
1/1
post-thumbnail

📚공부배경

나는 인생네컷 공유 보관 서비스 포포리의 Android 개발자로 있어서 인스타그램과 비슷한 뷰를 구현하는 개발하는 업무를 맡았다.

💡 해당 화면을 구현하기 위해서 아래의 조건을 만족하게 해야 했는데
  1. 상단 툴바와 바텀 네비게이션을 제외한 화면 전체가 스크롤이 되어야 한다.
  2. 제한된 숫자가 아닌 사용자가 가지고 있는 네컷사진 전부를 불러온다.

나는 아무 생각 없이

아이템이 같은 형식으로 여러 개 들어오니 RecyclerView를 사용하고 화면 전체가 스크롤 되어야 하니 스크롤 뷰를 사용해야겠다! 그런데 RecyclerView가 스크롤 될 수 있으니 RecyclerView는 스크롤을 막으면 되겠지?”

라고 생각을 하고 NestedScrollView 속에 RecyclerView를 넣고 RecyclerView의 스크롤을 막아서 구현을 해버렸다.

해당 뷰를 열심히 구현하고 팀 리더에게 검사를 맡으러 갔는데 결과는….


뒤지게 혼났습니다…

❓왜 사용하면 안 되는데?

결론부터 말을 하자면 내가 구현하려는 뷰에서는 RecyclerView의 본질을 살리기 힘들고 메모리 낭비가 너무 심해진다는 것이다.

why?

1. RecyclerView의 본질을 잊지말자

안드로이드 공식문서에서는 RecyclerView의 아래와 같이 정의하고 있다..

RecyclerView는 개별 요소를 재활용합니다. 항목이 스크롤 되어 화면에서 벗어나더라도 RecyclerView는 뷰를 제거하지 않습니다. 대신 RecyclerView는 화면에서 스크롤된 새 항목의 뷰를 재사용합니다. 이렇게 뷰를 재사용하면 앱의 응답성을 개선하고 전력 소모를 줄이기 때문에 성능이 개선됩니다

이 그림이 리사이클러뷰의 이해를 도울 수 있는데

ListView는 사용자가 스크롤 할 때마다 사라지는 뷰는 삭제되고 아래서 새로 등장하는 뷰는 생성되는 것을 반복하여 메모리 사용이 과도해지는 반면

RecyclerView는 화면에 보이는 정도의 View만 생성하고 스크롤 할 때마다 삭제하지 않고 객체를 이동시켜 재사용을 하여 메모리 사용을 줄일 수 있다.

요약하면 RecyclerView는 일반적으로 사용자가 스크롤할 때 필요한 아이템만 생성하고, 화면 밖으로 이동한 아이템은 재사용하여 메모리 사용량을 최적화를 하는 것이 본질이다.

2. NestedSrolledView의 특징

안드로이드 공식문서에서는 NestedSrolledView를 ScrollView와 거의 비슷하고 부모와 자식 모두 스크롤을 지원한다고 말하고 있다.

Nested scrolling이 기본값이 enable이기 때문에 이걸 비활성화시키면 단순하게 문제가 해결될 줄 알았다.

하지만 NestedScrollView는 스크롤이 가능한 모든 자식 View의 크기를 측정하려는 특징을 가지고 있다.

이 특징 때문에 일련의 과정을 진행하게 되는데 아래와 같다.

  1. NestedScrollView가 스크롤이 가능한 모든 자식 View의 크기를 측정하려고 시도한다
  2. 이 때문에 NestedScrollView 속의 RecyclerView의 레이아웃 매니저가 모든 아이템을 미리 생성한다.
  3. RecyclerView가 원래 가지고 있던 뷰 홀더의 재사용(Recycling)이라는 특성을 잃어버린다.

위에서 발생하는 문제점 때문에 내가 구현하려는 뷰 처럼 많은 아이템을 가진 RecyclerView를 사용할 때 메모리 문제가 발생할 수 있었던 것이다.

❗어떻게 해결했는데?

multi-view type을 사용하여 해결하였다.

기본적으로 RecyclerView는 단일 뷰 타입만 처리한다. 즉, 목록의 모든 아이템이 동일한 레이아웃을 가지는 것이다. 하지만 동일한 목록 내에서 서로 다른 레이아웃을 가진 아이템들을 표현해야 하는 경우에 multi-view type 기능을 사용할 수 있다.

즉, 뷰 타입을 정의하고 뷰 타입에 따라 다른 뷰 홀더를 생성하여 하나의 RecyclerView안에 서로 다른 뷰를 존재 할 수 있게 만드는 것이다.

나는 기존에 NestedSrolledView로 만든 뷰를 아래와 같이 설계하고 리펙토링을 진행하였다.

나는 뷰 타입을 Profile, Photo, Empty를 정의하였고 사용자가 사진 있으면 Photo, 없으면은 Empty가 보여지도록 하였다.

multi-view type 제작 과정

  1. 보여질 multi-view type을 정의한다.
companion object {
    const val VIEW_TYPE_PROFILE = 0
    const val VIEW_TYPE_PHOTO = 1
    const val VIEW_TYPE_EMPTY = 2
}
  1. 각 뷰 타입에 해당하는 ViewHolder Class를 정의한다.

  2. getItemViewType() 메소드를 만들어 각 아이템의 타입에 따라 해당하는 뷰 타입을 반환한다.

override fun getItemViewType(position: Int): Int {
    return when (getItem(position)) {
        is MyPageDisplayItem.Profile -> VIEW_TYPE_PROFILE
        is MyPageDisplayItem.Photo -> VIEW_TYPE_PHOTO
        is MyPageDisplayItem.Empty -> VIEW_TYPE_EMPTY
    }
}
  1. onCreateViewHolder() 메소드에서 각 뷰 타입에 해당하는 ViewHolder(2번에서 제작한)를 생성하고 반환한다.
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
    val context = parent.context
    return when (viewType) {
        VIEW_TYPE_PROFILE -> ProfileViewHolder(...)
        VIEW_TYPE_PHOTO -> PhotoViewHolder(...)
        VIEW_TYPE_EMPTY -> EmptyViewHolder(...)
        else -> throw IllegalArgumentException("Invalid view type")
    }
}
  1. onBindViewHolder() 메소드에서 각 뷰 타입에 해당하는 ViewHolder에 데이터를 바인딩한다.
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
    when (val item = getItem(position)) {
        is MyPageDisplayItem.Profile -> (holder as ProfileViewHolder).bind(item)
        is MyPageDisplayItem.Photo -> (holder as PhotoViewHolder).bind(item)
        else -> {}
    }
}

성공적으로 리펙토링을 진행한 화면의 모습 😆

마치며

짧은 시간 스프린트를 하며 서비스 제작에 들어가면 기능 구현에만 매몰되기 쉽다. 그럴수록 미리 설계를 진행해보며 내가 사용하려는 기술들이 서로 영향을 주지는 않는지, 메모리 관리 측면에서 효율적인지 판단하는 과정이 필요하다는 것을 느끼는 순간이었다.
이 글을 읽는 개발자들도 나와 같이 두 번 일을 해야 하는 순간이 오지를 않기를 바란다.

profile
Growth every day

2개의 댓글

comment-user-thumbnail
2023년 7월 10일

이해하기 너무 좋은 글이였습니다. 리사이클러뷰 마스터 화이팅!

답글 달기
comment-user-thumbnail
2024년 6월 13일

이미지도 같이 첨부하여 설명해주셔서 이해하기가 수월했습니다. 좋은글 올려주셔서 감사합니다.👍

답글 달기