[Android] ListView와 Adapter, ViewHolder 패턴

Ju-unn·2026년 8월 26일

여러 개의 데이터를 리스트 형태로 화면에 보여줘야 할 때 쓰는 게 ListView다. 이번 글에서는 ListView의 기본 구성, 스크롤 성능을 위한 ViewHolder 패턴, 그리고 지금도 이걸 배워야 하는 이유를 정리한다.

ListView 구성 요소와 Adapter 만들기

ListView는 크게 세 요소로 이루어진다. 화면에 실제로 그려지는 View, 그 View 하나하나에 들어갈 데이터인 Item, 그리고 이 둘을 연결해주는 다리 역할을 하는 Adapter다.

ListView를 실제로 만드는 과정은 대략 이렇다.

  1. 레이아웃 XML에 ListView를 추가한다.
  2. 리스트의 아이템 한 개가 어떻게 생겼는지 정의하는 아이템 레이아웃 XML을 따로 만든다.
  3. 리스트에 뿌릴 실제 데이터를 준비한다 (리스트나 배열 형태).
  4. 데이터와 아이템 뷰를 연결해주는 커스텀 어댑터를 만든다.
  5. Activity(또는 Fragment)에서 이 어댑터를 ListView에 연결한다.

어댑터를 만들 때는 BaseAdapter를 상속받아서 네 개의 메서드를 구현해야 한다. getCount()는 전체 아이템 개수를 반환하고, getItem(position)은 특정 위치의 데이터를 반환하고, getItemId(position)은 해당 아이템의 고유 ID를 반환한다. 그리고 가장 중요한 getView(position, convertView, parent)가 실제로 화면에 그릴 View를 만들어서 반환하는 역할을 한다.

class MovieAdapter(
    private val items: List<Movie>
) : BaseAdapter() {
    override fun getCount(): Int = items.size
    override fun getItem(position: Int): Movie = items[position]
    override fun getItemId(position: Int): Long = position.toLong()
    override fun getView(position: Int, convertView: View?, parent: ViewGroup?): View? {
        // 여기서 아이템 뷰를 만들고 데이터를 채워 넣는다
    }
}

리스트의 특정 아이템이 클릭됐을 때 반응하고 싶다면 setOnItemClickListener로 콜백을 등록하면 된다. 사용자가 아이템을 클릭하는 이벤트가 감지된 이후에 실행할 로직을 이 콜백 안에 작성하는 구조다.

listView.setOnItemClickListener { _, _, position, _ ->
    // position번째 아이템이 클릭됐을 때 실행할 로직
}

ViewHolder 패턴과 findViewById 최적화

위에서 만든 getView()를 그대로 두면 문제가 생긴다. ListView는 스크롤될 때마다 화면에 보여야 할 아이템에 대해 getView()를 계속 호출하는데, 이 함수 안에서 매번 findViewById()로 TextView나 ImageView 같은 뷰를 새로 찾고 있다면 스크롤할 때마다 이 탐색 작업이 반복된다.

findViewById()가 왜 비용이 큰 작업인지부터 짚고 넘어가자. 이 메서드는 호출될 때마다 뷰 계층 구조 전체를 순회하면서 원하는 ID를 가진 뷰를 찾는다. 뷰 계층이 복잡할수록, 그리고 이 호출이 반복될수록 누적되는 비용이 커진다. 리스트 아이템 하나에 뷰가 서너 개만 있어도, 스크롤할 때마다 그 서너 개를 매번 처음부터 다시 찾는 셈이다.

이 문제를 해결하는 게 ViewHolder 패턴이다. 원리는 간단하다. "한 번 찾은 뷰는 다시 찾지 말고 어딘가에 저장해두고 재사용하자"는, 캐싱의 가장 기본적인 형태다. getView()가 처음 호출될 때만 findViewById()로 뷰들을 찾아서 ViewHolder 객체 안에 저장해두고, 이후 같은 아이템 뷰가 재사용될 때는 저장해둔 참조를 그대로 꺼내 쓴다.

