Develup

[SwiftUI] SwiftUI의 Redraw 프로세스: 효율적인 UI 업데이트 완벽 가이드 본문

Swift/SwiftUI

[SwiftUI] SwiftUI의 Redraw 프로세스: 효율적인 UI 업데이트 완벽 가이드

Develup 2025. 3. 6. 18:28
반응형

반응형


SwiftUI가 iOS 개발 세계에 등장한 이후, 많은 개발자들이 선언적 UI 패러다임으로 전환했습니다. 하지만 이 새로운 프레임워크의 성능을 최적화하려면 화면 업데이트가 어떻게 이루어지는지, 즉 SwiftUI의 redraw 프로세스를 이해하는 것이 필수적입니다. 이 글에서는 SwiftUI의 화면 갱신 메커니즘, 불필요한 리드로우를 방지하는 방법, 그리고 앱의 성능을 최적화하는 기법에 대해 알아보겠습니다.

SwiftUI는 상태 변화에 따른 UI 업데이트를 자동으로 처리하지만, 이 과정이 어떻게 작동하는지 이해하면 더 효율적인 앱을 만들 수 있습니다. 특히 복잡한 화면에서는 불필요한 redraw가 성능 저하의 주요 원인이 될 수 있기 때문입니다.

 

SwiftUI의 View 재평가와 Redraw는 어떻게 다른가?

SwiftUI에서 가장 흔히 오해하는 부분 중 하나는 '뷰 재평가'와 '실제 redraw'의 차이입니다.

뷰 재평가 (View Reevaluation)

뷰 재평가는 SwiftUI가 뷰 계층 구조를 다시 계산하는 과정입니다.

struct ContentView: View {
    @State private var counter = 0
    
    var body: some View {
        VStack {
            Text("Count: \(counter)")
            Button("Increment") {
                counter += 1
            }
            HeavyComputationView() // 이 뷰의 body는 재평가되지만 항상 다시 그려지는 것은 아님
        }
    }
}

위 예제에서 counter 값이 변경되면 ContentView의 body 프로퍼티가 다시 실행됩니다. 이것이 '뷰 재평가'입니다. 그러나 중요한 점은 HeavyComputationView가 counter에 의존하지 않는다면, SwiftUI는 해당 뷰를 다시 그리지 않습니다.

실제 Redraw

실제 redraw는 화면에 픽셀을 다시 그리는 작업입니다.

struct CounterView: View {
    @State private var counter = 0
    
    var body: some View {
        Text("Count: \(counter)")
            .onTapGesture {
                counter += 1
            }
    }
}

이 예제에서 counter가 변경되면 Text 뷰의 내용이 변경되므로 실제로 redraw가 발생합니다. 이전 텍스트를 지우고 새 텍스트를 그리는 작업이 필요하기 때문입니다.

SwiftUI의 Redraw 트리거 요인

SwiftUI에서 뷰 리드로우는 여러 요인에 의해 트리거될 수 있습니다:

1. 상태 변경

struct StateExample: View {
    @State private var text = "Hello"
    
    var body: some View {
        VStack {
            Text(text)
            Button("Change") {
                text = "World" // 이 상태 변경은 Text 뷰의 redraw를 트리거
            }
        }
    }
}

@State, @StateObject, @ObservedObject, @EnvironmentObject 등의 프로퍼티 래퍼를 사용하면 상태 변경 시 관련 뷰가 다시 그려집니다.

2. 환경 변수 변경

struct EnvironmentExample: View {
    @Environment(\.colorScheme) var colorScheme
    
    var body: some View {
        Text("Current mode: \(colorScheme == .dark ? "Dark" : "Light")")
            // 기기의 색상 모드가 변경되면 이 Text 뷰가 redraw됨
    }
}

@Environment를 통해 시스템 설정(다크 모드 등)이 변경되면 해당 환경 변수를 관찰하는 뷰가 업데이트됩니다.

3. 애니메이션

struct AnimationExample: View {
    @State private var scale: CGFloat = 1.0
    
    var body: some View {
        Circle()
            .frame(width: 100, height: 100)
            .scaleEffect(scale)
            .onTapGesture {
                withAnimation(.spring()) {
                    scale = scale == 1.0 ? 2.0 : 1.0 // 애니메이션은 여러 redraw를 트리거
                }
            }
    }
}

애니메이션은 여러 프레임에 걸쳐 뷰의 속성을 변경하므로 연속적인 redraw를 발생시킵니다.

효율적인 Redraw를 위한 핵심 기법

1. ID와 동일성 활용하기

SwiftUI는 뷰의 ID를 통해 변경 사항을 추적합니다.

struct IdentityExample: View {
    @State private var items = [1, 2, 3]
    
    var body: some View {
        List {
            ForEach(items, id: \.self) { item in
                Text("Item \(item)")
            }
            Button("Shuffle") {
                items.shuffle() // ID를 통해 변경된 순서만 업데이트됨
            }
        }
    }
}

.id() 수정자를 사용하여 뷰의 ID를 명시적으로 설정할 수도 있습니다:

Text("Dynamic content")
    .id(dynamicValue) // dynamicValue가 변경될 때만 이 뷰를 새로 생성

2. Equatable 프로토콜 구현

복잡한 사용자 정의 뷰의 경우, Equatable 프로토콜을 구현하면 SwiftUI가 변경 사항을 더 효율적으로 감지할 수 있습니다.

struct ComplexView: View, Equatable {
    let data: MyData
    
    var body: some View {
        // 복잡한 뷰 구현
    }
    
    // Equatable 구현으로 SwiftUI는 실제 데이터가 변경되었을 때만 뷰를 업데이트
    static func == (lhs: ComplexView, rhs: ComplexView) -> Bool {
        return lhs.data == rhs.data
    }
}

