반응형
SwiftUI Study – ScrollView 안에서 LazyVStack을 사용해 성능을 최적화하는 방법
1. 왜 중요한가 (문제 배경)
ScrollView는 기본적으로 모든 자식 뷰를 즉시 렌더링(eager rendering) 합니다.
따라서 다음과 같은 상황에서는 성능 문제가 바로 드러납니다.
- 많은 개수의 셀(예: 50개 이상)
- 이미지가 포함된 셀
- 복잡한 레이아웃을 가진 셀
- 상태 업데이트가 잦은 화면
이때 가장 흔한 실수는 아래와 같습니다.
ScrollView + VStack 구조를 그대로 사용해
모든 셀을 한 번에 렌더링하여 메모리와 CPU를 불필요하게 소모하는 것
SwiftUI는 이를 해결하기 위해 LazyVStack / LazyHStack / LazyVGrid를 제공합니다.
Lazy 계열은 화면에 보이는 시점에 맞춰 지연 렌더링(lazy rendering) 하므로 성능이 크게 개선됩니다.
2. 잘못된 패턴 예시
❌ 예시 1: ScrollView + VStack 조합
struct WrongScrollView: View {
let items = Array(0..<500)
var body: some View {
ScrollView {
VStack(spacing: 12) {
ForEach(items, id: \.self) { i in
RowView(index: i)
}
}
}
}
}
struct RowView: View {
let index: Int
var body: some View {
Text("행 \(index)")
.frame(maxWidth: .infinity)
.padding()
.background(Color.blue.opacity(0.2))
.cornerRadius(8)
}
}
문제점
- 500개의 RowView를 한 번에 모두 초기화
- 화면에서 보이지 않는 뷰도 렌더링되어 CPU 낭비
- 스크롤과 애니메이션 시 렌더링 지연 가능
- 메모리 사용량 증가
실무에서 리스트가 200개만 넘어가도 체감될 만큼 성능이 떨어집니다.
❌ 예시 2: 이미지가 포함된 셀을 ScrollView + VStack으로 렌더링
ScrollView {
VStack {
ForEach(urls, id: \.self) { url in
AsyncImage(url: url) { image in
image.resizable()
}
}
}
}
문제점
- 모든 이미지 다운로드가 즉시 시작
- 캐시를 사용해도 초기 렌더링으로 인해 UI가 느려짐
- 사용자가 보지도 않는 이미지까지 로드됨
3. 올바른 패턴 예시
✅ 예시 1: LazyVStack 사용
struct CorrectScrollView: View {
let items = Array(0..<500)
var body: some View {
ScrollView {
LazyVStack(spacing: 12) {
ForEach(items, id: \.self) { i in
RowView(index: i)
}
}
}
}
}
장점
- 화면에 보여야 할 시점에만 셀을 렌더링
- 셀의 초기화 비용이 크게 줄어 성능 향상
- 스크롤 시 부드러운 인터랙션 유지
- 메모리 사용량 감소
SwiftUI의 Lazy 계열은 UIKit의 UITableView/UICollectionView와 유사한 방식입니다.
✅ 예시 2: LazyVGrid 사용해 복잡한 격자 레이아웃도 최적화
let columns = [
GridItem(.flexible()),
GridItem(.flexible())
]
ScrollView {
LazyVGrid(columns: columns, spacing: 20) {
ForEach(0..<200) { index in
RoundedRectangle(cornerRadius: 12)
.fill(Color.blue.opacity(0.3))
.frame(height: 120)
.overlay(Text("아이템 \(index)"))
}
}
.padding()
}
장점
- 복잡한 그리드도 부드럽게 렌더링
- 셀이 화면에 나타나야 할 때만 그려짐
- iPad 분할 화면에서도 안정적
✅ 예시 3: LazyStack + onAppear 조합으로 페이징(Pagination) 구현
struct PagingScrollView: View {
@State private var items = Array(0..<50)
var body: some View {
ScrollView {
LazyVStack {
ForEach(items, id: \.self) { item in
Text("아이템 \(item)")
.padding()
.onAppear {
if item == items.last {
loadMore()
}
}
}
}
}
}
func loadMore() {
let next = items.count
items.append(contentsOf: next..<(next + 20))
}
}
장점
- UIKit CollectionView 수준으로 자연스러운 인피니티 스크롤
- 렌더링 비용이 낮아 성능 유지 가능
4. 실전 적용 팁
✔ 팁 1 – 상황에따라 ScrollView 안에서 VStack을 쓰는 건 “거의 금지”
특히 다음 조건이 있을 때는 반드시 LazyVStack으로 교체:
- 셀이 50개 이상
- 이미지 포함
- 동적 셀 크기
- 애니메이션 예상
- 페이징 기능 구현
✔ 팁 2 – LazyStack은 높이 계산 방식이 다름을 이해하기
VStack은 모든 자식의 높이를 즉시 계산하지만,
LazyVStack은 보여줄 셀만 계산하므로 성능이 좋다.
✔ 팁 3 – LazyStack 내부에서 id는 안정적인 값 사용
렌더링 효율을 위해 index가 아닌 고유 ID 사용 권장.
✔ 팁 4 – LazyStack 안에서 상태 변경은 최소화
셀 내부 상태 변경이 많으면 다시 렌더링되므로
가능하면 ViewModel로 상태를 단일화하는 것이 좋다.
✔ 팁 5 – scrollDismissesKeyboard(.interactively)와도 잘 호환됨
LazyStack 기반 화면은 레이아웃이 안정되어
키보드 dismiss 동작도 자연스럽다.
5. 정리
- ScrollView + VStack은 많은 셀을 렌더링할 때 성능 병목의 주요 원인이다.
- LazyVStack/LazyVGrid는 SwiftUI에서 리스트 성능을 최적화하는 핵심 도구다.
- 지연 렌더링은 메모리·CPU 사용량을 크게 줄여 UI 반응성을 향상시킨다.
- 실무에서는 ScrollView를 쓰면 기본적으로 LazyVStack을 고려하는 것이 정석이다.
반응형
'Dev Study > SwiftUI' 카테고리의 다른 글
| SwiftUI Study – @EnvironmentObject 안전하게 사용하기 (크래시 방지·전역 상태 오염 방지) (0) | 2025.12.09 |
|---|---|
| SwiftUI Study – @StateObject와 @ObservedObject를 정확하게 선택하는 법 (뷰 재생성·초기화 문제 방지) (0) | 2025.12.09 |
| SwiftUI Study – NavigationStack을 안정적으로 설계하는 법 (중복 Push·잘못된 경로·상태 꼬임 방지) (0) | 2025.12.09 |
| SwiftUI Study – 이미지 로딩·캐싱·리사이징을 효율적으로 처리하는 실전 패턴 (0) | 2025.12.09 |
| SwiftUI Study – body 안에서 무거운 연산을 실행하지 않는 이유와 해결법 (0) | 2025.12.09 |
| SwiftUI Study – AnyView 남용을 피하고 타입 안정성을 유지하는 방법 (0) | 2025.12.09 |
| SwiftUI Study – View 중첩이 깊어지지 않도록 컴포넌트를 분리하는 실전 패턴 (0) | 2025.12.09 |
| SwiftUI Study – offset / position으로 레이아웃을 잡지 말아야 하는 이유 (0) | 2025.12.09 |

