SwiftUI의 View는 상태 변수(State)의 값이 변할 때마다 View를 다시 업데이트하는 특징이 있습니다.
그러나, 값이 변하더라도 View를 다시 그릴 필요가 없는 경우에도 이러한 특성은 불필요한 업데이트를 발생시켜 성능에 영향을 줄 수 있습니다.
이런 경우 EquatableView를 사용하면 SwiftUI가 값의 변화를 비교하여 불필요한 업데이트를 방지할 수 있습니다.
View가 Equatable을 채택하면, SwiftUI가 해당 View의 업데이트를 요청할 때 현재 값과 이전 값이 동일한지 비교하여, 값이 같다면 View를 다시 그리지 않습니다.
사용법은 아래와 같습니다.
EquatableView(content: ChildView(number: number)
// 또는
ChildView(number: Number)
.equatable()
또는 View가 Equatable을 채택하면 Equatable(content:)
또는 .equatable()
없이도 동일한 방식으로 작동합니다.
Equatable 프로토콜을 View가 채택한 경우, 별도로 EquatableView로 감싸지 않더라도 값 변화 감지를 통해 불필요한 렌더링을 방지할 수 있습니다.
struct NumberParityView: View, Equatable {
...
}
버튼을 누르면 number 값을 임의로 변경하는 부모 View가 있습니다.
자식 View는 number의 값이 짝수인지 홀수인지에 따라 다르게 그려지도록 구성되었습니다.
버튼을 눌러 number의 값이 바뀌면 자식 View는 매번 init이 호출됩니다.
그러나 body는 홀짝이 바뀌는 경우에만 호출되므로, 이를 통해 불필요한 업데이트를 줄이고 최적화할 수 있습니다.
import SwiftUI
struct ContentView: View {
@State private var number = 0
var body: some View {
VStack {
NumberParityView(number: number)
.padding()
.border(number % 2 == 0 ? .red : .blue)
Button("Random Number Generator") {
randomNumberButtonTouched()
}
Text("number = \(number)")
}
}
private func randomNumberButtonTouched() {
number = Int.random(in: 1...100)
}
}
struct NumberParityView: View, Equatable {
@State private var flag = false
let number: Int
init(number: Int) {
self.number = number
print("init")
}
var body: some View {
let _ = Self._printChanges()
Text(number % 2 == 0 ? "EVEN" : "ODD")
}
static func == (lhs: Self, rhs: Self) -> Bool {
lhs.number % 2 == rhs.number % 2
}
}
EquatableView를 사용할 때 유의할 점이 있습니다.
위 예제에서 View 내에 @State
값이 없으면 SwiftUI는 해당 View를 매번 새로운 상태로 간주하고 View를 업데이트할 때마다 body를 새로 호출하게 됩니다.
실제로 @State private var flag
가 없으면 body가 매번 렌더링됨을 확인할 수 있습니다.
단, Equatable을 준수하는 모델을 사용하는 경우에는 정상적으로 동작합니다.
import SwiftUI
struct ContentView: View {
@State private var numberModel = NumberModel(number: 0)
var body: some View {
VStack {
NumberParityView(numberModel: numberModel)
.padding()
.border(numberModel.number % 2 == 0 ? .red : .blue)
Button("Random Number Generator") {
randomNumberButtonTouched()
}
Text("number = \(numberModel.number)")
}
}
private func randomNumberButtonTouched() {
numberModel = NumberModel(number: Int.random(in: 0...1))
}
}
struct NumberParityView: View, Equatable {
let numberModel: NumberModel
init(numberModel: NumberModel) {
self.numberModel = numberModel
print("init")
}
var body: some View {
let _ = Self._printChanges()
Text(numberModel.number % 2 == 0 ? "EVEN" : "ODD")
}
static func == (lhs: Self, rhs: Self) -> Bool {
lhs.numberModel == rhs.numberModel
}
}
struct NumberModel: Equatable {
let number: Int
}
내부에 @State
가 없다면 View를 Value type으로 간주합니다.
이 경우 View 내부의 상태를 SwiftUI가 추적하지 않기 때문에 Equatable을 채택했다 하더라도 매번 View가 렌더링하게 됩니다.
데이터 모델이 Equatable을 준수하기 때문에 @State
없이도 모델이 동일한지 비교하고, 동일하면 다시 렌더링하지 않습니다.
SwiftUI의 EquatableView는 렌더링 성능을 최적화하는 데 유용하게 사용할 수 있습니다.
특히 값이 자주 변하지 않거나, 특정 조건에서만 View가 변경될 필요가 있을 때 성능을 크게 개선할 수 있습니다.
그러나 모든 경우에 적합한 것은 아니며, 자주 변하는 데이터의 경우 오히려 비교 오버헤드로 인해 성능 저하가 발생할 수 있습니다.
또한 비교가 복잡한 데이터 타입에서는 Equatable을 구현하는 비용이 다시 렌더링하는 비용보다 클 수 있어 주의가 필요합니다.