[SwiftUI] 데이터 모델 상태 관찰하기

이정훈·어제
0

SwiftUI

목록 보기
8/8
post-thumbnail

SwiftUI의 View 렌더링 매커니즘은 데이터의 상태에 기반한다. SwiftUI에서 관찰하는 데이터가 변경되면 새로운 데이터를 바탕으로 새로운 UI를 화면에 그린다. 이때 우리는 몇가지 프로토콜과 프로퍼티 래퍼를 통해 간단하게 데이터의 상태에 따라 SwiftUI의 View를 자동으로 갱신할 수 있다.

이번 포스트에서는 SwiftUI의 상태 기반 렌더링 메커니즘을 이해하고, 데이터 모델을 SwiftUI View 업데이트에 반영하기 위한 ObervableObject protocol, @ObservedObject, @StateObject, @EnvironmentObject 등의 프로퍼티 래퍼를 활용해 데이터 변화에 따라 자동으로 뷰를 갱신하는 방법에 대해 알아본다.

ObervableObject protocol

protocol ObservableObject : AnyObject

ObservableObject는 protocol로 클래스 타입에만 채택 가능한 protocol이다. ObservableObject protocol을 채택한 클래스 타입은 objectWillChange를 통해 @Published property wrapper의 값이 변경 되기 전 변경된 값을 방출한다.

final class AppModel: ObservableObject {
	@Publsihed var value: Int = 0
}

ObservableObject의 구현체에서 변경된 값을 SwiftUI View에서 자동으로 반영하기 위해서는 아래의 property wrapper를 사용하여 데이터가 변경 되었을 때, SwiftUI View를 자동으로 업데이트 할 수 있다.

@ObservedObject

struct ContentView: View {
	@ObservedObject var appModel: AppModel
	
	var body: some View {
		...
	}
}

@ObservedObject property wrapper의 동작 방식은 다음과 같다. @ObservedObjectObervableObject protocol 구현체의 objectWillChange를 구독한다. 그리고 해당 객체의 프로퍼티 값이 변경되기 직전에 이벤트를 방출하여, 관련된 View가 다시 그려지도록 한다.

그런데 왜 didChange의 방식이 아닌, willChange의 방식으로 동작하게 되는걸까?

SwiftUI가 didChange가 아닌 willChange 방식을 채택한 이유는, 변경 직전에 알림을 보내 여러 상태 변화를 하나의 렌더링 사이클로 묶어 처리하기 위함이다. 이를 통해 불필요한 뷰 업데이트를 줄이고, 효율적인 렌더링 성능을 확보할 수 있다.

@ObservedObject의 단점

@ObservedObject은 주입받은 클래스 인스턴스의 생명 주기를 소유하지 않는다. 그 말인 즉, 해당 View 생명주기와 @ObservedObject로 감싸진 객체의 생명 주기가 반드시 일치하지 않으며, 상위 View의 상태 변화에 의해 예상치 못한 값을 가질 수 있음을 나타낸다.

@StateObject

struct ContentView: View {
	@StateObject var appModel: AppModel
	
	var body: some View {
		...
	}
}

@StateObject property wrapper는 위에서 언급한 @ObservedObject의 단점을 보완한 것으로, SwiftUI@StateObject로 선언된 변수는 body 실행 전 최초로 초기화하며, SwiftUI는 해당 객체를 View의 생명 주기와 함께 가져간다. 따라서 SwiftUIContentView가 더이상 필요하지 않다면, appModel을 메모리에서 해제한다.

@EnvironmentObject

struct ChildView: View {
	@EnvironmentObject var appModel: AppModel
	
	var body: some View {
		...
	}
}

struct ContentView: View {
	@StateObject var appModel: AppModel
	
	var body: some View {
		ChildView()
			.environmentObject(appModel)
	}
}

개발을 하다 보면, ObservableObject를 여러 계층을 거쳐 멀리 떨어진 하위 View까지 전달해야 하는 상황이 발생할 수 있다. 이때 SwiftUI는 @EnvironmentObject를 제공하며, 이를 통해 상위 View에서 한 번만 주입하면 하위 View 어디서든 해당 객체를 자동으로 주입받아 사용할 수 있다.

@Observable (iOS 17+)

@Observable은 iOS 17에서 도입된 Observation 기능의 일부로, SwiftUI에서 데이터 모델을 보다 간단하게 관찰할 수 있는 새로운 도구이다. 데이터 모델에 @Observable을 선언하는 것만으로, 별도의 ObservableObject 프로토콜 채택이나 @Published 속성 선언 없이도 뷰가 모델의 상태 변화를 자동으로 감지하고 UI를 갱신할 수 있다. 사용법은 아래와 같이 단순히 데이터 모델에서 @Observable만 추가로 선언해 주면 된다.

@Observable class AppModel {
	var value: Int = 0
}

위와 같이 @Observable로 선언해 준 것 만으로 SwiftUI는 데이터 모델이 가지고 있는 프로퍼티의 변화를 추적할 수 있고, 프로퍼티의 변화가 감지되면 SwiftUI의 body를 재랜더링 한다.

@Observable 매크로를 사용한다면, 더 이상 @ObservedObject, @StateObject, @EnvironmentObject는 사용하지 않아도 된다. 대신, @State, @Environment, @Bindable과 같은 property wrapper를 통해 더 간단하게 사용할 수 있다.

먼저, 데이터 모델이 뷰 내부에서 직접 관리되어야 하는 상태라면, @State 프로퍼티를 사용하여 해당 모델을 뷰의 생명주기와 함께 유지하고 관리할 수 있다.

struct ContentView: View {
    @State var model: AppModel?

    var body: some View {

        VStack {
            ...
        }
        .padding()
        .sheet(item: $model) { _ in
	        ...
        }
    }
}

위와 같이 View에 저장 되어 있는 model이라는 값의 상태에 따라 sheet를 띄울지, 말지를 결정하기 때문에 @State 프로퍼티 래퍼의 선언이 필요하다.

만약, 데이터 모델이 전역적으로 사용할 수 있는 환경이 필요하다면, @Environment 프로퍼티 래퍼를 사용할 수 있다.

struct ContentView: View {
	@Environment(AppModel.self) var appModel
	
	var body: some View {
		...
	}
}

또는, 해당 데이터 모델이 다른 View와 바인딩에만 사용된다면, 새롭게 등장한 @Bindable 프로퍼티 래퍼를 사용할 수 있다.

struct ContentView: View {
    @Bindable var appModel: AppModel
    
    var body: some View {
	    ...
    }
}

위의 세가지 고려사항에도 해당 되지 않는다면, 일반적인 프로퍼티로 선언할 수 있다.

SwiftUI는 인스턴스 마다 변경을 감지하기 때문에 배열이나, 옵셔널 타입에서도 동일하게 동작할 수 있다.

struct ContentView: View {
	var models: [AppModel]

    var body: some View {
        VStack {
            ForEach(models) { model in
                Text("\(model.count)")
                Button("Tap") {
                    model.count += 1
                }
            }
        }
        .padding()
    }
}

위와 같이 AppModel이 있을 때, SwiftUI는 각각의 AppModel 인스턴스의 변화를 감지하고 View를 새로 그리게 된다.

https://developer.apple.com/kr/videos/play/wwdc2020/10040/?time=925
https://developer.apple.com/kr/videos/play/wwdc2023/10149/

profile
새롭게 알게된 것을 기록하는 공간

0개의 댓글