반응형

SwiftUI Study – body 안에서 무거운 연산을 실행하지 않는 이유와 해결법

 

1. 왜 중요한가 (문제 배경)

SwiftUI의 body는 상태가 변경될 때마다 반드시 다시 호출되는 함수입니다.

즉, body는 다음과 같은 순간마다 재실행됩니다.

  • @State 값 변경
  • @Binding 변경
  • ObservableObject 업데이트
  • 환경 값(Environment) 변경
  • 부모 뷰 재렌더링
  • 애니메이션 중간 프레임

이 뜻은 곧 다음을 의미합니다.

body 안에서 무거운 연산을 실행하면 성능이 즉시 하락한다.

대표적인 문제 상황

  • 정렬(sort), 필터(filter), 매핑(map)
  • JSON 디코딩, 날짜 포맷팅(DateFormatter)
  • 이미지 리사이징
  • 네트워크 요청 or 블록킹 I/O
  • 비싼 계산(루프, 알고리즘 등)

body는 “뷰 선언”만 담당해야 하며, “연산/로직”을 포함해서는 안 됩니다.

 

2. 잘못된 패턴 예시

❌ 예시 1: body 안에서 정렬/필터 실행

struct WrongListView: View {
    let items: [Item]

    var body: some View {
        List {
            ForEach(items.sorted(by: { $0.score > $1.score })) { item in   // ❌ 정렬이 매번 수행됨
                Text(item.title)
            }
        }
    }
}

문제점

  • body가 호출될 때마다 정렬 재실행
  • 데이터 개수가 많아지면 렌더링 지연 발생
  • 스크롤 시에도 반복 실행되며 프레임 드랍 발생 가능

❌ 예시 2: 날짜 포맷터 매번 생성

struct WrongDateView: View {
    let date: Date

    var body: some View {
        let formatter = DateFormatter()   // ❌ 비용 큼
        formatter.dateFormat = "yyyy-MM-dd"

        return Text(formatter.string(from: date))
    }
}

문제점

  • DateFormatter는 매우 비싼 객체
  • body 재호출마다 반복 생성 → 불필요한 CPU 소모

❌ 예시 3: body 안에서 네트워크 요청

struct WrongNetworkView: View {
    @State private var result: String = ""

    var body: some View {
        VStack {
            Text(result)
        }
        .onAppear {
            load()     // ❌ 하지만 load 안에서 무거운 작업을 동기 실행하면 문제
        }
    }

    func load() {
        let data = try! Data(contentsOf: URL(string: "https://...")!)  // ❌ 동기 I/O
        result = String(decoding: data, as: UTF8.self)
    }
}

문제점

  • 동기 네트워크는 UI 스레드를 블록
  • body와 무관한 작업이 UI 성능을 직접 저하시킴

❌ 예시 4: 이미지 리사이징/연산을 body 안에서 수행

Image(uiImage: image.resize(width: 300))   // ❌ 매번 resize 재실행

문제점

  • 스크롤 리스트 안에 있을 경우 성능이 참혹하게 떨어짐
  • 연산이 반복 수행되며 CPU 사용률 증가

 

3. 올바른 패턴 예시

✅ 예시 1: 연산은 미리 계산한 property로 분리

struct CorrectListView: View {
    let items: [Item]

    var sortedItems: [Item] {
        items.sorted(by: { $0.score > $1.score })   // body 밖에서 계산
    }

    var body: some View {
        List(sortedItems) { item in
            Text(item.title)
        }
    }
}

장점

  • body는 단순히 렌더링만 담당
  • SwiftUI diffing과 독립적
  • 성능 저하 없음

✅ 예시 2: DateFormatter는 정적(static)으로 캐시

struct CorrectDateView: View {
    let date: Date

    static let formatter: DateFormatter = {
        let f = DateFormatter()
        f.dateFormat = "yyyy-MM-dd"
        return f
    }()

    var body: some View {
        Text(Self.formatter.string(from: date))
    }
}

장점

  • DateFormatter는 1번만 생성
  • body 호출은 단순 string 사용만 수행
  • 성능 안정적

✅ 예시 3: 비동기 연산은 task / onAppear + async로 분리

struct CorrectNetworkView: View {
    @State private var text = ""

    var body: some View {
        Text(text)
            .task {
                text = await load()
            }
    }

    func load() async -> String {
        let (data, _) = try! await URLSession.shared.data(from: URL(string: "https://...")!)
        return String(decoding: data, as: UTF8.self)
    }
}

장점

  • body가 비동기 작업과 분리됨
  • UI 스레드 차단 없음
  • 상태 변경 흐름도 안정적

✅ 예시 4: 이미지 연산은 ViewModel 또는 background task에서 수행

@MainActor
final class PhotoViewModel: ObservableObject {
    @Published var resized: UIImage?

    func process(_ image: UIImage) {
        Task.detached {
            let result = image.resize(width: 300)   // 백그라운드 연산
            await MainActor.run {
                self.resized = result
            }
        }
    }
}

body에서는 단순히:

if let img = viewModel.resized {
    Image(uiImage: img)
}

 

4. 실전 적용 팁

✔ 팁 1 – “body는 절대로 계산 로직을 포함하지 않는다”

body는 화면 선언만 담당해야 하며,

연산/필터/정렬/변환/날짜처리 등은 모두 별도 프로퍼티나 메서드로 이동.

✔ 팁 2 – ViewModel이 존재한다면 연산은 대부분 ViewModel로 이동

SwiftUI는 UI 선언, ViewModel은 연산/로직이라는 역할이 명확해짐.

✔ 팁 3 – 정렬/필터는 body 바깥에서 수행하면 성능이 확연히 좋아짐

특히 리스트에서 필수.

✔ 팁 4 – 애니메이션 중에는 body가 수십 번 호출된다

→ body 안에 연산이 있으면 초당 수십 번 반복 실행됨

→ 프레임 드랍 / 스크롤 끊김 발생

✔ 팁 5 – DateFormatter, NumberFormatter, Regex 등은 static 캐시가 정석

가장 비용이 큰 객체들 중 하나.

 

5. 정리

  • body는 선언적 UI의 “그리기 청사진”일 뿐이며, 연산 로직이 들어가면 성능이 급격히 떨어진다.
  • 정렬/필터/매핑, 이미지 처리, 날짜 포맷팅, 네트워크 등은 절대 body 안에 넣지 않는다.
  • 가능한 모든 계산은 body 외부에서 수행하거나 ViewModel로 이동한다.
  • 이 원칙을 지키면 SwiftUI의 성능 문제 절반은 자동으로 해결된다.
반응형
Posted by 까칠코더
,