Compose 리스트 — LazyColumn·key·성능
왜 리스트가 성능의 격전지인가
목록은 앱에서 가장 흔하면서 가장 성능이 위험한 UI다. 채팅·피드·검색 결과는 수백~수천 항목이 흔하다. 만약 이 모든 항목의 UI를 한꺼번에 메모리에 만들면 — 화면엔 10개만 보이는데 1000개를 다 그려 메모리가 폭발하고 스크롤이 렉으로 끊긴다. 핵심 통찰은 단순하다 — 사용자는 한 번에 화면만큼만 본다. 그러니 보이는 것만 만들고, 스크롤로 벗어난 것은 버리고 들어오는 것만 새로 만들면 된다. 이게 **LazyColumn/LazyRow/LazyVerticalGrid**의 발상이고, RecyclerView의 Compose 버전이다. 그리고 이 재활용을 정확하고 부드럽게 만드는 열쇠가 **key**다. (출처: Android — Lists and grids in Compose.)
Lazy 컴포넌트 — 보이는 것만 구성한다
일반 Column { items.forEach { Row(it) } }은 모든 항목을 즉시 구성한다 — 항목이 소수일 때만 괜찮다. 항목이 많으면 **LazyColumn**을 쓴다 — 현재 보이는 항목(+스크롤 여유분)만 구성하고, 스크롤하면 벗어난 항목은 폐기, 새로 들어온 항목만 구성한다.
LazyColumn {
item { Header() } // 단일 항목 (헤더)
items(members, key = { it.id }) { member -> // 목록 — key 지정
MemberRow(member)
}
stickyHeader { SectionHeader() } // 스크롤해도 고정되는 헤더
}
items(list)로 목록을, item { }으로 단일 항목(헤더·푸터)을, stickyHeader로 스크롤해도 상단에 붙는 헤더를 만든다. 핵심은 — LazyColumn은 1000개를 모두 만들지 않으므로 메모리·CPU가 데이터 크기와 무관하게 일정하다는 점이다.
key — 왜 거의 항상 필요한가
items(list, key = { it.id })의 **key**가 성능과 정확성 둘 다의 핵심이다. key가 없으면 Compose는 항목을 "위치(index)"로 식별한다 — 그래서 목록 중간에 항목을 하나 추가하거나 삭제하면, 그 아래의 모든 항목이 위치가 밀려 Compose가 "전부 바뀐 것"으로 보고 다시 그린다(불필요한 recomposition·깜빡임). 더 나쁘게는 애니메이션이나 항목 내부 상태(체크박스 선택 등)가 엉뚱한 항목에 붙는다.
key를 주면 — Compose가 항목을 "정체성(id)"으로 추적한다. 그래서 항목이 추가·삭제·이동돼도 "어느 게 어느 것인지" 알아, 바뀌지 않은 항목은 재구성하지 않고, 스크롤 위치·항목 애니메이션을 정확히 유지한다.
items(members, key = { it.id }) { m ->
MemberRow(m, Modifier.animateItem()) // key 덕분에 추가/삭제/이동이 부드럽게 애니메이션
}
스크롤 상태와 derivedStateOf
rememberLazyListState()로 스크롤 상태를 잡으면 프로그래밍적 스크롤(animateScrollToItem)·위치 관찰을 할 수 있다. 한 가지 함정 — 스크롤 위치는 매 프레임 바뀌므로, 거기서 파생된 값("맨 위에 있나")을 그냥 읽으면 매 픽셀마다 recompose된다. **derivedStateOf**로 결과(불리언)가 바뀔 때만 recompose하게 한다([04]).
val state = rememberLazyListState()
val showTopButton by remember {
derivedStateOf { state.firstVisibleItemIndex > 5 } // 인덱스가 매번 바뀌어도 불리언이 바뀔 때만
}
LazyColumn(state = state) { /* ... */ }
페이징 — 무한 스크롤
서버 데이터가 수만 건이면 한 번에 다 받을 수 없다. Paging 3(PagingData·collectAsLazyPagingItems)는 스크롤에 맞춰 다음 페이지를 자동 요청·캐싱·로딩 상태 표시한다 — 무한 스크롤·당겨서 새로고침을 표준 방식으로 처리한다.
성능 — 항목을 가볍고 안정적으로
대량 목록의 성능은 몇 가지 습관에서 갈린다.
key지정 — 위 이유로 거의 항상 필요하다.contentType— 항목 종류가 여럿(텍스트/이미지/광고)이면 타입별로 재활용해 효율을 높인다.- 항목을 가볍게 — 각 항목의 무거운 계산은
remember로 캐시하고, 불안정(unstable) 파라미터를 피해([17 성능]) 재구성을 줄인다. - 중첩 스크롤 주의 —
LazyColumn안에 높이가 무한인 다른 세로 스크롤을 넣으면 측정이 충돌해 크래시나 성능 문제가 난다.
정리 — 보이는 것만, key로 안정되게
Compose 리스트의 핵심 — LazyColumn/LazyRow/LazyGrid로 보이는 항목만 구성·재활용(데이터가 커도 메모리·CPU 일정), key로 항목을 정체성으로 추적(변경에 강하고 애니메이션·스크롤·내부 상태 유지), 스크롤 파생 값은 rememberLazyListState+derivedStateOf, 무한 스크롤은 Paging 3, 항목은 가볍게·contentType으로 재활용. 대량 목록의 성능은 key와 가벼운 항목에서 갈린다.
한 줄 요약 — LazyColumn/Row/Grid=보이는 항목만 구성·재활용(
item/items/stickyHeader, 메모리/CPU가 데이터 크기와 무관).key={it.id}거의 필수(위치가 아닌 정체성으로 추적→변경/애니메이션/내부 상태 유지). 스크롤 파생은rememberLazyListState+derivedStateOf(과한 recompose 방지). 무한 스크롤 Paging 3.contentType·가벼운 항목·중첩 스크롤 회피.
(출처: Android — Lists and grids / Paging in Compose / Compose performance — keys.)