반응형
SwiftUI에서 많이 하는 실수 - View의 body 안에서 무거운 연산을 실행하는 실수
SwiftUI의 body는 상태가 변경될 때마다 여러 번 호출되는 계산 프로퍼티입니다.
여기에 정렬, 필터링, 무거운 변환, JSON 파싱 등을 넣어두면
작은 상태 변화에도 동일 연산이 반복 실행되어 성능 문제가 터지기 쉽습니다.
1. 문제 원인
- “화면 그릴 때 한 번만 호출되겠지”라는 오해
- View와 ViewModel/UseCase 책임 분리가 안 되어 있음
- 빠른 구현을 위해 모든 로직을 body 안에 때려 넣음
2. 나타나는 증상
- 스크롤이 끊기고, 입력할 때마다 UI가 버벅거림
- 리스트가 길어질수록 성능이 급격히 나빠짐
- Instruments로 보면 body 호출 때 CPU가 이상하게 높게 찍힘
3. 잘못된 코드 예시
struct CardsView: View {
@State private var searchText = ""
@State private var allCards: [SecureCard] = loadFromDisk() // 디스크 로드라고 가정
var body: some View {
// ❌ 상태가 바뀔 때마다 필터 + 정렬 전체 재계산
let filtered = allCards
.filter { $0.title.contains(searchText) }
.sorted { $0.createdAt > $1.createdAt }
return VStack {
TextField("검색", text: $searchText)
.textFieldStyle(.roundedBorder)
.padding()
List(filtered, id: \.id) {
Text($0.title)
}
}
}
}
- searchText에서 글자 하나 입력할 때마다 전체 리스트를 정렬/필터링 합니다.
4. 올바른 코드 예시
ViewModel로 연산을 분리
final class CardsViewModel: ObservableObject {
@Published var searchText: String = ""
@Published private(set) var filtered: [SecureCard] = []
private var allCards: [SecureCard] = []
init() {
allCards = loadFromDisk()
applyFilter()
}
func applyFilter() {
let text = searchText.trimmingCharacters(in: .whitespacesAndNewlines)
filtered = allCards
.filter { card in
text.isEmpty || card.title.localizedCaseInsensitiveContains(text)
}
.sorted { $0.createdAt > $1.createdAt }
}
}
struct CardsView: View {
@StateObject private var viewModel = CardsViewModel()
var body: some View {
VStack {
TextField("검색", text: $viewModel.searchText)
.textFieldStyle(.roundedBorder)
.padding()
.onChange(of: viewModel.searchText) { _ in
viewModel.applyFilter() // ✅ ViewModel에서 처리
}
List(viewModel.filtered, id: \.id) { card in
Text(card.title)
}
}
}
}
5. 정리 및 팁
- “무거운 연산”의 기준
- 컬렉션 전체 순회가 필요한 정렬/필터
- JSON 파싱, 파일 읽기/쓰기
- 이미지 리사이징/디코딩 등
- 이런 연산은 ViewModel, UseCase, 별도 Helper로 옮기고, View는 계산된 값만 참조하는 구조로 설계합니다.
- body는 최대한 “상태 → UI” 매핑에만 집중하도록 만들수록 SwiftUI의 장점이 살아납니다.
반응형

