[SwiftUI] View는 언제 Invalidate될까

강대훈·2025년 9월 8일
post-thumbnail

아래의 코드는 어떻게 동작할까요? 버튼을 누를 때마다 숫자와 색은 계속 바뀔까요?

struct MainView: View {
    @State private var title: String = "Hello World"
    
    var body: some View {
        Button {
            title = "Clicked"
        } label: {
            Text(title)
        }
        SubView(title: $title)
	        .background(.random)
    }
}

struct SubView: View {
    @Binding var title: String
    
    var body: some View {
        Text("\(Int.random(in: 0...100))")
    }
}

extension ShapeStyle where Self == Color {
    static var random: Color {
        Color(
            red: .random(in: 0...1),
            green: .random(in: 0...1),
            blue: .random(in: 0...1)
        )
    }
}

"Hello World" 에서 "Clicked" 로 상태가 변경될 때만 숫자와 배경색이 바뀌고 그 이후로는 바뀌지 않는 것을 확인할 수 있습니다.

title은 계속해서 데이터를 받고 있지만 상태는 단 한 번만 변하고 있습니다.

SwiftUI는 diffing 엔진을 활용하여 상태가 변했는지 체크하고 만약 변경됐다면 기존의 body를 Invalidate합니다.

만약 아래와 같이 하위뷰에서 Binding이 아니라 일반 저장 속성을 사용하면 어떻게 될까요?

struct MainView: View {
    @State private var title: String = "Hello World"
    
    var body: some View {
        Button {
            title = "Clicked"
        } label: {
            Text(title)
        }
        SubView(title: title)
            .background(.random)
    }
}

struct SubView: View {
    let title: String
    
    var body: some View {
        Text("\(Int.random(in: 0...100))")
    }
}

마찬가지로 숫자와 배경색이 한 번만 바뀌는 것을 확인할 수 있습니다.

SwiftUI가 스스로 추적하고 UI를 Invalidate하는 장치는 오직 Dynamic Property입니다. 하지만 하위 뷰에서 Dynamic Property를 가지지 않는데, 어떻게 숫자가 바뀐 걸까요?

기본적으로 SwiftUI에서의 View는 값 타입이기 때문에 View 내부에서 저장 속성을 변경할 수 없습니다. 그렇기 때문에 현재 상위 뷰로부터 간접적으로 변경되고 있습니다.

MainView의 body는 Dynamic Property의 변경으로 인해서 invalidate되었습니다. 그리고 SubView도 다시 초기화되었습니다. 여기서 마침 SubView에 존재했던 저장 속성이 새로 초기화 되면서 상태가 변경되었습니다.

해당 경우에 SwiftUI는 상태가 변경되었음을 판단하고 body를 Invalidate합니다. 이로써 반드시 Dynamic Property가 존재해야만 View가 다시 그려지는 것이 아니라는 것을 알 수 있습니다.

아래의 코드처럼 SubView의 저장 속성을 다른 상태로 초기화하지 않는다면 body는 Invalidate되지 않는 것을 확인할 수 있습니다.

struct MainView: View {
    @State private var title: String = "Hello World"
    
    var body: some View {
        Button {
            title = "Clicked"
        } label: {
            Text(title)
        }
        
        SubView()
            .background(.random)
    }
}

struct SubView: View {
    let title: String = "Hello World"
    
    var body: some View {
        Text("\(Int.random(in: 0...100))")
    }
}

State는 상태가 변경되어야만 diffing을 적용하고 body가 Invalidate가 된다는 것을 알게 되었습니다. 그에 따라서 하위뷰도 마찬가지로 상태가 변경된다면 Binding, 기본 저장 속성 할 거 없이 body는 Invalidate됩니다.


이번엔 ObservableObject에서 Published를 사용한 경우입니다. 아래의 코드는 어떻게 동작할까요?

final class MainViewModel: ObservableObject {
    @Published var title: String = "Hello World"
    
    func changeTitle() {
        title = "Clicked"
    }
}

struct MainView: View {
    @StateObject var viewModel = MainViewModel()
    @State private var title: String = "Hello World"
    
    var body: some View {
        Button {
            viewModel.changeTitle()
        } label: {
            Text(viewModel.title)
        }
        
        SubView(title: $viewModel.title)
            .background(.random)
    }
}

struct SubView: View {
    @Binding var title: String
    
    var body: some View {
        Text("\(Int.random(in: 0...100))")
    }
}

State를 사용했을 때와 다르게 title의 상태가 변하지 않았음에도 배경색이 계속해서 변하고 있습니다. (= MainView의 body가 계속해서 Invalidate 되고 있다.)

이는 하위뷰에 Binding 대신 일반 저장 속성을 사용해도 마찬가지입니다.

왜 이런 차이가 발생했을까요? 이것은 SwiftUI가 의도한 동작일까요?

Published의 메커니즘은 willSet 상황에서 퍼블리싱이 동작합니다. 즉, 새로운 값이 실제로 저장되기 전에 Subscriber에게 신호를 줍니다. 여기서 objectWillChange 가 호출되는데, 이것이 view를 Invalidate하라는 신호를 주게 됩니다.

여기서 SwiftUI는 Invalidate하라는 신호를 받았으니 실제로 상태가 변했는지 diffing하지 않고 곧바로 View를 Invalidate하게 됩니다.

정리하면 다음과 같습니다.

State상태가 실제로 변경되었는지 확인하고 View를 Invalidate 할지 결정하지만, PublishedwillSet이 호출되었다면 상태가 실제로 변경되었는지 확인하지 않고 View를 바로 Invalidate 합니다.


그렇다면 하위뷰가 ViewModel을 ObservedObject로 받으면 어떻게 될까요?

struct MainView: View {
    @StateObject var viewModel = MainViewModel()
    @State private var title: String = "Hello World"
    
    var body: some View {
        Button {
            viewModel.changeTitle()
        } label: {
            Text(viewModel.title)
        }
        
        SubView(viewModel: viewModel)
            .background(.random)
    }
}

struct SubView: View {
    @ObservedObject var viewModel: MainViewModel
    
    var body: some View {
        Text("\(Int.random(in: 0...100))")
    }
}

SubView에 있는 숫자도 계속해서 바뀌는 것을 볼 수 있습니다. 이것은 ObjectWillChange 의 영향이 ObservedObject까지 미친다는 것을 확인할 수 있습니다.

아무래도 EnvironmentObject 또한 동일하지 않을까 싶습니다.


마무리하면서 정리하면 이렇게 될 거 같습니다.

  • PublishedState는 Invalidate하는 조건이 다르다.
  • 하위뷰에서 Dynamic Property를 사용하지 않아도, 하위뷰를 다시 초기화하는 과정에서 Invalidate될 수 있다.
  • 상위뷰가 Invalidate된다고 해서 하위뷰가 반드시 Invalidate되는 것은 아니다.

참고자료

https://sujinnaljin.medium.com/swiftui-view%EB%A5%BC-redraw-%ED%95%98%EB%8A%94-%EC%A1%B0%EA%B1%B4%EC%9D%80-%EC%96%B4%EB%96%BB%EA%B2%8C-%EB%90%A0%EA%B9%8C-db3d7551df2

https://developer.apple.com/kr/videos/play/wwdc2020/10040/

0개의 댓글