버그 잡이

SwiftUI 성능 이해 및 최적화 방법 알아보기 본문

SwiftUI

SwiftUI 성능 이해 및 최적화 방법 알아보기

버그잡이 2024. 6. 7. 13:22

SwiftUI를 쓰다보면 기존 AutoLayout 보다 성능이 떨어짐을 느낄 수 있습니다.

실제로 AutoLayout보다 메모리를 평균 20% 정도 더 차지하고(물론 어떤 뷰를 그리느냐에 따라 달라지겠지만)

이에 따라 성능상으로도 다소 떨어진다는 연구 결과도 있습니다.

이해 없이 SwiftUI를 쓰면 성능상 저하를 가져올 수 있기 때문에 SwiftUI는 어떤 식으로 비교 연산을 하고 어떻게 최적화 할 수 있을지 알아봅시다.

 

https://medium.com/@vladislavshkodich/mastering-swiftui-are-you-really-as-good-as-you-think-40a4953f7e88

위 글(Mastering SwiftUI: Are you really as good as you think)을 보고 관련 내용을 정리한 글 입니다.

 

글쓴이는 글에 앞서 두 가지 질문을 던집니다.

"SwiftUI View가 왜 Struct인지 알고 있니?"

 

"SwiftUI View가 빌드하는 동안 init이 얼마나 호출되는지 알고 있니?"

 

 

SwiftUI의 View는 왜 struct인가?

 

SwiftUI는 생명주기 동안 view를 수없이 많이 다시 그립니다. (reconstruct)

그렇기 때문에 Class 보다 가벼운 Struct를 채택하는게 성능상에 이점이 있습니다.

 

* stack 메모리

* dynamic dispatch

 

 

SwiftUI는 왜 View를 다시 그리나?

 

Reconstruct는 State가 변할때 시작됩니다.

단, State만 바뀌고 그 State가 View에 적용되지 않았을때는 View가 재구성 되지 않습니다.

 

*TIP

View의 body 안에 아래 코드를 추가하면 view 안에 있는 state가 바뀌었는지 로그로 확인해볼 수 있습니다.

let _ = Self.printChanges()

 

@State 대신에 @ObservableObject도 자주 쓰는데요. 

ObservableObject는 State와 다르게 view에 적용되지 않았을때도 view를 다시 그립니다.

이러한 이슈를 개선해서 나온 것이 @Observable입니다.

@Observable은 @State와 비슷하게 동작하기 때문에 바뀐 상태가 view에 적용되어 있지 않으면 body를 다시 호출하지 않습니다.

(단, iOS 17부터 사용 가능하네요.. ㅜ)

 

 

SwiftUI가 왜 struct로 되어있는지 View는 언제 다시 그려지는지 알아보았는데요.

다음으로 SwiftUI가 어떻게 View를 다시 그리는지 그 과정을 좀 더 구체적으로 알아보겠습니다.

 

 

SwiftUI와 Reconstruct 그 세부 과정

 

상태가 변경되었을때 SwiftUI가 하는 작업의 순서는 아래와 같습니다.

 

1. 새로운 상태와 기존 상태를 비교

2. 상태가 다르면 body 호출하여 새로운 뷰를 생성 후 기존 뷰와 비교

3. 새로운 뷰와 기존 뷰를 비교해서 다르면 새로운 뷰를 뷰 트리에 추가

 

이 원리에서 우리가 최적화 할 수 있는 방법을 찾을 수 있습니다.

 

1. body 내부를 깔끔하게 할 것

- body 내부에 불필요한 연산이 포함되어 있다면, View를 비교하는 과정에서 매번 리소스를 잡아먹게 됩니다.

 

2. body 안에 있는 View들을 ChildView로 쪼개서 관리

- 앞서 말했듯이 상태가 바뀌지 않으면 SwiftUI는 View를 비교하지 않습니다.

- 상태가 바뀐 하위 뷰만 다시 그려질 겁니다.

- 그렇기 때문에 상태와 연관이 있는 뷰 끼리 모아서 쪼개는 것이 좋습니다.

 

import SwiftUI

// 상태와 관련된 하위 뷰
struct ChildView: View {
    @Binding var counter: Int

    var body: some View {
        VStack {
            Text("Counter: \(counter)")
            Button(action: {
                counter += 1
            }) {
                Text("Increment")
            }
        }
    }
}

// 메인 뷰
struct ContentView: View {
    @State private var counter: Int = 0
    @State private var otherState: String = "Initial State"

    var body: some View {
        VStack {
            // 상태와 연관된 하위 뷰
            ChildView(counter: $counter)
            
            // 상태와 무관한 다른 뷰
            Text("Other State: \(otherState)")
            
            Button(action: {
                otherState = "State Changed"
            }) {
                Text("Change Other State")
            }
        }
    }
}

 

 

SwiftUI View가 비교하는 Method

SwiftUI가 View를 새로 만들고 기존 View 비교하는 연산을 한다고 설명했는데요.

Views들을 비교할때 SwiftUI는 3가지 방법을 사용한다고 합니다.

 

- memcmp (가장 빠름)

- Equality (조금 느림)

- Reflection (느림)

 

위부터 아래로 갈수록 성능이 떨어진다고 합니다.

이를 위해서 POD(Plain Old Data)라는 용어를 이해할 필요가 있는데요.

 

array, string, @State와 같은 value semantic의 상태 변수를 포함한 View StructNon-POD 객체고

int, bool 같이 순수한 View StructPOD 객체입니다.

근데 이게 좀 불명확해서 print(_isPOD(ContentView.self)) 로 찍어보는게 정확하다고 합니다.

 

POD에 대해서는 이 정도까지만 알아보고 

"그래서 어떻게 비교 성능을 올릴건데?" 라는 질문에 답하면

 

POD View는 memcmp를 사용하고

Non-POD View는 Reflection을 사용한다고 합니다. (단, Equatable을 재택하면 Equality를 사용)

 

그래서 Non-POD View를 쓸때는 Equatable을 채택하자. 이겁니다.

 

 

세 줄 요약

 

1. 비교를 최소화 하자

- ObservableObject 대신 @Observable 사용하기

 

2. body를 최적화 하자

- body에서 불필요한 연산 제거

- Child View를 분리해서 불필요한 View reconstruct 제거

 

3. 비교 성능을 최적화 하자.

- Non-POD View일때는 Equatable을 채택

 

 

반응형
Comments