반응형

SwiftUI Study – View Refresh(리렌더링) 최소화: EquatableView·Transaction·Subview 분리로 성능 최적화하기

 

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

SwiftUI는 상태 기반 UI 시스템으로, 상태가 변경되면 해당 상태를 참조하는 View가 다시 계산됩니다.

이 방식은 선언적이고 단순하지만 다음과 같은 문제가 발생할 수 있습니다.

  • 작은 변경에도 과도한 리렌더링 발생
  • 리스트나 반복 뷰에서 비싼 연산이 반복 실행
  • 애니메이션이 불필요하게 다시 시작
  • 복잡한 화면에서 성능 저하 또는 프레임 드랍

SwiftUI는 내부적으로 diffing 기반으로 View를 비교하지만,

적절히 구조화하지 않으면 불필요한 업데이트가 끊임없이 발생합니다.

따라서 실제 변경된 값만 업데이트되도록 View를 최적화하는 설계는 매우 중요합니다.

 

2. 잘못된 패턴 예시

❌ 예시 1: 큰 State 구조체 하나로 전체 화면이 갱신됨

struct LargeState {
    var name: String
    var age: Int
    var logs: [String]
    var preferences: Preferences
}

@State private var state = LargeState(...)

문제점

  • state.name만 바뀌어도 state 전체가 변경되었다고 판단
  • 해당 값을 사용하는 모든 View가 갱신
  • 화면이 복잡하면 체감 성능 저하 발생

❌ 예시 2: 값이 바뀌지 않아도 animation(value:)로 인한 재렌더링

Text(title)
    .animation(.easeInOut, value: title)   // ❌ title이 같아도 트리거될 수 있음

문제점

  • Property 변화 감지 타이밍·비교 방식 때문에
    애니메이션이 불필요하게 다시 실행되는 사례 존재

❌ 예시 3: Subview에 불필요한 연산이 매번 실행됨

struct ExpensiveTagView: View {
    let tags: [String]

    var body: some View {
        let computed = tags.sorted()  // ❌ 매번 계산됨
        return Text(computed.joined(separator: ", "))
    }
}

문제점

  • 스크롤 리스트 안에서 반복되면 매우 비효율적
  • View는 struct이지만 body 계산은 비용이 클 수 있음

❌ 예시 4: 부모 View의 상태 변화가 하위 모든 View를 리렌더링

var body: some View {
    VStack {
        Header(user: user)      // ❌ user 변경 시 전체 VStack 재계산
        Content(items: items)   // ❌ items 변경 시 전체 VStack 재계산
        Footer()                // ❌ 필요 없어도 매번 계산
    }
}

 

3. 올바른 패턴 예시

✅ 예시 1: EquatableView로 값이 “진짜로 변경될 때만” 리렌더링

struct UserHeader: View, Equatable {
    let name: String
    let age: Int

    static func == (lhs: Self, rhs: Self) -> Bool {
        lhs.name == rhs.name && lhs.age == rhs.age
    }

    var body: some View {
        VStack {
            Text(name)
            Text("\(age)")
        }
    }
}

EquatableView(content: {
    UserHeader(name: user.name, age: user.age)
})

장점

  • Equatable 비교 결과가 동일하면 body 재계산 건너뜀
  • 리스트, 대시보드 등 성능 개선 효과 매우 큼

✅ 예시 2: Subview로 분리해 최소 단위 리렌더링 적용

struct ParentView: View {
    @State private var name = "홍길동"
    @State private var count = 0

    var body: some View {
        VStack {
            NameSection(name: name)     // name 변경 시에만 update
            CountSection(count: count)  // count 변경 시에만 update
        }
    }
}

장점

  • SwiftUI diffing이 더 효율적으로 작동
  • 불필요한 렌더링을 자동으로 차단

✅ 예시 3: 계산 비용이 큰 연산은 body 외부로 이동

struct TagView: View {
    let tags: [String]

    var sortedTags: [String] {
        tags.sorted()
    }

    var body: some View {
        Text(sortedTags.joined(separator: ", "))  // ✔ 필요할 때만 계산
    }
}

또는 @MainActor/Task 기반 async 연산:

@State private var heavyResult: [String] = []

.task {
    heavyResult = await heavyCompute(tags)
}

✅ 예시 4: Transaction으로 불필요한 애니메이션 억제

Text(title)
    .transaction { transaction in
        transaction.disablesAnimations = true
    }

장점

  • 값은 바꾸되 애니메이션은 실행하지 않도록 제어 가능
  • 특정 구간만 애니메이션 비활성화 가능

✅ 예시 5: ObservableObject는 @Published 분리로 최소 단위 갱신

final class UserVM: ObservableObject {
    @Published var name: String = ""
    @Published var age: Int = 0
}

장점

  • name 변경 시 age 관련 View는 갱신되지 않음
  • 모델 단위를 더 잘게 나누면 렌더링 효율 상승

 

4. 실전 적용 팁

✔ 팁 1 – “큰 State 하나” 대신 필요한 값만 별도로 분리

SwiftUI 렌더링 단위가 작아질수록 성능이 좋아진다.

✔ 팁 2 – Subview 분리만으로도 성능이 극적으로 개선될 수 있다

부모 View의 상태 변경이 하위 전체를 흔들지 않도록 구조화.

✔ 팁 3 – EquatableView는 리스트, 카드, 반복 뷰에 특히 효과적

초당 수십~수백 번의 diffing을 줄여준다.

✔ 팁 4 – body 내부에 무거운 연산 절대 금지

정렬, 합산, 필터링, 이미지 처리 등은 반드시 외부로 분리.

✔ 팁 5 – Animation(value:)는 꼭 필요한 경우에만

불필요한 UI 업데이트를 유발할 수 있음.

 

5. 정리

  • SwiftUI의 성능 문제 대부분은 불필요한 렌더링에서 비롯된다.
  • EquatableView·Subview 분리·계산 최적화를 적용하면
    SwiftUI의 선언적 모델에서도 정밀한 렌더링 제어가 가능하다.
  • 특히 리스트·그리드·대시보드 같은 고밀도 UI에서 효과가 크며,
    앱 전체 퍼포먼스와 부드러움이 크게 향상된다.
반응형
Posted by 까칠코더
,