3. @ViewBuilder와 지연 평가 활용

@ViewBuilder를 사용하면 조건부 뷰 생성을 최적화할 수 있습니다.

struct OptimizedView: View {
    @State private var showDetail = false
    
    var body: some View {
        VStack {
            Button("Toggle Detail") {
                showDetail.toggle()
            }
            
            if showDetail {
                detailView // 필요할 때만 생성됨
            }
        }
    }
    
    @ViewBuilder
    var detailView: some View {
        Text("Detail information")
        Image("DetailImage")
        // 복잡한 서브뷰들...
    }
}

4. 계산 비용이 높은 작업 분리

struct OptimizedHeavyView: View {
    let data: [Int]
    
    // 비싼 계산은 한 번만 수행
    private let processedData: [String] = {
        // 데이터 처리 로직...
        return ["Processed Item 1", "Processed Item 2"]
    }()
    
    var body: some View {
        List(processedData, id: \.self) { item in
            Text(item)
        }
    }
}

위 예제에서 processedData는 뷰가 재평가될 때마다 재계산되지 않습니다.

불필요한 Redraw 방지 기법

1. 상태 관리 최적화

struct OptimizedStateManagement: View {
    // 잘못된 방법: 모든 것을 하나의 상태로 관리
    // @State private var allData = ComplexObject()
    
    // 더 나은 방법: 관련 상태만 그룹화
    @State private var userName = ""
    @State private var isLoggedIn = false
    
    var body: some View {
        VStack {
            if isLoggedIn {
                Text("Welcome, \(userName)!")
            } else {
                TextField("Name", text: $userName)
                Button("Login") {
                    isLoggedIn = true
                }
            }
        }
    }
}

2. 상태 변경 범위 제한

struct ParentView: View {
    var body: some View {
        VStack {
            HeaderView()
            ContentView() // 이 뷰 내부의 상태 변경은 HeaderView에 영향을 주지 않음
            FooterView()
        }
    }
}

struct ContentView: View {
    @State private var localState = 0
    
    var body: some View {
        Button("Update") {
            localState += 1 // 이 상태 변경은 ContentView만 업데이트
        }
        Text("State: \(localState)")
    }
}

3. EquatableView 활용

struct ExpensiveView: View, Equatable {
    let data: MyData
    
    var body: some View {
        // 복잡한 뷰 레이아웃...
    }
    
    static func == (lhs: ExpensiveView, rhs: ExpensiveView) -> Bool {
        return lhs.data == rhs.data
    }
}

struct ParentView: View {
    @State private var data = MyData()
    @State private var otherState = 0
    
    var body: some View {
        VStack {
            Button("Change other state") {
                otherState += 1 // 이 변경은 ExpensiveView의 redraw를 트리거하지 않음
            }
            Text("Other state: \(otherState)")
            EquatableView(content: ExpensiveView(data: data))
        }
    }
}

EquatableView는 내용이 변경되었을 때만 컨텐츠를 다시 그립니다.

 

자주 묻는 질문 (FAQ)

SwiftUI에서 모든 상태 변경이 redraw를 트리거하나요?

아니요. SwiftUI는 상태 변경에 따라 뷰 계층 구조를 재평가하지만, 실제로 변경된 부분만 다시 그립니다. 부모 뷰가 재평가되더라도 자식 뷰의 입력이 변경되지 않으면 자식 뷰는 다시 그려지지 않습니다.

리스트나 스크롤 뷰에서 성능을 최적화하는 방법은?

  1. 고유 ID를 제공하여 변경 사항을 정확히 추적하도록 합니다.
  2. LazyVStack이나 LazyHStack을 사용하여 화면에 보이는 요소만 로드합니다.
  3. 셀 콘텐츠를 작게 유지하고 복잡한 레이아웃은 피합니다.
  4. 대량의 데이터를 처리할 때는 페이지네이션을 고려합니다.

상태 변경 없이 뷰를 강제로 다시 그리려면 어떻게 해야 하나요?

드문 경우지만, .id(UUID()) 수정자를 사용하여 뷰를 강제로 다시 그릴 수 있습니다. 그러나 이 방법은 상태 관리가 올바르게 되지 않았음을 의미할 수 있으므로 신중하게 사용해야 합니다.

Button("Force Redraw") {
    // 주의: 이 방법은 성능에 영향을 미칠 수 있음
    forceRedraw.toggle()
}
.id(forceRedraw ? UUID() : nil)

@State와 @StateObject의 차이점은 무엇인가요?

@State는 값 타입(구조체, 열거형)을 위한 것이며, @StateObject는 참조 타입(클래스)을 위한 것입니다. @StateObject는 ObservableObject 프로토콜을 준수하는 객체의 수명 주기를 관리하고, 해당 객체의 @Published 속성이 변경될 때 뷰를 업데이트합니다.

결론

SwiftUI의 redraw 프로세스를 이해하는 것은 효율적이고 성능이 좋은 앱을 개발하기 위한 핵심입니다. 뷰 재평가와 실제 redraw의 차이를 이해하고, 상태 관리를 최적화하며, 적절한 도구와 기법을 활용하면 사용자 경험을 크게 향상시킬 수 있습니다.

가장 중요한 점은 모든 UI 최적화의 기본 원칙을 따르는 것입니다: 필요한 것만 업데이트하고, 계산은 최소화하며, 사용자가 볼 수 있는 것에 집중하세요. SwiftUI는 이미 많은 최적화를 자동으로 수행하지만, 개발자가 프레임워크의 작동 방식을 이해하고 협력할 때 최상의 결과를 얻을 수 있습니다.

이러한 기법들을 적용하면 더 부드럽고 반응성이 뛰어난 SwiftUI 앱을 만들 수 있을 것입니다.

반응형