728x90
안녕하세요.
오늘은 선언형 UI 프레임워크인 Jetpack Compose의 LazyColumn과
기존 방식인 RecyclerView를 사용해
동일한 리스트 화면을 만들어보면서 두 방식을 비교해보겠습니다.
RecyclerView란?
RecyclerView는 안드로이드에서 많은 양의 데이터를 효율적으로 리스트나 그리드 형태로 보여줄 때 사용하는 대표적인 UI 컴포넌트입니다.
주로 Adapter, ViewHolder, XML 레이아웃 파일 등 여러 파일과 클래스가 필요합니다.
구조 요약
- RecyclerView: 화면에 리스트를 보여주는 뷰
- Adapter: 데이터를 뷰에 연결해주는 역할
- ViewHolder: 각 아이템 뷰의 참조를 보관
- XML 레이아웃: 아이템 디자인 정의
RecyclerView의 주요 장점 : 성능 최적화
- 실제로 화면에 보이는 아이템만 메모리에 올리고, 스크롤 시 기존 뷰를 재활용(ViewHolder)하여 불필요한 리소스 낭비를 막습니다.
Jetpack Compose의 LazyColumn이란?
Jetpack Compose는 선언형 UI 프레임워크로, UI를 함수로 직접 그릴 수 있습니다.
리스트를 만들 때는 LazyColumn을 사용하며, Adapter나 XML이 필요 없습니다.
특징
- UI와 데이터를 한 곳에서 선언적으로 작성
- Adapter, ViewHolder, XML 없이 코드만으로 구현
- 상태 관리가 쉽고, 코드가 훨씬 간결함
LazyColumn의 내부 동작 원리 (RecyclerView와의 비교)
RecyclerView와 LazyColumn 모두 "보이는 아이템만 메모리에 올리고, 스크롤 시 필요한 부분만 그린다"는 공통점을 가집니다.
하지만 구현 방식에는 약간의 차이가 있습니다.
RecyclerView의 원리

