서론

SwiftUI는 새로운 View와 기존 View를 비교하고 변경된 부분만을 업데이트하여 성능을 최적화합니다.
그렇다면, SwiftUI는 어떻게 비교하고 렌더링 여부를 결정할까요?
이 글에서는 SwiftUI가 뷰를 어떻게 비교하고, 변경된 부분만 업데이트하는지에 대해 자세히 살펴보겠습니다.


SwiftUI가 View를 렌더링할 때의 과정

SwiftUI는 View의 Dependency가 변경되면 새로운 View 값을 생성(init) 합니다.
이때 "새로 생성된 View"는 렌더링을 위해 만들어진 것이 아닙니다.
만약 새로 생성된 View가 기존 View와 동일하다면, SwiftUI는 새로 만든 View를 버리고 아무 작업도 하지 않습니다.

Dependency 변경으로 init이 호출되지만, 새로 생성된 View가 이전과 동일하면 body가 호출되지 않습니다.

import SwiftUI

struct ContentView: View {
    @State private var number = 0

    var body: some View {
        VStack {
            Button("눌러 = \(number)") {
                number += 1
            }
            ChildView()
        }
    }
}

fileprivate struct ChildView: View {
    init() {
        print("init occured")
    }

    var body: some View {
        let _ = Self._printChanges()
        Text("호출되지 않는 편안함")
    }
}

새로 생성된 View가 이전과 동일하지 않다면 Body를 호출합니다.
그러나, body가 호출되었다고 해서 항상 화면이 렌더링되는 것은 아닙니다.
SwiftUI는 body가 동일한지 diffing을 통해 비교하고, 같으면 View Tree가 변경되지 않도록 렌더링을 생략합니다.

ViewModel 값이 업데이트되면 body를 호출합니다. 하지만, body가 이전과 동일하면 다시 렌더링하지 않습니다.

렌더링 한 경우렌더링 하지 않은 경우

let _ = Self._printChange() 를 통해 body가 호출되었음을 확인할 수 있습니다.
하지만, Instrument로 확인해 보면 렌더링이 일어나지 않아 CPU 점유율이 렌더링이 일어났을 때와 크게 차이가 남을 알 수 있습니다.


SwiftUI가 View를 비교하는 방법

SwiftUI는 아래의 세 가지 방법으로 View가 이전과 같은지 비교한다고 합니다.

  1. memcmp (빠름)
  2. equality (중간)
  3. reflection (느림)

memcmp

SwiftUI의 View 비교에서 memcmp는 값 타입에 대해 Equatable 대신 바이트 단위 비교가 가능한 경우에 사용합니다.
특히 POD 객체와 같이 property가 단순하고 추가적인 메모리 할당이나 상태 변화가 없는 구조체인 경우 memcmp를 이용해 비교합니다.

POD(Plain Old Data) 는 단순히 데이터를 저장하고 추가적인 복사, 이동, 소멸과 같은 특별한 동작을 가지지 않는 경우를 말합니다.
swiftlang.github

Swift는 다음 값 타임을 POD 타입으로 분류합니다.

  • Int, Float, Range<POD>, ClosedRange<POD>

다음 값 타입을 non-POD 타입으로 분류합니다.

  • String, Character, Array, Dictionary, Set, Error, Result, @State, @Binding ...

객체의 경우_isPOD(_:) 함수를 이용해 특정 타입이 POD 타입인지 확인할 수 있습니다.

equality

View가 Equatable을 채택한 경우 equality 방식으로 비교합니다.
Text(_:)와 같이 간단한 View는 SwiftUI에서 자체적으로 Equatable을 채택하고 있습니다.
자세한 내용은 아래 글을 참고하세요.
https://velog.io/@sustainable-git/EquatableView와-Equatable로-SwiftUI-View의-렌더링-성능-개선하기

reflection

@State, @Binding 과 같은 Dynamic property가 변경되면 body를 호출하고 diffing을 합니다.
SwiftUI는 이 때 reflection을 이용한 비교를 수행합니다.
reflection이 사용되는 경우 새로 생성된 View 트리와 기존 Tree를 비교하기 위해 View의 하위 Tree를 순회하며 차이를 분석합니다.
두 Tree가 동일하다면, SwiftUI는 기존 View를 재사용하고 렌더링을 생략합니다.

정리

비교 방식상황단계결과
mcmcmp상태 변경이 없는 경우init 만으로 비교메모리 비교를 통해 변경 사항을 확인하고, 동일한 경우 body 호출을 생략할 수 있음
equalityView가 Equatable을 채택한 경우== 함수를 사용해 비교값이 동일하면 불필요한 렌더링을 생략하고 기존 View를 사용
reflection동적 상태, 속성이 변경된 경우body가 호출되어 비교새로운 View Tree와 기존 View Tree를 비교하고 필요 시 렌더링

결론

SwiftUI는 렌더링 최적화를 위해 세 가지 방식(memcmp, equality, reflection)을 사용하여 View를 비교합니다.
이 중 POD 객체는 빠르게 비교할 수 있는 memcmp 방식을 사용하며, Equatable을 채택한 타입은 값 비교로 최적화를 이룹니다.
동적 속성(@State, @Binding)이 변경되면 reflection 방식으로 비교하고, 필요 시 렌더링을 생략하는 구조로 동작합니다.

성능 최적화를 위해 POD 객체를 사용하는 것이 유리하며, 그렇지 않다면 Equatable을 채택하여 뷰의 불필요한 렌더링을 최소화할 수 있습니다.

profile
https://github.com/sustainable-git

0개의 댓글

Powered by GraphCDN, the GraphQL CDN