반응형

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의 장점이 살아납니다.
반응형
Posted by 까칠코더
,