- ViewHolder 패턴을 사용해, 한 번 만들어진 아이템 뷰를 재활용합니다.
- 예를 들어, 화면에 10개 아이템이 보인다면
10개의 ViewHolder만 만들어두고, 스크롤할 때마다 데이터만 바꿔서 재사용합니다. - 이로써, 수천 개의 데이터도 실제로는 10~20개 정도의 뷰만 메모리에 올려 효율적으로 관리합니다.
LazyColumn의 원리
- Compose의 선언형 UI 철학에 맞게, LazyColumn은 "필요할 때만" Composable 함수를 실행해서 아이템 UI를 만듭니다.
- 화면에 보이는 아이템만 실제로 Compose(렌더링)하며, 스크롤로 새로운 아이템이 등장하면 그때 새로 Compose합니다.
- 이미 화면에서 사라진 아이템은 Compose 내부에서 자동으로 메모리에서 관리(해제)합니다.
- RecyclerView처럼 뷰를 재활용하는 구조는 아니지만, Compose의 효율적인 상태 관리와 컴포지션 트리 덕분에 불필요한 리소스 낭비 없이 성능을 보장합니다.
실제로 어떻게 동작할까?
LazyColumn은 내부적으로 LazyListState라는 객체를 통해 현재 스크롤 위치와 화면에 보여야 할 아이템 인덱스를 계산합니다.
그리고 화면에 필요한 만큼만 Composable 함수를 실행해서 UI를 그립니다.
스크롤이 발생하면, 화면 밖으로 나간 아이템은 자동으로 컴포지션 트리에서 제거되고, 새로 들어온 아이템만 다시 Compose합니다.
코드비교 : 연락처 목록 만들기
1. 데이터 클래스 정의
// 연락처 정보를 담는 데이터 클래스
data class Contact(
val name: String, // 이름
val phoneNumber: String, // 전화번호
val profileResId: Int // 프로필 사진 리소스 ID (drawable)
)
2. RecyclerView 방식
1) 아이템 레이아웃 (res/layout/item_contact.xml)
- bg_circle은 원형 배경 drawable입니다.
- ic_profile_placeholder는 기본 프로필 이미지입니다.
<!-- 연락처 아이템의 레이아웃: 프로필, 이름, 전화번호 -->
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="horizontal"
android:padding="12dp"
android:gravity="center_vertical"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<!-- 원형 프로필 이미지 -->
<ImageView
android:id="@+id/imgProfile"
android:layout_width="40dp"
android:layout_height="40dp"
android:src="@drawable/ic_profile_placeholder"
android:scaleType="centerCrop"
android:background="@drawable/bg_circle"
android:layout_marginEnd="12dp"/>
<!-- 이름과 전화번호를 세로로 배치 -->
<LinearLayout
android:orientation="vertical"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1">
<TextView
android:id="@+id/tvName"
android:textSize="16sp"
android:textStyle="bold"
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
<TextView
android:id="@+id/tvPhone"
android:textSize="14sp"
android:textColor="#888888"
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
</LinearLayout>
</LinearLayout>
2) Adapter & ViewHolder
// 연락처 리스트를 위한 Adapter
class ContactAdapter(
private val contacts: List<Contact>,
private val onClick: (Contact) -> Unit
) : RecyclerView.Adapter<ContactAdapter.ContactViewHolder>() {
// 아이템 뷰의 참조를 보관하는 ViewHolder
class ContactViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
val imgProfile: ImageView = itemView.findViewById(R.id.imgProfile)
val tvName: TextView = itemView.findViewById(R.id.tvName)
val tvPhone: TextView = itemView.findViewById(R.id.tvPhone)
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ContactViewHolder {
val view = LayoutInflater.from(parent.context)
.inflate(R.layout.item_contact, parent, false)
return ContactViewHolder(view)
}
override fun onBindViewHolder(holder: ContactViewHolder, position: Int) {
val contact = contacts[position]
holder.imgProfile.setImageResource(contact.profileResId) // 프로필 사진 설정
holder.tvName.text = contact.name // 이름 설정
holder.tvPhone.text = contact.phoneNumber // 전화번호 설정
holder.itemView.setOnClickListener { onClick(contact) } // 클릭 이벤트
}
override fun getItemCount() = contacts.size
}
3) Activity에서 사용
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
val recyclerView = findViewById<RecyclerView>(R.id.recyclerView)
// 예시 연락처 데이터
val contacts = listOf(
Contact("홍길동", "010-1234-5678", R.drawable.profile1),
Contact("김철수", "010-2345-6789", R.drawable.profile2),
Contact("이영희", "010-3456-7890", R.drawable.profile3)
)
val adapter = ContactAdapter(contacts) { contact ->
Toast.makeText(this, "${contact.name} 님 클릭!", Toast.LENGTH_SHORT).show()
}
recyclerView.adapter = adapter
recyclerView.layoutManager = LinearLayoutManager(this)
}
}
3. Jetpack Compose 방식
1) Composable 함수
@Composable
fun ContactList(contacts: List<Contact>, onClick: (Contact) -> Unit) {
LazyColumn {
items(contacts) { contact ->
ContactItem(contact, onClick)
}
}
}
@Composable
fun ContactItem(contact: Contact, onClick: (Contact) -> Unit) {
Row(
modifier = Modifier
.fillMaxWidth()
.clickable { onClick(contact) }
.padding(12.dp),
verticalAlignment = Alignment.CenterVertically
) {
// 프로필 이미지 (원형)
Image(
painter = painterResource(id = contact.profileResId),
contentDescription = "프로필 사진",
modifier = Modifier
.size(40.dp)
.clip(CircleShape)
)
Spacer(modifier = Modifier.width(12.dp))
// 이름과 전화번호를 세로로 배치
Column {
Text(
text = contact.name,
fontWeight = FontWeight.Bold,
fontSize = 16.sp
)
Text(
text = contact.phoneNumber,
color = Color.Gray,
fontSize = 14.sp
)
}
}
}
2) Activity에서 사용
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
val contacts = listOf(
Contact("홍길동", "010-1234-5678", R.drawable.profile1),
Contact("김철수", "010-2345-6789", R.drawable.profile2),
Contact("이영희", "010-3456-7890", R.drawable.profile3)
)
ContactList(contacts) { contact ->
Toast.makeText(this, "${contact.name} 님 클릭!", Toast.LENGTH_SHORT).show()
}
}
}
}
- RecyclerView는 ViewHolder 패턴으로 뷰를 재사용해 성능을 최적화합니다.
- LazyColumn은 Compose의 선언형 패러다임에 맞게 "필요할 때만 Compose"하는 방식으로 성능을 최적화합니다.
- 둘 다 대용량 데이터에서도 "화면에 보이는 만큼만" UI를 만들어 효율적으로 리스트를 구현할 수 있습니다.
- LazyColumn은 Compose의 선언적 스타일에 맞게 이 원리를 내부적으로 효율적으로 적용하고 있어 개발자가 별도의 최적화 코드를 신경 쓸 필요 없이 간결하게 리스트 UI를 만들 수 있습니다.
'안드로이드 코틀린' 카테고리의 다른 글
| [Kotlin][Android] Jetpack Compose로 CameraX로 객체 인식하기 (2) | 2025.07.22 |
|---|---|
| [Kotlin][Android] Jetpack Compose에서 Modifier 확장 함수로 Shimmer Effect 구현하기 (0) | 2025.07.15 |
| [Kotlin][Android] Hilt 활용 예시 만들기 (2) | 2025.07.04 |
| [Kotlin][Android] Keystore를 사용해 암호화하기 (0) | 2025.07.03 |
| [Kotlin][Android] 코루틴(Coroutine) 사용하기 (2) | 2025.06.26 |