2023.09.22 2차 수정!
하아.. 길고도 길었던 고난의 이해시간을 거쳐 드디어 이해가 된 아니, 쓰면서 내 이해가 맞나 확인하기 위해서 글을 씁니다.
위의 프로퍼티 래퍼들을 처음 보게 된 것은 Apple의 공식 document로 나와있는 SwiftUI Tutorial을 할 때 였지만, 약 1달 가량이 지난 지금 이해를 마치고 기분 좋은 마음으로 글을 작성해 봅니다!ㅎㅎ
먼저 제 이해를 도운 Reference들을 첨부하겠습니다.
https://purple.telstra.com/blog/swiftui---state-vs--stateobject-vs--observedobject-vs--environme
개인적으로는 정말 좋은 예시와 너무 좋은 설명을 동반하고 있다고 생각하기에, 정독하여 한번 보시는 것을 추천드립니다.
자, 이제 한번 정리를 시작해 보겠습니다.
먼저 @StateObject, @ObservedObject, @EnvironmentObject 등의 프로퍼티 래퍼들은 하나의 공통점을 가집니다.
바로 데이터가 업데이트 되었을때, 뷰의 업데이트를 사용해주기 위해서 존재한다는 것인데요,
@State 라는 데이터의 상태를 관찰하며 값이 변경되면 뷰를 다시 그려주는 간단한 래퍼도 존재하지만, @State와 같은 프로퍼티 래퍼는 class와 같이 Reference 타입인 객체를 나타내기에 한계가 존재합니다.
어떤 한계가 있는지 예시를 적어보겠습니다.
class MyClass {
var num: Int = 0
}
@State var myClass: MyClass = MyClass()
예시 코드가 100% 정확한 코드는 아니지만, @State의 한계에 대해 이해를 돕고자 적어보자면,
@State로 다뤄주는 myClass 인스턴스 즉 객체(인스턴스와 객체는 엄연히 다르게 표현 될 수 있지만, 글에서의 맥락과 벗어남으로 같게 생각하겠습니다.)
myClass.num += 1
이라는 코드가 있을때,
num이 현재 화면에 표시되고 있는 값이라면, myClass.num += 1 이 아무리 계속되어도 myClass의 num값은 변하겠지만, 화면은 업데이트가 되지 않습니다.
이유로는 @State의 값이 MyClass객체 자체를 가리키고 있기 때문에, 객체안의 num값은 변했을지라도, 객체자체의 변한 점은 없다고 인식되기 때문입니다.
따라서, 이럴 경우를 위해 @StateObject와 같은 프로퍼티 래퍼가 필요한 것입니다.
@StateObject는 직역한 단어의 뜻과 같이 객체를 위한 프로퍼티 래퍼입니다.
@State가 단순한 Value타입의 값들(String, Int...)에 최적화 되었다면, 객체와 같은 Reference타입의 변화에는 @StateObject가 최적이라고 볼 수 있죠.
@StateObject var myClass: MyClass = MyClass()
의 꼴로 생성되며, @State와 다른 점이라고는 @State가 @StateObject로 바뀐 점 하나 뿐입니다.
이름만 보더라도, @State와 @StateObject의 차이는 객체를 위한 프로퍼티 래퍼냐 아니냐? 로 볼 수 있습니다.
이제 위에 @State였기 때문에 나타나던 문제가 사라지고, myClass.num += 1 이 진행되면 그 값의 변경사항에 따라 뷰가 없데이트 되어 사용자로 하여금 바뀐 데이터를 볼 수 있게 됩니다. 바로 @StateObject가 붙어 객체의 값까지 관찰을 하고 있기 때문입니다.
그럼 @StateObject만 써준다면 아무 문제가 없이 어떠한 페이지에서도 그 값을 다룰 수 있게 될까요?
물론 아닙니다
@StateObject는 보통 사용되는 뷰들이 있다면, 제일 상위의 뷰에 존재해야 합니다. 왜 그래야 할까요?
import SwiftUI
class MyClass: ObservableObject {
@Published var num: Int = 0
}
struct StateObjectNavigation: View {
var body: some View {
NavigationView {
NavigationLink(destination: StateObjectNextPage()) {
Text("Go next page")
}
}
}
}
struct StateObjectNextPage: View{
@StateObject var myClass = MyClass()
var body: some View {
VStack {
Text(String(myClass.num))
Button(action: {
myClass.num += 1
}, label: {
Text("add StateObject num")
})
}
}
}
struct StateObjectNavigation_Previews: PreviewProvider {
static var previews: some View {
StateObjectNavigation()
}
}
제일 첫 코드블락인 MyClass의 구성부터 보게 되면,
@StateObject를 사용하기 위해 MyClass 클래스는 ObservableObject라는 프로토콜을 준수하고, num에는 @Published라는 새로운 프로퍼티 래퍼가 붙어있습니다.
ObservabelObject를 써줌으로써 다른 뷰에서 @StateObject로 받아줄 수 있게 되고,
@Published가 변수 앞에 써져 있음으로 num이라는 변수가 변하는 것을 내뿜어주어서 확인할 수 있게해 num값이 변하는 것을 확인 가능하게 해줍니다.
자 이제 왜 상위에 존재해야 하는지에 대해서 찾아보자면,,
위 코드를 실행해보면, myclass의 num값 자체는 잘 변하지만, NavigationLink를 타고 나왔다 다시 들어가 본다면, num의 값은 초기의 0 값으로 복귀되어 있을 것이다. StateObjectNextPage가 초기화 되면서 myClass도 역시 다시 초기화 된다는 말입니다.
-> myClass의 경우에 전 페이지로 돌아갔다가 다시 들어오게 되면, 새로운 객체가 생기고, 원래의 객체는 deinit으로 초기화됩니다.
그말은 그럼 @StateObject가 StateObjectNextPage가 아니라 StateObjectNavigation에 있으면 해결되는 문제라는 거 아닌가?
맞습니다. 그래서 하위 뷰들에서 공통으로 쓰이는 객체라면 뷰들을 감싸고 있는 상위뷰에 존재하는게 객체가 초기화가 다시 되지 않고 같은 값을 쓸 수 있는 것이죠
그리고 StateObjectNavigation에 @StateObject가 존재한다면 StateObjectNextPage로는 값을 어떻게 전달해줄까?
라는 질문에 대한 답변들이 아직 건들이지 않은 두가지의 프로퍼티 래퍼인 @ObservedObject와 @EnvironmentObject 입니다.
거의 다 왔다! 지겨워도 조금만 더 참자!
이제는 다 이해해 보자!
@ObservedObject를 사용하여 상위 뷰에서 데이터를 전달해준 코드입니다.
import SwiftUI
class MyClass: ObservableObject {
@Published var num: Int = 0
}
struct StateObjectNavigation: View {
@StateObject var myClass = MyClass()
var body: some View {
NavigationView {
NavigationLink(destination: StateObjectNextPage(myClass: myClass)){
Text("Go next page")
}
}
}
}
struct StateObjectNextPage: View {
@ObservedObject var myClass: MyClass
var body: some View {
VStack {
Text(String(myClass.num))
Button(action: {
myClass.num += 1
}, label: {
Text("add StateObject num")
})
}
}
}
struct StateObjectNavigation_Previews: PreviewProvider {
static var previews: some View {
StateObjectNavigation()
}
}
부모뷰에서 @StateObject로 만들어진 myclass를 하위 뷰의 파라미터로 전달해주고, 그 값을 받는 자식뷰에서는 @ObservedObject를 이용해서 그 데이터를 받아줍니다.
끝!
마치 @State값을 @Binding해오는 것과 동일한 과정이죠?
@EnvironmentObject를 사용한 데이터 가져오기도 봐보자, 무엇이 다를까?
import SwiftUI
class MyClass: ObservableObject {
@Published var num: Int = 0
}
struct StateObjectNavigation: View {
@StateObject var myClass = MyClass()
var body: some View {
NavigationView {
NavigationLink(destination: StateObjectNextPage()) {
Text("Go next page")
}
}
}
}
struct StateObjectNextPage: View{
@EnvironmentObject var myClass: MyClass
var body: some View {
VStack {
Text(String(myClass.num))
Button(action: {
myClass.num += 1
}, label: {
Text("add StateObject num")
})
}
}
}
struct StateObjectNavigation_Previews: PreviewProvider {
static var previews: some View {
StateObjectNavigation()
.environmentObject(MyClass())
}
}
다른 점으로는 자식뷰로 갈때 @ObervedObject는 하위뷰의 파라미터로 객체를 전달해준것에 반해 @EnvironmentObject는 그런 과정이 없습니다.
직접 전달해줄 필요가 없는 것입니다. @EnvironmentObject로 프로퍼티래퍼를 정해준다면, 직접 그 객체를 Environment에서 찾기 때문입니다.
Preview struct 에 .environmentObject(MyClass()) 가 들어간 것은 @EnvironmentObject를 사용했을때의 Preview 화면을 작동하게 만들어줍니다ㅎ
여기서 @ObservedObject와 @EnvironmentObject의 차이를 유추 할 수 있는데,
@ObservedObject의 경우, 자식뷰가 여러개고 자식의 자식에서 데이터가 쓰인다면 2번 전달해주는 과정이 있을 것입니다. 하지만, 전역적으로 myClass 같은 변수를 관리해줄 필요가 없겠죠.
반면에, @EnvironmentObject의 경우는 뷰의 관계를 쭉 따라 데이터를 보낼 그럴 필요가 없을 것이라는 겁니다. @ObservedObject에 비해 많은 복잡한 하위 뷰들에서 공통적으로 사용될 때 이점이 있을 것입니다. 자식의 자식이더라도 데이터를 계속 이어올 필요 없이, @EnvironmentObject로 Environment에 있는 객체를 끌어다 뷰에서 사용하면 되는 것이지요.
이 얼마나 간편한가...
WWDC에서 위에 대해 설명하는 이미지를 첨부하자면,
@EnvironmentObject는 선언해주고 가져오면 된다는 것입니다.
위와 같은 그림에서는 @ObservedObject로 객체를 가져오는데 복잡함이 있겠죠?!
그.리.고. 마지막으로는 위의 그림에서와 같이 처음 @StateObject가 선언된 화면에는 .environmentObject(MyClass())와 같은 설정 값 주입이 있어야 한다. 그래야 나중에 찾을 수 있을테니..!!
+알파!!
@StateObject의 경우 뷰 스택에서 상단에 있는게 선호되는 이유가 있다면, @StateObject가 선언된 뷰의 하위 뷰에서는 제대로 동작하지만, 선언된 해당 뷰의 상위 뷰로 가게되면, @StateObject로 만들어진 객체가 초기화가 된다. 이 경우에 굉장히 중요한 비즈니스 로직을 담당하고 있는 객체가 초기화가 되면 안되는건 당연하기에 하나의 기능을 담당하는 class단위로 @StateObject를 만드는 것을 추천합니다!
따라서, @StateObject가 쓰이는 적절한 위치로는, 해당 객체가 초기화가 되어도 문제가 없는 선에서의 최대 계층에 있다면, 다른 말로 해보면, 해당 프로퍼티가 쓰이는 시퀀스의 최상단 뷰에 있는 것이 적절하다.
끝!
'iOS > SwiftUI' 카테고리의 다른 글
NavigationView, NavigationLink(Pop to Root) 원하는 페이지로 이동하기 (0) | 2022.05.05 |
---|