ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [Swift] SwiftUI 프로퍼티 래퍼 뿌시기 3: @ObservedObject와 @StateObject 차이 이해하기
    테크 2024. 3. 7. 17:18

     

    이전 시간에 Combine의 Publisher와 Subscriber에 대해서 초간단 설명을 드렸는데요.

    이것만 딱 기억하시면 됩니다!

    Publisher가 내보낸 값/이벤트 Subscriber가 볼 수 있고
    Subscriber Publisher가 내보낸 값/이벤트를 볼 수 있다

     

    저번 시간에 ObservableObject에서는 이벤트를 방출할 수 있다고 했죠?

    그렇다면 이 이벤트를 받아볼 수 있는 구독자 즉 Subscriber가 있어야 하는데

    내가 바로 Subscriber다! 라고 선언해주는 것이 @ObservedObject라고 했습니다.

     

    @ObservedObject

    @propertyWrapper @frozen
    struct ObservedObject<ObjectType> where ObjectType : ObservableObject
    • ObservableObject를 따르는 클래스의 인스턴스를 생성할 때 사용하는 프로퍼티 래퍼
    • @Observable로 감싸진 클래스를 @ObservedObject로 다시 감싸면 안됨 → 컴파일 에러 발생
    • @Observable 객체를 바인딩 할 때는 @Bindable을 사용하는 것이 좋음

    빨간색 글자는 지금은 몰라도 됩니다.

    제가 다음에 @Observable 공부할 때 다시 말씀드리도록 할게요!

     

    어쨌든 이미 @ObservedObject를 사용하는 방법을 대충 배웠는데요. [참고]

    이번엔 좀 더 자세하게 알아보기 위해서 몇 가지 코드를 추가했습니다.

     

    PlayerView에서는 random number를 생성하고 있습니다.

    하위 View로는 CounterView가 있네요?

    CounterView는 ObservableObject를 구독하고 있는 counter가 있습니다.

    import SwiftUI
    
    class Counter: ObservableObject {
        @Published var count: Int = 0
    
        func increaseCounter() {
            count = count + 1
        }
    }
    
    struct CounterView: View {
        @ObservedObject private var counter = Counter() // Here!
        
        var body: some View {
            VStack {
                Button("Increase Counter") {
                    counter.increaseCounter()
                }
                Text("\(counter.count)")
            }
        }
    }
    
    struct PlayerView: View {
        @State private var randomNumber = 0
    
        var body: some View {
            VStack {
                Button("Generate Random Number") {
                    randomNumber = (0..<1000).randomElement()!
                }
                Text("\(randomNumber)")
                CounterView()
            }
        }
    }

     

    굉장히 단순한 코드같지만, 예상과는 다른 동작을 합니다...! (뭐가 문제야!!!)

    코드에서 Increase Counter 버튼을 몇 번 클릭했다가

    Generate Random Number 버튼을 클릭하게 되면

    Counter의 count 값이 0으로 다시 초기화가 되는 기이한 일이 벌어집니다...!

     

    PlayerView의 @State 변수가 바뀔 때 마다 하위 View들이 초기화되기 때문인데요.

     

    이런 이유 때문에 Apple에서는 ObservedObject를 사용하는 것을 달가워하지 않습니다.

    그렇다면 ObservedObject 말고 어떤 걸 사용하면 될까요?

    정답은 @StateObject입니다!

     

    @StateObject

    @frozen @propertyWrapper
    struct StateObject<ObjectType> where ObjectType : ObservableObject
    • ObservableObject를 따르는 클래스의 인스턴스를 생성할 때 사용하는 프로퍼티 래퍼
    • 인스턴스는 single source of truth(SSOT)임 잘 몰라도 돼요!
    • App, Scene, View에서 사용하는 것을 권고
    • Private으로 사용하는 것을 권고
    • View의 input이 바뀐다고 하더라도 새로운 인스턴스가 생성되지 않음
    • 구조체, 문자열, 정수형 등을 저장할 땐 @State를 쓰는 것이 좋음
    • @Observable 프로퍼티 래퍼로 클래스를 생성할 때도 @State를 쓰는 것이 좋음 [참고]

    앞서 Single Source of Truth(SSOT)라는 용어를 썼는데요.

    쉽게 말하자면 ObservableObject가 갖고 있는 값들은

    다른 곳에 중복적으로 저장되어 있는 것이 아니라 해당 ObservableObject에만 있다고 생각하심 됩니다.

     

    @StateObject를 사용할 때 새로운 인스턴스를 생성하는데요.

    이 인스턴스는 View Identity가 바뀌지 않는 한 새로 생성되지 않습니다.

    이 부분 때문에 @ObservedObject와 차이가 발생하는건데요.

     

    바로 예제 코드 가시죠!

    위의 코드에서 @ObservedObject를 @StateObject로만 바꿨습니다.

    struct CounterView: View {
        @StateObject private var counter = Counter() // Here!
        
        var body: some View {
            VStack {
                Button("Increase Counter") {
                    counter.increaseCounter()
                }
                Text("\(counter.count)")
            }
        }
    }

     

    @ObservedObject를 @StateObject로 바꿔주게 되면

    PlayerView에서 randomNumber 값이 바뀌어 하위 View가 initialization이 된다고 하더라도

    @StateObject로 선언한 ObservableObject 인스턴스는 초기화가 되지 않습니다.

     

    그럼 아까 전에 View의 Identity가 바뀌면 @StateObject가 초기화될 수 있다는 것을 간접적으로 말씀드렸는데요.

    아래처럼 하위 View인 CounterView의 id를 random 값으로 설정하게 된다면

    @State로 선언한 randomNumber 값이 바뀔 때마다 View의 id가 바뀌기 때문에

    @ObservedObject처럼 매번 초기화시킬 수 있습니다.

    struct PlayerView: View {
        @State private var randomNumber = 0
    
        var body: some View {
            VStack {
                Button("Generate Random Number") {
                    randomNumber = (0..<1000).randomElement()!
                }
                Text("\(randomNumber)")
                CounterView().id(randomNumber) // Here!
            }
        }
    }

     

    @ObservedObject는 쓸모없나?

    이렇게만 보면 @ObservedObject는 쓸모없는 것 처럼 느껴집니다...

    하지만 @ObservedObject가 쓸 곳이 있기 때문에 여전히 deprecated 되지 않았겠져?

     

    @ObservedObject는 아래처럼 @StateObject 값을 다른 View에서 사용할 때 사용할 수 있습니다!

    MySubView에서 model의 isEnabled 값(=published property)이 바뀌게 되면

    해당 property를 구독하고 있는 모든 Subscriber들은 이 변경사항을 실시간으로 전달받을 수 있습니다!

    class DataModel: ObservableObject {
        @Published var name = "Some Name"
        @Published var isEnabled = false
    }
    
    struct MyView: View {
        @StateObject private var model = DataModel()
    
        var body: some View {
            Text(model.name)
            MySubView(model: model)
        }
    }
    
    struct MySubView: View {
        @ObservedObject var model: DataModel // Here!
    
        var body: some View {
            Toggle("Enabled", isOn: $model.isEnabled) // Here!
        }
    }

     

    근데 Apple 문서를 보면 @ObservedObect를 대체할 수 있는 것이 @EnvironmentObject라고 하는데요.

    이제 이걸 또 살펴봐야 하겠네요...

    빨리 최근에 나온 @Observable 마스터하고 싶다ㅠ...!

     

    Property Wrapper에 대해서 함께 공부하고 싶다면
    #PropertyWrapper 태그로 들어가기!
Designed by Tistory.