@StateObject vs. @ObservedObject: 차이점 설명

이지수·2023년 7월 23일
0

Swift학습

목록 보기
9/14
post-thumbnail

🚩출처

  • 본 글은 영문으로 작성된 @StateObject vs. @ObservedObject: The differences explained을 보고 정리한(거의 의역) 글입니다. 글 내용 뿐 아니라 코드와 예시 화면 모두 원본 글에서 가져온 것입니다.
  • 원문 너무 좋은 글인듯 합니다. 추천합니다..

들어가며

  • @StateObject와 @ObservedObject SwiftUI View에게 관찰하는 값의 변화를 알려주는 프로퍼티 래퍼이다. 이 둘은 유사해보이지만 중요한 다른점이 있다.
  • 우선, 왜 항상 @ObservedObject만 쓰면 안되는지 궁금할 것이다.

ObservedObject란 무엇인가?

  • @StateObject와 @ObservedObject 모두 ObservableObject 프로젝트를 채택한 객체가 필요하다.
  • 이 프로토콜은 객체가 변경되기 전에 방출하는 게시자가 있는 객체를 나타내며 SwiftUI가 뷰 다시 그리기를 트리거하도록 알려준다
    • This protocol stands for an object with a publisher that emits before the object has changed and allows you to tell SwiftUI to trigger a view redraw.
  • ObservableObject를 채택하는 타입은 SwiftUI뷰에 연결되고 SwiftUI로 하여금 변화를 감지한 후 다시 그리게 만들기 위하여 @ObservedObject 프로퍼티 래퍼와 결합될 수 있다.

예시 - 카운터
📌 예제 깔끔 Good! 🙌 🙌 🙌

final class CounterViewModel: ObservableObject {
    @Published var count = 0

    func incrementCounter() {
        count += 1
    }
}

struct CounterView: View {
    @ObservedObject var viewModel = CounterViewModel()

    var body: some View {
        VStack {
            Text("Count is: \(viewModel.count)")
            Button("Increment Counter") {
                viewModel.incrementCounter()
            }
        }
    }
}
  • ObservabbleObject인 뷰 모델 클래스를 선언
    • 내부에 @Published 카운터가 있음
  • 구독하는 구조체에서는 @ObservedObject로 뷰 모델을 구독
  • 뷰 모델 내부의 @Published 카운터는 버튼(Increment Counter 버튼)이 눌러질 때마다 증가한다. 그리고 모든 구독자(위에선 CounterView)에게 변경 신호를 준다.

예시 - 카운터
수동적 방법

final class CounterViewModel: ObservableObject {
    private(set) var count = 0

    func incrementCounter() {
        count += 1
        objectWillChange.send()
    }
}
  • 자동으로 변경 신호를 주는 @Published 안 쓰고 objectWillChange.send()같은 수동으로 신호를 보내는(신호를 줘라!할 때만 변경 신호를 보내는) 방법도 쓴다고 한다.
  • 만약 게시된 여러 속성(published property)을 한번에 없데이트하여 일반 매개변수로 나타내고 수동 신호를 사용하는 경우 이 방법을 쓰는 게 더 나을 수도 있다
    • ?????
    • However, it might be a better solution if you’re updating multiple published properties at once to mark those properties as regular arguments and use the manual signal instead.

@StateObject란 무엇인가?

  • @StateObject는 @ObservedObject와 비슷하게 작용한다.
  • 우리는 앞의 @ObservedObject를 쓴 카운터 예제를 @StateObject를 써서 바꿀 수 있다.

예시 - 카운터
📌 @StateObject 사용 버전

struct CounterView: View {
    /// ✅Using @StateObject instead of @ObservedObject
    @StateObject var viewModel = CounterViewModel()

    var body: some View {
        VStack {
            Text("Count is: \(viewModel.count)")
            Button("Increment Counter") {
                viewModel.incrementCounter()
            }
        }
    }
}
  • @ObservedObject 대신에 @StateObject를 쓴 것 말고는 딱히 바뀐게 없다.
  • 그러나 @StateObject를 언제 @ObservedObject 대신에 쓸지 명확히 하는 데에는 상당한 차이가 있다.
  • @StateObject 프로퍼티 래퍼로 표시한 구독 객체 파괴되지 않으며 그것들이 뷰 스트럭트가 다시 그려질 때에 재-인스턴스화(re-instantiated)되지 않는다.
  • 또 다른 뷰가 하위 뷰를 담고 있을 때 이 차이를 이해하는 것은 필수다.
  • 이 것이 어떻게 동작하는지 설명하기 위해서 앞선 카운터 예제의 뷰 를 또 다른 뷰로 감싼다.

카운터 예제 변형 - 하위 뷰 포함

struct RandomNumberView: View {
    @State var randomNumber = 0

