SwiftUI Study – 리스트 성능 최적화를 위한 셀 안정성(Cell Identity)·업데이트 최소화·비동기 작업 관리 전략
Dev Study/SwiftUI 2025. 12. 9. 10:50반응형
SwiftUI Study – 리스트 성능 최적화를 위한 셀 안정성(Cell Identity)·업데이트 최소화·비동기 작업 관리 전략
1. 왜 중요한가 (문제 배경)
SwiftUI의 List는 편리하지만, 대규모 데이터나 실시간 업데이트 환경에서는 다음과 같은 문제가 발생한다.
- 셀이 깜빡임 또는 재렌더링 과도
- 빠르게 스크롤할 때 비동기 작업 중첩 실행
- 이미지/네트워크 요청이 반복되어 성능 저하
- 아이디(identity)가 불안정하여 diffing 실패
- 셀마다 상태가 섞여 의도치 않은 UI 변경
대부분은 정체성(Identity) 과 비동기 로딩 방식을 올바르게 설계하지 않아 발생한다.
SwiftUI는 선언형이라 그리는 방식이 아니라
데이터 변화에 따라 뷰 트리를 재생성하는 방식이기 때문에
리스트 성능 최적화는 필수이다.
2. 잘못된 패턴 예시
❌ 예시 1: ForEach에서 id를 잘못 지정해 깜빡임 발생
List {
ForEach(items, id: \.self) { item in // ❌ self는 불안정한 ID
Row(item: item)
}
}
문제점
- 값 타입에서 내용이 조금만 변해도 다른 셀로 인식됨
- 전체 셀이 재렌더링되면서 깜빡임
- diffing 효율이 떨어져 불필요한 애니메이션 발생
❌ 예시 2: Row 내부에서 비동기 로딩을 직접 수행
struct WrongRow: View {
let item: Item
@State private var detail: Detail?
var body: some View {
VStack {
Text(item.title)
Text(detail?.note ?? "로딩 중…")
}
.task {
detail = try? await api.fetchDetail(item.id) // ❌ 셀마다 실행됨
}
}
}
문제점
- 스크롤 시 동일 API가 반복 호출
- 셀 재사용/재생성으로 동일 작업이 중복됨
- 빠르게 스크롤하면 수십 개 요청이 동시에 발생
- UI는 깜빡이고 성능은 크게 저하
❌ 예시 3: 셀 상태가 리스트 외부 상태와 충돌
struct WrongRow: View {
@State var isExpanded = false // ❌ 셀이 사라지면 상태가 초기화됨
var body: some View {
VStack { ... }
}
}
문제점
- 스크롤로 셀이 사라졌다 다시 나타나면 상태 초기화
- 확장/축소 UI가 불안정
- 셀 상태는 View 내부가 아닌 상위 레이어에서 관리되어야 함
3. 올바른 패턴 예시
✅ 예시 1: 안정적인 ID(identity) 사용
struct Item: Identifiable {
let id: UUID
let title: String
}
List(items) { item in
Row(item: item)
}
장점
- SwiftUI가 diffing을 정확하게 수행
- 셀 깜빡임·재렌더링 최소화
- 아이디가 변하지 않아 업데이트가 필요한 부분만 변경
✅ 예시 2: 비동기 요청은 셀이 아닌 상위 ViewModel에서 수행
@MainActor
final class ItemsViewModel: ObservableObject {
@Published var items: [Item] = []
@Published var details: [UUID: Detail] = [:]
func load() async {
items = await api.fetchItems()
for item in items {
Task {
let detail = try? await api.fetchDetail(item.id)
await MainActor.run {
details[item.id] = detail
}
}
}
}
}
사용:
struct ItemsView: View {
@StateObject private var vm = ItemsViewModel()
var body: some View {
List(vm.items) { item in
Row(item: item, detail: vm.details[item.id])
}
.task { await vm.load() }
}
}
장점
- 비동기 로딩은 단 한 곳에서 수행
- Row는 단순히 “표현”만 담당 → 깜빡임 없음
- 스크롤과 무관하게 안정적으로 로딩됨
✅ 예시 3: 셀 내부 상태를 외부에서 관리 (식별 가능한 상태)
struct ItemRow: View {
let item: Item
@Binding var isExpanded: Bool
var body: some View {
VStack {
Text(item.title)
if isExpanded { Text("더보기 내용") }
}
.onTapGesture { isExpanded.toggle() }
}
}
상위에서 관리:
@State private var expanded: [UUID: Bool] = [:]
List(items) { item in
ItemRow(item: item, isExpanded: Binding(
get: { expanded[item.id] ?? false },
set: { expanded[item.id] = $0 }
))
}
장점
- 셀이 사라졌다 나타나도 상태가 유지됨
- 스크롤 위치와 관계없이 일관된 UI
- 복잡한 리스트에서도 안정성 향상
✅ 예시 4: 이미지 캐싱 포함한 효율적 셀 렌더링
(28번 팁에서 만든 로더 활용)
struct ItemRow: View {
let item: Item
var body: some View {
HStack {
CachedAsyncImage(url: item.thumbnail, size: CGSize(width: 80, height: 80))
.frame(width: 60, height: 60)
Text(item.title)
}
}
}
장점
- 스크롤 시 깜빡임 제거
- 반복 요청 없음
- 메모리 안정성 증가
4. 실전 적용 팁
✔ 팁 1 – ID는 절대 변하면 안 되는 안정적인 값이어야 한다
특히 정렬·필터링 시 ID가 변경되지 않도록 주의.
✔ 팁 2 – 비동기 로딩은 Row가 아니라 ViewModel에서
Row는 가볍게 유지해야 스크롤 성능이 좋아진다.
✔ 팁 3 – 셀 내부의 상태는 상위 View에서 관리
Row 내부에 @State를 넣으면 UI 불안정의 주요 원인.
✔ 팁 4 – 이미지 로딩은 캐시 + 다운샘플링이 필수
리스트 성능에 가장 큰 영향을 미친다.
✔ 팁 5 – 스크롤 중 무거운 연산 절대 금지
Layout 연산, JSON 디코딩, 이미지 리사이즈 등은 백그라운드에서 처리.
5. 정리
- SwiftUI 리스트 성능 최적화의 핵심은 정체성(Identity), 비동기 로딩 관리,
그리고 셀 상태를 어디에 둘 것인가이다. - 네트워크/이미지 로딩을 셀에서 수행하면 성능 문제는 반드시 발생하므로
ViewModel 단일 지점에서 처리하도록 구조를 설계해야 한다. - 안정적인 ID와 캐시 전략을 도입하면 깜빡임 없는 자연스러운 스크롤 경험을 만들 수 있다.
반응형