이 재사용을 가능하게 해주는 재료가 convertView다. ListView는 아이템이 스크롤되어 화면 밖으로 나가면 그 뷰 인스턴스를 버리지 않고 보관해뒀다가, 새로운 아이템을 그려야 할 때 getView()의 convertView 매개변수로 다시 넘겨준다. 그러면 새 뷰를 처음부터 inflate하는 대신, 기존 뷰 껍데기를 그대로 재활용하면서 데이터만 새로 채워 넣을 수 있다. ViewHolder는 이 재활용된 뷰 껍데기 안에 들어 있는 자식 뷰들의 참조를 캐싱해두는 역할을 한다.

정리하면 이렇다. convertView가 "뷰 껍데기 자체를 재사용"하는 것이고, ViewHolder가 "그 껍데기 안의 자식 뷰 참조를 재사용"하는 것이다. 둘이 합쳐지면 스크롤 중 반복되는 findViewById() 호출을 사실상 없앨 수 있다.

ListView는 지금도 쓰나? - RecyclerView와의 관계

여기까지 읽으면 자연스럽게 이런 의문이 든다. "이거 요즘도 실무에서 쓰는 거 맞나요?" 애매하게 넘어가면 나중에 헷갈리니 분명히 짚고 가겠다.

결론부터 말하면, 새 프로젝트를 시작할 때는 ListView 대신 RecyclerView를 쓰는 게 사실상 표준이다. ListView가 API 문서에 "이제 쓰지 마세요"라고 명시적으로 박제되어 있는 건 아니지만, 공식 RecyclerView 가이드를 포함해 커뮤니티 전반에서 신규 개발 시에는 RecyclerView를 쓰도록 권장하고 있다. 실제로 ListView는 레거시 코드베이스나 아주 단순한 화면 정도에서만 여전히 보이는 수준이다.

그렇다면 왜 이미 대체된 걸로 취급받는 ListView와 Adapter/ViewHolder를 지금 배우고 있는 걸까. 이유는 RecyclerView가 등장한 배경 자체가 ViewHolder 패턴과 맞닿아 있기 때문이다. ListView 시절에는 방금 위에서 본 것처럼 ViewHolder 패턴 적용이 "선택 사항"이었다 — 개발자가 직접 신경 써서 구현해야 하는 최적화 기법이었고, 안 써도 일단 앱은 돌아갔다(다만 스크롤 성능이 나빴을 뿐이다). RecyclerView는 이 최적화 기법을 아예 필수 구조로 강제해버린 버전이다. RecyclerView를 쓸 때는 RecyclerView.ViewHolder를 상속하는 ViewHolder 클래스를 만드는 게 선택이 아니라 필수이고, 화면 밖으로 나간 아이템의 뷰를 파괴하지 않고 재사용(recycle)하는 것이 RecyclerView 이름 자체에 들어 있는 핵심 성능 개선 포인트다. (참고: RecyclerView 개요)

즉 이번 글에서 ListView를 배운 건 "옛날 기술이니까 알아두자"가 아니라 "RecyclerView가 왜 그렇게 설계됐는지 이해하기 위한 사전 지식"으로 보는 게 맞다. ListView의 한계(뷰 재사용을 개발자가 직접 신경 써야 하고, findViewById()를 매번 호출하는 게 기본값이었던 구조)를 먼저 이해하고 나면, RecyclerView가 왜 ViewHolder를 필수로 만들었는지가 바로 납득이 된다. 취업 준비 관점에서도 마찬가지다. 실무에서는 RecyclerView를 쓰겠지만, RecyclerView의 ViewHolder가 정확히 어떤 문제를 해결하기 위해 나온 개념인지 설명할 수 있어야 원리를 이해하고 쓰는 사람으로 보인다.

profile
한줄한줄 기록해보자

0개의 댓글