    var body: some View {
        VStack {
            Text("Random number is: \(randomNumber)")
            Button("Randomize number") {
                randomNumber = (0..<1000).randomElement()!
            }
        }.padding(.bottom)
        
        //✅ 중요! 
        //가장 처음에 구현했던 @ObservedObject로 
        //뷰 모델을 구독하는 CounterView()를 
        //RandomNumberView의 하위뷰로 넣었다. 이러면 어떤 일이 벌어질까?
        CounterView()
    }
}
  • randomNumberView는 randomize button을 누르면 난수가 생성되도록 한다.
    @State 프로퍼티 래퍼로 표시되어 있는 randomNumber 프로퍼티는 CounterView뷰를 다시 그리도록 뷰를 다시 그릴 것이다.
    • RandomNumberView가 다시 그려지면 그 안에 포함된 CounterView도 다시 그려진다.
  • CounterView 내부에 @ObservedObject를 썼을 때는 난수가 생성될 때마다 카운터가 리셋되는 것을 볼 수 있다.
    • 즉, 하위 뷰에서 @ObservedObject를 써서 게시 객체 구독하는 경우에는 상위 뷰가 다시 그려질 때마다 하위 뷰의 구독 객체가 게시 객체의 초기 상태로 되돌아 간다.
    • randomize button을 눌렀을 뿐인데 @ObservedObject를 썼던 카운터까지 초기화된다.
    • 본 의도대로라면 카운터는 난수 생성과 별개로 실행되어야 하기때문에 randomize button을 누르더라도 카운터의 숫자는 보존되어야 한다. 그러나 @ObservedObject 프로퍼티 래퍼를 써서 뷰 모델을 구독했기 때문에 카운터가 리셋되어 0이 되는 것이다.
  • 이를 바로 잡기 위해선, @ObservedObject 대신에 @StateObject를 써야 한다. @StateObject는 뷰 모델 객체가 뷰가 다시 그려지는 동안에도 카운터 수가 보존되도록 한다.

실행 영상
@ObservedObject로 뷰 모델을 구독할 때. randomize 버튼을 누르면 카운터 수가 보존되지 않고 리셋된다.

https://www.avanderlee.com/wp-content/uploads/2022/02/observedobject_counter_demo.mp4


그래서, 언제 @StateObject를 써야 하는가?

  • 언제 @StateObject를 @ObservedObject 대신에 쓸지 아는 것이 좋다.
  • SwiftUI는 뷰를 언제나 (any time) 재 생성하기 때문에 @ObservedObject 프로퍼티를 뷰 안에 생성하는 것은 안정적이지 않다.
  • @ObservedObject를 의존성 때문에 쓰는 게 아니라면, @StateObject 프로퍼티 래퍼를 쓰는 것이 뷰가 새로 그려지는 상황에서도 일관적인 결과를 보장한다.

그렇다면 모든 뷰에 항상 같은 인스턴스를 사용하기 위해 @StateObject만 써야 하는가?

  • Siblings observing the same @StateObject instance down the line don’t require marking the object with this property wrapper.
    • 그 후에(아마 한번 @StateObject를 써서 구독한 이 후를 말하는 듯???) 같은 @StateObject 인스턴스를 관찰하는 것들은 @StateObject 프로퍼티 래퍼를 써서 표시할 필요가 없다.
  • It’s important not to do this since you’ll ask to retain and manage the object’s lifecycle in two places.
    • 왜냐하면 두 곳에서 객체의 생명주기를 관리하고 유지하기를 요청하게 되는 것이기 때문에 그렇게 하지 않는 것이 중요하다.
  • As described in the previous section: if you inject the observed object, you should use @ObservedObject.
    • 전 섹션에서 설명한대로, 관찰된 객체를 주입하는 경우 @ObservedObject를 써야 한다.

결론

  • @StateObject와 @ObservedObject는 비슷한 특성들을 가지고 있지만 SwiftUI가 그들의 생명주기를 관리하는 방식이 다르다.
  • 현재 뷰에 구독하는 객체를 생성할 때는 결과의 일관성을 확실히 하기 위해 StateObject 프로퍼티 래퍼를 써라.
  • 관찰하는 객체의 의존성을 주입할 때마다 @ObservedObject를 쓸 수 있다.

stand for
: 의미하다

emit
: 방출하다

tell
:(지시,충고,명령의) 말을 하다, 시키다

combine with
: ~와 결합되다

comform to(with)
: ~에 따르다

significant
:상당한

down the line
: 일이 발생하고 있는 도중에

  • 뒤에 특정 시점이 오면, 특정 시점 이후를 뜻함

retain
:유지하다

describe
:설명하다

profile
iOS 개발자 꿈나무

2개의 댓글

comment-user-thumbnail
2023년 7월 23일

정보 감사합니다.

1개의 답글