ListView는 세로 리스트를 표시하기 위한 기본 Android 컴포넌트로, API 1(Android 1.0)부터 사용할 수 있습니다. ListView는 ArrayAdapter를 사용하여 항목 리스트를 만들고 사용자 정의 셀 레이아웃을 지원합니다. ListView가 무엇인지, 구성 방법, 그리고 이 오래되었지만 여전히 사용되는 컴포넌트가 숨기고 있는 함정을 알아보겠습니다. IT Sectr에서는 레거시 프로젝트에서 ListView를 접하며 Android 4.4와의 하위 호환성을 유지하고 있습니다. 최신 대안과 비교하려면 RecyclerView에 관한 문서를 읽어보세요.
주요 내용
ListView는 android.widget 패키지의 ViewGroup으로, 스크롤 가능한 항목 리스트를 표시하도록 설계되었습니다. ListView는 AdapterView 패턴을 구현합니다. 데이터는 어댑터(Adapter)를 통해 전달되며, 데이터 항목을 화면에 표시되는 View로 변환합니다. ListView는 첫 번째 버전(API 1)부터 Android SDK에 존재하며 추가 라이브러리가 필요하지 않습니다.
아키텍처는 RecyclerView보다 간단합니다. LayoutManager가 내장되어 있고(세로 스크롤만 가능), 변경 애니메이션이 없으며, 구분선은 XML 속성 android:divider로 설정됩니다. ListView는 자동으로 OverScrollView와 포커스 강조를 추가하여 프로토타이핑을 단순화합니다. Statista(2025)에 따르면, Play Store 상위 100개 Android 앱 중 약 15%가 여전히 ListView를 포함하고 있습니다 — 대부분 최소한의 지원을 받는 레거시 프로젝트입니다.
ListView는 2008년부터 2014년까지 Android의 주요 리스트 컴포넌트였으며, 2014년 Google I/O에서 RecyclerView가 발표되었습니다. 부분적으로 사용 중단(soft-deprecated)되었음에도 불구하고, ListView는 모든 Android 버전에서 계속 사용할 수 있으며 SDK API 35에서 @Deprecated로 표시되지 않았습니다. 프로젝트가 Android 4.4(API 19) 이하를 지원하는 경우, RecyclerView에는 AndroidX 및 Gradle 종속성이 필요하므로 ListView가 유일한 내장 선택입니다.
ArrayAdapter는 ListView에서 가장 인기 있는 어댑터입니다. List<T>와 각 항목의 레이아웃을 받습니다. 기본적으로 ArrayAdapter는 각 객체에서 toString()을 호출하고 simple_list_item_1(단일 TextView)에 텍스트를 표시합니다. 사용자 정의 표시를 위해서는 getView()를 재정의하거나 생성자에 사용자 정의 레이아웃을 전달해야 합니다.
// ArrayAdapter로 ListView(간단한 텍스트)
class MainActivity : AppCompatActivity() {
private lateinit var binding: ActivityMainBinding
private val countries = listOf(
"러시아", "미국", "독일",
"프랑스", "일본", "중국", "브라질"
)
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)
val adapter = ArrayAdapter<String>(
this,
android.R.layout.simple_list_item_1,
countries
)
binding.listView.adapter = adapter
// 클릭 처리
binding.listView.onItemClickListener =
AdapterView.OnItemClickListener { _, _, position, _ ->
Toast.makeText(this, countries[position], Toast.LENGTH_SHORT).show()
}
}
}
SimpleAdapter는 Map 데이터의 리스트용으로 설계되었습니다. List<Map<String, Any>>와 Map 키를 레이아웃의 위젯 ID에 바인딩하기 위한 from/to 배열을 받습니다. SimpleAdapter는 별도의 어댑터 클래스가 필요 없는 빠른 프로토타입에 편리합니다. 단점은 타입 안전성 부족과 사용자 정의 로직의 복잡성입니다.
<!-- ListView 레이아웃 -->
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<ListView
android:id="@+id/listView"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:divider="@color/divider"
android:dividerHeight="1px"
android:scrollbars="vertical" />
</LinearLayout>
사용자 정의 어댑터는 ListView의 getView() 메서드를 재정의합니다. RecyclerView는 이를 onCreateViewHolder + onBindViewHolder로 나누지만, ListView에서는 두 단계가 결합되어 있으므로 개발자가 convertView(재사용 가능한 View)와 setTag() 메서드를 통해 ViewHolder 패턴을 독립적으로 구현해야 합니다.
// 데이터 모델
data class Product(
name: String,
price: String,
imageResId: Int
)
// ViewHolder로 사용자 정의 어댑터
class ProductAdapter(
context: Context,
private val products: List<Product>
) : ArrayAdapter<Product>(context, 0, products) {
// 캐싱을 위한 ViewHolder
private class ViewHolder(view: View) {
val nameText: TextView = view.findViewById(R.id.tvName)
val priceText: TextView = view.findViewById(R.id.tvPrice)
val imageView: ImageView = view.findViewById(R.id.ivProduct)
}
override fun getView(position: Int, convertView: View?, parent: ViewGroup): View {
val view = convertView ?: LayoutInflater.from(context)
.inflate(R.layout.item_product, parent, false)
.also { it.tag = ViewHolder(it) }
val holder = view.tag as ViewHolder
val product = getItem(position)
holder.nameText.text = product.name
holder.priceText.text = product.price
holder.imageView.setImageResource(product.imageResId)
return view
}
}
RecyclerView와의 주요 차이점: ListView에서 getView() 메서드는 항목이 화면에 나타날 때마다 호출됩니다. convertView와 ViewHolder를 사용하지 않으면 스크롤할 때마다 새 View 객체가 생성되어 50개 이상 항목 리스트에서 메모리 누수 및 지연이 발생합니다. 사용자 정의 레이아웃이 있는 ListView에서는 view.tag를 사용한 ViewHolder가 필수 최적화입니다.
ListView에는 Google이 RecyclerView에서 수정한 네 가지 아키텍처 제한 사항이 있습니다: (1) 세로 방향만 가능 — 그리드, 가로 리스트 또는 계단식 없음; (2) 세부 알림 없음 — 데이터 변경 시 notifyDataSetChanged()를 통해 전체 다시 그리기가 트리거되어 떨림 발생; (3) 항목 추가/제거 애니메이션 없음; (4) 책임 분리 없음 — LayoutManager가 내장되어 있고 ItemAnimator가 없음.
<!-- item_product.xml — ListView 항목 레이아웃 -->
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:padding="12dp">
<ImageView
android:id="@+id/ivProduct"
android:layout_width="56dp"
android:layout_height="56dp"
android:scaleType="centerCrop" />
<LinearLayout
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:orientation="vertical"
android:paddingStart="12dp">
<TextView
android:id="@+id/tvName"
android:textSize="16sp"
android:textStyle="bold" />
<TextView
android:id="@+id/tvPrice"
android:textSize="14sp"
android:textColor="@color/price" />
</LinearLayout>
</LinearLayout>
실제 영향: 최대 20개 항목의 리스트에서는 ListView가 RecyclerView만큼 빠르게 작동합니다. 100개 이상 항목에서는 차이가 눈에 띄게 나타납니다. diff 계산 부족과 강제 ViewHolder로 인해 ListView의 FPS가 떨어지기 시작합니다. 자주 업데이트되는 리스트(채팅, 피드, 로그)에는 ListView가 적합하지 않습니다 — Paging 3과 함께 RecyclerView를 사용하세요. IT Sectr에서는 50개 이상의 항목이 있는 레거시 화면을 작업할 때 ListView를 RecyclerView로 마이그레이션합니다.
ListView는 코드 없이 빠른 구성을 위한 XML 속성 세트를 제공합니다: android:divider(구분선 색상/드로어블), android:dividerHeight(두께), android:scrollbars(스크롤바), android:fastScrollEnabled(핸들러로 빠른 스크롤), android:choiceMode(선택 모드: singleChoice, multipleChoice). android:entries 속성을 사용하면 어댑터 없이 리소스 배열(string-array)에서 리스트를 채울 수 있습니다.
| 속성 | 설명 | 예시 값 |
|---|---|---|
| android:divider | 구분선 색상 또는 드로어블 | @color/gray_light |
| android:dividerHeight | px/dp 단위 구분선 두께 | 1dp |
| android:entries | 리소스의 문자열 배열 | @array/countries |
| android:choiceMode | 항목 선택 모드 | singleChoice |
| android:fastScrollEnabled | 빠른 스크롤 | true |
| android:overlapAnchor | 팝업 리스트 앵커(API 21+) | true |
선택 모드: choiceMode="singleChoice"는 선택된 항목이 강조 표시되는 RadioButton 스타일을 활성화합니다. choiceMode="multipleChoice" — 여러 항목에 대한 체크박스입니다. 선택 데이터는 listView.checkedItemPosition 또는 listView.checkedItemIds를 통해 사용할 수 있습니다. 중요: choiceMode에서 ListView는 배경에 StateListDrawable을 사용합니다 — 표준 파란색이 디자인과 맞지 않으면 자체 선택기를 설정하세요.
선택은 ListView와 RecyclerView 사이에서 프로젝트 요구 사항에 따라 달라집니다. RecyclerView가 현대적인 표준이지만, ListView는 레거시 코드 유지 관리 비용이 최소화된 간단한 화면에서 정당화될 수 있습니다. 주요 매개변수 비교:
| 매개변수 | ListView | RecyclerView |
|---|---|---|
| 라이브러리 크기 | SDK에 내장(≈60KB) | AndroidX(종속성 포함 ≈300KB) |
| 최소 버전 | API 1 | API 14(AndroidX 통해) |
| 방향 | 세로만 | 세로, 가로, 그리드, 계단식 |
| ViewHolder | 수동 구현 | 아키텍처상 필수 |
| 애니메이션 | 페이드 전환만 | DefaultItemAnimator(add, remove, move, change) |
| 세부 업데이트 | 없음(notifyDataSetChanged만) | notifyItemInserted/Removed/Changed |
| 클릭 리스너 | onItemClickListener(내장) | ViewHolder 통해(내장 아님) |
| 성능 | 최대 50개 항목 리스트에 충분 | 1000+ 항목에서 안정적인 60FPS |
결론: 새 프로젝트에는 RecyclerView를 사용하세요. ListView는 마이그레이션이 불합리하게 비싼 레거시 코드에만 남겨두세요. Material Design 3 가이드라인(2026)에 따르면, 모든 복잡성의 리스트는 최신 성능 및 접근성 표준을 충족하기 위해 RecyclerView를 사용해야 합니다.
자주 묻는 질문
ListView는 Android SDK API 35에서 @Deprecated로 표시되지 않았지만, Google은 새 프로젝트에서 사용을 권장하지 않습니다(soft-deprecated). 이 컴포넌트는 모든 Android 버전에서 계속 작동합니다. RecyclerView는 2014년부터 공식 대체품입니다. 새 프로젝트를 시작하는 경우 RecyclerView를 선택하세요. 기존 프로젝트를 유지하는 경우 ListView는 변경 없이 계속 작동합니다.
ListView는 addHeaderView(View, Object, boolean) 및 addFooterView(View, Object, boolean) 메서드를 지원합니다. 헤더와 푸터는 일반 항목 리스트에 추가되지만 어댑터에는 참여하지 않습니다 — 항상 리스트의 시작과 끝에 표시됩니다. 중요: 어댑터를 설정(setAdapter())하기 전에 헤더/푸터를 추가하세요. 그렇지 않으면 포함되지 않습니다. 헤더를 추가한 후 항목 위치 ID는 헤더 수만큼 이동합니다.
ListView와 ScrollView 모두 세로 제스처를 처리하여 충돌이 발생합니다. 외부 ScrollView가 스크롤을 가로채서 ListView가 스크롤되지 않습니다. 해결책: (1) ListView를 ScrollView 안에 중첩하지 마세요 — NestedScrollingChild와 함께 RecyclerView를 사용하세요. (2) ScrollView를 NestedScrollView(API 21+)로 교체하세요. (3) ScrollView 내에서 고정 높이(android:layout_height="...dp")의 ListView를 사용하세요. 하지만 이렇게 하면 응답성이 손상됩니다.
ListView에서 데이터 지우기는 어댑터 유형에 따라 다릅니다. ArrayAdapter의 경우: adapter.clear() 및 adapter.addAll(newList) 후 adapter.notifyDataSetChanged()를 호출합니다. 사용자 정의 어댑터의 경우: 데이터 리스트를 업데이트하고 notifyDataSetChanged()를 호출합니다. 참고: listView.invalidateViews()는 View를 다시 그리기만 하고 데이터는 업데이트하지 않습니다. 항상 어댑터를 통해 데이터를 변경하고 직접 변경하지 마세요.
ListView 항목의 높이는 항목 레이아웃에서 설정됩니다. 레이아웃이 layout_height="wrap_content"를 사용하는 경우 높이가 내용에 맞게 조정됩니다. 모든 항목에 고정 높이를 사용하려면 getView()에서 LayoutParams를 설정하는 사용자 정의 어댑터를 사용하세요: view.layoutParams.height = resources.getDimensionPixelSize(R.dimen.itemHeight). 다양한 높이의 경우 다른 뷰 유형으로 RecyclerView를 사용하세요.
요약
턴키 방식의 모바일 애플리케이션을 개발해 드립니다
IT Sectr는 2017년부터 스타트업과 기업을 위한 iOS 및 Android 애플리케이션을 만듭니다. 저희가 상담해 드리고 최적의 솔루션을 제안하겠습니다.