2025. 11. 18. 00:27ㆍiOS
요새 SwiftUI로 화면을 빠르게 만드는데 재미가 들렸어요. 그치만 아무 생각없이 스유로 만들다보면 놓치기 쉬운 포인트들이 있어요. 이 포인트들을 놓치면 성능이 우려돼요. 체감상 느낄 수 없어도 스유에서 중요한 포인트들이라 한번 정리해봅니다.
StateObject 와 ObservedObject 차이 이해하기
StateObject
뷰가 객체의 소유일 경우에 선언합니다. 이 객체의 생명주기를 담당하는 뷰인 경우 사용합니다.
뷰가 생성될 때 객체를 힙 할당하고 id/token을 부여합니다. 뷰가 리렌더링될 때 해당 id/token을 기반으로 기존 인스턴스를 재사용합니다.
SwiftUI는 StateObject 객체를 별도 힙에 객체를 저장하고 identity를 유지하여 단 한번만 생성됩니다.
ObservedObject
뷰가 객체의 소유가 아니고 관찰만 할 경우에 선언합니다. 하나의 객체를 여러 뷰에서 공유하고 싶을 때 사용합니다.
ObservedObject로 선언하게 되면 뷰가 렌더링 될 때마다 힙할당을 하게 됩니다.
@Observable 매크로가 나온 이후 위 선언들은 모두 의미가 없어졌어요.
@Observable로 선언된 인스턴스는 SwiftUI가 자동으로 변화를 감지하고 @State로 선언하면 생명주기를 관리해줘요.
View body diffing 알고리즘 이해하기
SwiftUI 렌더링 기본 매커니즘
SwiftUI는 상태가 변경될 때마다 뷰트리를 새로 생성하여 이전 뷰트리와 비교하여 뷰를 리렌더링합니다.
1. @State, @StateObject 등 상태 변경
2. 영향 받는 뷰의 body가 호출되며 뷰트리 구성
3. 이전 뷰트리와 diffing 알고리즘 실행
4. 변경된 부분 UI 업데이트
SwiftUI에서 성능이 좋다는 것은 위 과정이 막힘없이 이루어진다는 것을 의미해요.
만약 하나의 과정에서 막힌다면 뷰 렌더링이 늦어지고 버벅이게 될 거예요.
View를 최대한 가볍게 만드는 것이 좋습니다!
만약 무거운 View가 있다면, placeholderView를 만들어두는 것이 좋아요.
diffing 알고리즘
View가 들고 있는 property들을 비교합니다.
property가
Equatable 하다면,
👉🏻 == 로직을 통해 비교
Value Type이라면,
👉🏻 Value 타입 내부를 전부 비교
Reference Type이라면,
👉🏻 동일한 인스턴스인지 비교
Closure라면,
👉🏻 무조건 다른 뷰임
뷰가 들고 있는 프로퍼티가 변하면 그 뷰는 diffing 알고리즘을 실행하고 이는 뷰 업데이트 로직을 탑니다.
struct Test1View: View {
@State var count: Int = 0
var body: some View {
VStack(spacing: 10) {
TextView(count: count)
Button1View(count: $count)
Button2View(count: $count)
}
.padding()
}
}
struct TextView: View {
let count: Int
var body: some View {
Text("색깔")
.background(count % 2 == 0 ? Color.red : Color.green)
}
}
struct Button1View: View {
@Binding var count: Int
var body: some View {
Button {
count += 2
} label: {
Text("짝수")
}
}
}
위 예제 코드에서 Button1을 계속 클릭하면, TextView는 계속 업데이트돼요.
이를 개선하려면 아래처럼 isRed 프로퍼티로 변경하면 이 프로퍼티가 변경될 때만 뷰가 업데이트 됩니다.
struct TextView: View {
let isRed: Bool
var body: some View {
Text("색깔")
.background(isRed ? Color.red : Color.green)
}
}
아래는 Button1을 6번 눌렀을 때 인스트루먼트예요. isRed는 계속 true이기 때문에 TextView는 생성될 때 1번 body가 호출되었습니다. Button2도 count 프로퍼티를 들고 있기 때문에 뷰가 업데이트된 것을 볼 수 있어요.

UIViewRepresentable 조심하기
UIView를 SwiftUI에서 쓰기 위해선 아래와 같이 위 프로토콜을 채택해주어야 합니다.
struct AttributedText: UIViewRepresentable {
private let string: String
private let color: UIColor
init(string: String, color: UIColor) {
self.string = string
self.color = color
}
func makeUIView(context: Context) -> UILabel {
let label = UILabel()
label.text = string
label.textColor = color
return label
}
func updateUIView(_ uiView: UILabel, context: Context) {
uiView.textColor = color
}
}
하지만, 위 프로토콜을 채택한 스유뷰는 업데이트가 어느 시점에 되는지 명확히 알기 어려워요.
struct Test2View: View {
@State var selectedName: [String] = []
private let items: [Item] = [
.init(name: "A"),
.init(name: "B"),
.init(name: "C"),
.init(name: "D"),
.init(name: "E"),
.init(name: "F")
]
struct Item: Hashable {
let id = UUID()
let name: String
}
var body: some View {
ScrollView {
VStack {
ForEach(items, id: \.id) { item in
Button {
// 선택한 아이템 추가/제거 로직 구현
} label: {
TextView(
name: item.name,
isSelected: isSelected(item: item)
)
}
}
}
.padding(.top, 16)
.padding(.horizontal, 16)
}
}
private func isSelected(item: Item) -> Bool {
selectedName.contains(item.name)
}
}
private struct TextView: View {
let name: String
let isSelected: Bool
var body: some View {
AttributedText(
string: name,
color: isSelected ? .blue : .red
)
}
}
위 Test2View를 Instrument로 돌리고 버튼을 2번 클릭해서 디버깅해보면 아래와 같이 AttributedText가 34번 업데이트 된 것을 볼 수 있어요. Time Profiler로 계속 디버깅해보면 UILabel 내부 attributedString을 변경하는 코드들이 계속 실행되는 것으로 보이는데 이 곳은 개발자들이 건들일 수가 없는 부분으로 보입니다.
그래서 최대한 UIViewRepresentable은 사용하지 않는 것이 좋아 보여요!

'iOS' 카테고리의 다른 글
| Link Binary with Libraries (Crash - Library not loaded) (0) | 2025.11.20 |
|---|---|
| StoreKit 2 기반 인앱결제 / 구독 정리하기 (0) | 2025.11.14 |
| @Published protocol에 정의하기 (0) | 2022.11.05 |
| ViewController 생명주기의 몰랐던 점 (0) | 2022.10.24 |
| [CoreAnimation] Layer에 CoreAnimation을 무한으로 체이닝할 수 있을까? (2) | 2022.10.23 |