SwiftUI에서 때 놓치기 쉬운 포인트 정리

2025. 11. 18. 00:27iOS

요새 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은 사용하지 않는 것이 좋아 보여요!

 

 

 

반응형