보이는 항목만 구성하는 목록. View 시스템의 RecyclerView에 대응한다.
LazyColumn(
state = listState,
contentPadding = PaddingValues(16.dp),
verticalArrangement = Arrangement.spacedBy(8.dp)
) {
item { Header() }
items(users, key = { it.id }) { user -> UserRow(user) }
items(10) { index -> Placeholder(index) }
}
왜 Column을 쓰면 안 되나
Column(Modifier.verticalScroll(rememberScrollState())) {
-
users.forEach { UserRow(it) } }
-
1,000개면 1,000개를 전부 구성하고 측정하고 배치한다
-
첫 프레임이 수백 ms 걸리고 메모리가 폭발한다
LazyColumn 은 화면에 보이는 것 + 약간의 여유만 구성한다
key가 왜 거의 항상 필요한가
items(users, key = { it.id }) { ... }
key 가 없으면 위치(index) 로 항목을 식별한다
목록 맨 앞에 항목을 하나 추가하면
- 모든 항목의 index 가 밀린다
- Compose 는 "전부 바뀌었다" 고 판단한다
- 각 항목의 remember 상태(펼침·애니메이션 진행) 가 엉뚱한 항목으로 옮겨간다
- 스크롤 위치가 튄다
key 를 주면 항목의 정체가 유지된다
- 애니메이션(animateItem) 도 이때만 올바르게 동작한다
key 는 안정적이고 유일해야 한다
❌ key = { it.hashCode() } 내용이 바뀌면 달라진다
❌ key = { index } key 없는 것과 같다
✅ key = { it.id }
스크롤 상태
val listState = rememberLazyListState()
val showTopButton by remember {
derivedStateOf { listState.firstVisibleItemIndex > 5 }
}
derivedStateOf 없이 firstVisibleItemIndex 를 직접 읽으면
- 스크롤 픽셀마다 리컴포지션이 돈다 감싸면 Boolean 결과가 바뀔 때만 돈다
무한 스크롤 — Paging 3
val items = viewModel.pagedFlow.collectAsLazyPagingItems()
LazyColumn {
items(items.itemCount, key = items.itemKey { it.id }) { i ->
items[i]?.let { UserRow(it) }
}
if (items.loadState.append is LoadState.Loading) item { LoadingRow() }
}
Paging 3 이 주는 것
- 페이지 요청·중복 방지·재시도
- RemoteMediator 로 네트워크 + Room 캐시 결합
- 로딩/에러 상태를 loadState 로 노출
직접 구현하면 스크롤 중 중복 요청과 경합을 다 다뤄야 한다
면접 함정
- ❌
LazyColumn안에LazyColumn을 중첩한다 → 높이가 무한이 돼 예외가 난다.item으로 펼치거나 중첩 스크롤을 설계한다. - ❌ "key는 애니메이션용" → 상태 보존과 스크롤 안정성이 더 큰 이유다.