일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | 4 | 5 | 6 | 7 |
8 | 9 | 10 | 11 | 12 | 13 | 14 |
15 | 16 | 17 | 18 | 19 | 20 | 21 |
22 | 23 | 24 | 25 | 26 | 27 | 28 |
29 | 30 | 31 |
- 앱 비교 프로젝트
- 애플 디벨로퍼 아카데미 후기
- OS
- Swift 디자인패턴
- 데이터베이스
- 애플 아카데미 후기
- 운영체제
- SWIFT
- ObservedObject
- StateObject
- useReducer
- global soop
- 치지직
- 애플 디벨로퍼 아카데미
- 소프트웨어분석및설계
- apple developer academy 후기
- swift문법
- 애플 디벨로퍼 아카데미 21주차 회고
- ObservableObject
- sqoop
- 제앱소
- react
- 네이버 부스트캠프
- Swift 문법
- iOS 개발 오류
- 데이터베이스 공부
- 숭실대
- Swift 기능
- 네이버 치지직
- Apple Developer Academy @ POSTECH
- Today
- Total
사과하는 제라스
[Swift 지식] @ObservedObject vs @StateObject 이 둘 언제 쓰는데? (feat.Observation) - (24.06.07 업데이트) 본문
[Swift 지식] @ObservedObject vs @StateObject 이 둘 언제 쓰는데? (feat.Observation) - (24.06.07 업데이트)
Xerath(제라스) 2024. 5. 5. 18:58목차
서론
안녕하세요~! 개발자 제라스입니다! 👋🏻🤖👋🏻
오늘은 아주 기본적인 개념을 들고 왔습니다ㅎㅎㅎ
SwiftUI 학습 초반에 많이들 배우겠지만 사실 요즘 워낙 iOS17의 입김이 센 요즘이기에 저는 ObservableObject가 아닌 Observable 매크로를 통해 Observation을 구현했었는데요!
제가 최근에 ObservableObject인 ObservedObject와 StateObject의 차이에 대해 질문을 받았습니다...!
근데...근데!!! 제대로 이걸 설명하기가 어렵더라구요 ㅠㅠㅠ
워낙 유명한 아티클인 다음 Counter 예제 글을 통해 배운 둘의 차이는...
https://www.avanderlee.com/swiftui/stateobject-observedobject-differences/
'단순히 @state 효과가 전부인가...?'
'근데 이걸 또 어떻게 설명하지???'
참 의문이 들었습니다...
그래서 이 둘의 차이점을 알면서 가장 중요하게 생각하는 이 프로퍼티 래퍼들을 사용하는 상황을 비교해보고자 합니다.
그럼 오늘도 시작해보겠습니다!
24.06.07 업데이트)
Apple Developer Academy 내에서 발표를 맡아서 진행한 이후 추가적인 내용을 업데이트했습니다.
내용을 이해하는 데에 영상이 조금 더 나을 수 있기에 영상을 참고해보셔도 좋을 것 같습니다...!!
GitHub 테스트 코드: https://github.com/yoondj98/SwiftStudy/tree/main/ObservableObjectTest
https://youtu.be/bUul1MtIcE8?si=QOkLcTNw3o6cYUeA
Observation은 왜 필요할까?
일단 우리는 Data Model과 이걸 이용하는 View 사이를 분리합니다!
예를 들면,
final class Counter: ObservableObject {
@Published var count: Int = 0
func addCount() {
self.count += 1
}
}
이런 Counter 모델과
struct ChildView: View {
@StateObject var SOCounter = Counter()
var body: some View {
VStack {
Text("\(SOCounter.count)")
Button("SOCounter 증가") {
SOCounter.addCount()
printAddress(of: SOCounter, name: "SOCounter")
}
}
}
}
이 Counter 모델을 들고있는 ChildView를 나누는 것처럼요!
이 둘을 나눠놔야 모듈성(Modularity, 모듈로 나누는 것), 테스트 가능성(Testability, Model or View를 다른 Model or View로 대체 가능한 것)을 높일 수가 있거든요~~
아무튼 그렇다!
그렇게 분리된 이 'Model'. 얘를 여러 View에서 가져다가 쓸 수도 있겠죠?
다시 정확하게 말하자면 동일한 데이터를 여러 View에서 가져다가 보여주는데 어디서 변경이 이뤄지면 다른 곳들도 동기화 시켜줘야 할 때가 있잖아요?
그럴 때! 를 위해서 변화를 인지하고 다시 다른 View들에도 뿌려주기 위해서 우리는 Observation이란 걸 사용합니다!
근데 이 Observation 방식이 iOS 17에서 migrating되면서 Observable 매크로를 통해 구현하는 방식으로 바뀌었습니다...!!
그래서 예전에 쓰던 ObservableObject 프로토콜을 채택하는 모델을 만들고 이걸 @ObservedObject, @StateObject를 붙여서 사용하는 방식은 이제 옛날 구현 방식이 되었어요 ㅎㅎㅎㅎㅎ
(그만 좀 바껴...!!😭😭)
하지만! 이 방식도 잘 알아둬야 합니다!
아직까진 iOS16까지 제공하는 기업들이 많기도 하고,
만약 추후 iOS17 이상을 타깃하는 서비스를 만든다고 하더라도 결국 리팩토링하는 과정이 요해질 수 있기에 알아두는게 중요하죠ㅎㅎ
그리고 생각해보면 우리가 이 iOS17 이전의 Observation 방식을 깊게 파야하는 이유는
어느 View를 들어가든 대부분의 상단에 늘 위치하잖아요?!?!
그만큼 자주 쓰는 건데 어떻게 동작하는지 모르고 넘어가는 건 나중에 문제가 될 수 있다고 생각합니다.
아휴~~~ 말이 많네...! 🙏🏻🙏🏻
무튼무튼, 그럼 iOS 17 이전의 Observation 사용방식을 알아보겠습니다!
ObservableObject
그럼 먼저 ObservableObject부터 볼게요!
이 친구는 프로토콜인데 얘를 채택하는 class는 어느 곳에서든 변화가 생기면 @Published로 선언해둔 프로퍼티에 한해서는 변화를 감지하고 새로 해당 class 객체의 값을 변경해줍니다.
https://developer.apple.com/documentation/combine/observableobject
A type of object with a publisher that emits before the object has changed.
우왕...영어다...!🤮🤮
간단하게 꾸역해석해보자면...!
객체의 변화가 있기 전에 값을 방출해주는 Publisher를 갖고 있는 객체의 타입입니다.
여기서 말하는 publisher는 사실 objectWillChange입니다 ㅎㅎ
즉, ObservableObject는 objectWillChange 라는 Publisher를 갖고 있어요!
엥? objectWillChange는 뭔디요..??
이 친구는 SwiftUI에 객체 내의 값의 변화를 알림으로써 SwiftUI가 View를 업데이트하도록 만들어줍니다.
즉, SwiftUI 프레임워크가 이 objectWillChange라는 Publisher를 구독하고 있기에 객체의 상태 변화를 감지할 수 있는 거죠.
감지가 되면 바로 해당 객체를 가진 뷰들을 업데이트함으로써 변경된 상태를 반영합니다!
근데 이런 objectWillChange 퍼블리셔가 객체 내의 값이 변화했다는 걸 전달하는 방법이 바로 다음 코드입니다.
objectWillChange.send()
그래서 우리는 ObservableObject 프로토콜을 준수하는 class 내의 속성들이 변경될 때 해당 동작을 구현해줘야 변화를 감지시킬 수 있습니다.
다음은 예시 코드입니다.
import Foundation
class Coffee: ObservableObject {
var sugar: Int
init(sugar: Int) {
self.sugar = sugar
}
func addSugar(amount: Int) {
sugar += amount
}
}
위처럼 Coffee라는 ObservableObject를 준수하는 class를 만들었어요
이때 sugar의 값을 변경하는 함수 addSugar를 만들었는데 이걸 View에서 사용하면 View는 새로운 값으로 변경되지 않습니다.
왜냐하면 객체의 값이 변경되었다는 걸 아직 SwiftUI에서 모르기에 View를 새로 업데이트하지 않는 거죠.
그래서 사용하는게 objectwillChange...!!
다음과 같이 써주면 됩니다.
import Foundation
class Coffee: ObservableObject {
var sugar: Int
init(sugar: Int) {
self.sugar = sugar
}
func addSugar(amount: Int) {
sugar += amount
self.objectWillChange.send() // <- 요기~~
}
}
일단 위에서 언급한 것처럼 ObservableObject는 objectWillChange라는 Publisher를 갖고 있거든요?
그래서 바~~~아로 objectwillChange를 갖다가 send 함수를 쓸 수 있습니다.
(즉, 위 코드처럼 objectWillChange.send()를 쓸 수 있고 이 함수가 변화를 감지시키는 Trigger 역할을 하는 거죠!)
이렇게 하면 sugar에 amount만큼 값을 증가시키고, 그 변화된 사실을 send함수를 통해 SwiftUI에 알려줍니다.
그럼 View에서도 그 변화를 감지해서 업데이트를 하고 제대로 된 값이 나오는 거죠.
위와 같은 방식도 되지만 함수가 여러가지가 생기게 되면 일일이 적어주는게 귀찮으니까
sugar의 willSet이나 didSet에 objectWillChange.send()를 적어주면 간결하되 동일한 동작을 하게 구현할 수 있습니다.
다음과 같이요!
class Coffee: ObservableObject {
var sugar: Int {
willSet {
self.objectWillChange.send()
}
}
init(sugar: Int) {
self.sugar = sugar
}
func addSugar(amount: Int) {
sugar += amount
}
}
@Published의 쓰임
근데 이걸 간단하게 @Published라는 프로퍼티 래퍼를 써주면 편해집니다.
이 래퍼로 감싸면 해당 값이 변경될 때마다 objectwillChange 퍼블리셔가 자동으로 발행되기 때문에 값의 변화를 바로 알려줍니다.
즉, 따로 objectWillChange.send() 이런 걸 작성 안해줘도 알아서 값이 바뀌면 변화를 전달하는 거죠.
다음과 같은 코드로 간결해집니다!
import Foundation
class Coffee: ObservableObject {
@Published var sugar: Int
init(sugar: Int) {
self.sugar = sugar
}
func addSugar(amount: Int) {
sugar += amount
// self.objectWillChange.send() -> 안 써줘도 됨
}
}
근데 쓰다보니 변화를 감지해서 다시 렌더링해주는 이 기능이 되게 비슷한게 있습니다.
@State- @Binding이랑 상당히 비슷하죠??
아니 그럼 @State-@Binding으로 구현하면 안돼?
결론부터 말하자면, 넹넹...안됩니다ㅎㅎㅎ
이건 애플 공식문서에도 나와있습니다.
https://developer.apple.com/documentation/swiftui/stateobject
StateObject의 공식문서에서 중간에 Note로 명시되어있는데,
우리가 만약 struct, String, Int와 같은 값타입을 저장하면서(Source Of Truth로 가짐) 쓰려고 하면 @State로 관리를 합니다.
만약 값타입이 아니라 참조 타입을 SOT로 가지려면 Observation을 써야 합니다.
위 Note에는 Observable이란 말이 나오는데 이건 iOS 17에서 구현하는 Observation의 방법 중 하나로 매크로를 활용하는 방식입니다.
이 친구는 따로 ObservableObject같은 걸 안 쓰고 @State를 써주면 됩니다.
하지만 그 이전 버젼에서는 ObservableObject를 써야합니다.
그럼 '왜 그럴까?'에 대해 살펴보면 @State를 알면 됩니다.
https://developer.apple.com/documentation/swiftui/state
Overview를 보면 @State는 값 타입에 대해서 SOT로 해당 View 계층에 저장한다고 합니다.
여기서 알 수가 있습니다!
아하! @State는 값타입의 변화를 인지하는구나
근데 만약 우리가 참조 타입을 @State로 해두면,,, 참조타입의 주소값이 변하지 않는 이상 @State는 값의 변화를 인지하지 못하죠!
그래서 이 친구의 변화를 인지할 방안으로 ObservableObject를 쓰는 겁니다.
물론, iOS17부터는 @Observable 매크로가 나오고 Observation 방식이 새로 나와서 @State가 해당 Observable를 채택하는 클래스에서 쓰입니다!
사용방식은 다음 공식문서를 참고해보시면 좋을 것 같아요!
그럼 ObservedObject와 StateObject의 차이는 뭔데?
하~~~~이제야 진짜 얘기를 하게 되네요 ㅋㅋㅋㅋㅋ
ObservableObject에는 ObservedObject와 StateObject 두가지 종류가 있죠.
근데 말씀 중에 죄송합니다!!
저는 이 둘을 대등한 관점에서 차이점을 보는 것이 의미가 없다고 생각합니다.
그래서 이 둘의 사용 상황의 차이점 정도로 정리하려고 합니다.
한번 그 과정을 천천히 살펴볼게요.
일단 제가 생각하는 가장 중요한 개념은 @StateObject입니다.
이 친구는 @ObeservedObject와는 달리 구현되어있는 VIew의 계층에 Source Of Truth가 생성됩니다.
그러면 @StateObject는 자신이 포함된 View가 아예 사라지고 나중에 생성되는 상황을 제외하고는 값을 유지합니다.
이 말이 생각보다 어려운데...
간단히 정리해보면 해당 View가 업데이트되거나 변경이 없는 상황에서는 값을 유지한다는 겁니다.
만약 어떤 ParentView 내에 ChildView가 서브View로 있다고 합시다!
이 ChildView 내에는 @StateObject가 있습니다.
이때 StateObject 내의 어떤 값이 변경되면 자신을 포함한 View(ChildView)의 상단 View(ParentView)에서 리렌더링이 발생합니다.
이때, 당연히 ParentView 안에 있는 ChildView는 사라지고 바로 재생성이 됩니다...!!
이건 init을 걸어보면 알 수 있어요!
다음은 제가 만든 테스트 예제인데 ChildView에 있는 객체가 ObservedObject여도, StateObject여도 항상 ChildView가 init이 실행됩니다.
하지만...! 여기서 눈여겨 볼 것은 Counter가 주소를 유지하고 값도 유지한다는 거에요!
이게 가능한 이유는...
StateObject로 된 객체는 참조가 Source of truth로 등록되어있고 이걸 SwiftUI에서 갖고있다가 ChildView가 재생성되면 바로 다시 입혀주기 때문이에요.
그래서 객체의 주소값도 동일하고 값도 유지가 되고 있는 거죠!
그러면 언제 우리는 이걸 쓰느냐?
1. View에서 해당 객체를 소유하고 View가 생성될 때 해당 객체도 생성되고, 사라질 때 같이 사라져도 될 때
-> 즉, 이 View가 시작하는 부분이라고 생각되어질 때
2. View가 활성화되어 있는 상태에서 계속 데이터를 유지할 수 있게 하고 싶을 때
-> 상위 View의 변화로 인해서 현재 View가 변화되지 않고 싶을 때
조금 표현이 어렵다면 이렇게 이해해보면 좋을 것 같습니다.
어떤 View가 가장 위에 있어서 처음 데이터를 만들어서 사용하기 시작할 때 사용하면 좋구,
View가 자기 자신의 위에서 관리하지 않는 데이터를 처음 만들어서 관리할 때 쓰기 좋습니다.
즉, 간단하게는 내가 처음으로 이 데이터를 만들어서 쓰고 유지하려고 한다? 하면 그때 @StateObject로 만들어주면 됩니다.
근데 만약 이게 유지될 필요가 없다면 굳이 @StateObject가 아닌 @ObservedObject를 쓰면 됩니다.
@ObservedObject는 해당 View가 다시 그려지면 상태값이 초기화가 됩니다.
그럼 @ObservedObject는 어떨 때 쓰는게 좋은가?? 라고 한다면...
1. 잠시 사용할 View 내부에서 데이터 값이 변동이 일어날 때
2. 상단에 있는 @StateObject를 참조하는 방식으로 사용할 때 (@State-@Binding의 @Binding의 역할과 비슷)
3. 외부에서 주입해주는 값을 사용하고자 할 때
4. 반복되는 항목 내에서 사용되는 경우
이 정도가 될 것 같습니다.
1번은 굳이 다른 View에 데이터를 전송하거나 다른 View에서 해당 View의 상태를 유지할 필요가 없을 때, 근데, 해당 View 내에서 변화를 감지해서 리렌더링은 해줘야 할 때입니다.
2번은 상단에서 이미 @StateObject로 SOT를 생성하고 그걸 참조하도록 만들 때인데 마치 @State-@Binding과 같은 상황이라고 보시면 됩니다!(상위 View -> 자식 View 로 전달)
3번은 우리가 @StateObject는 처음부터 객체를 선언하면서 만들어줘야 하는데 @ObservedObject는 주입받아서 사용하는지라 이렇게 써야 하는 경우입니다. 근데, 이건 자연스레 2번에서의 상황과 같게 느껴질 것 같아요 ㅎㅎ
4번은 사실 StateObject로 해도 큰 문제는 발생하지 않는 거 같아요. 다만, 동일한 View가 반복될 때 각각이 StateObject를 들고 있는 상태는 성능 면에서 문제가 발생할 수 있어요. 그렇기에 StateObject를 남발하는 코드는 지양해야 합니다..!!
마무리
이렇게 오늘은 ObservableObject의 동작방식과 @ObservedObject와 @StateObject의 특성에 따른 사용 타이밍을 살펴보았습니다.
둘의 차이점을 개념적으로 이해하는 것도 중요했지만 이렇게 사용하는 상황이 꽤 중요하다고 느껴졌습니다.
사실 @StateObject와 @ObservedObject의 차이점은 이해가 가지만 실제로 구현하면서 종종 메모리 낭비 등의 문제가 이 부분에서 발생하는 거 같아요...
그렇기에... 대충 어느 타이밍에 '쓰면 된다' 혹은 '써야 한다'를 알아만 둔다면 문제 분석이나 메모리 개선에 도움이 되지 않을까 싶네요 ㅎㅎ
생각보다 ObservableObject에 대한 두 프로퍼티 래퍼의 비교보다는 상황 설명을 많이 했는데,
이 둘의 차이점을 다룬 포스팅들은 워낙 많으니 한번 찾아보시는 것도 좋을 것 같습니다 :-D
그럼 오늘도 긴 포스팅 읽어주셔서 감사드리고 다음에는 더 좋은 글로 돌아오겠습니다!
참고
https://developer.apple.com/documentation/swiftui/state
https://developer.apple.com/documentation/combine/observableobject
https://developer.apple.com/documentation/combine/observableobject/objectwillchange-2oa5v
https://developer.apple.com/documentation/swiftui/stateobject
https://developer.apple.com/documentation/swiftui/observedobject/
https://velog.io/@minsang/SwiftUI-StateObject-ObservedObject-EnvironmentObject#observedobject
https://ios-development.tistory.com/1160
https://green1229.tistory.com/228
https://parkjju.github.io/vue-TIL/swiftuiOpen/231226-1.html#observedobject
아직 꼬꼬마 개발자입니다.
더 나은 설명이나 코드가 있다면 언제든 환영입니다.
적극적인 조언과 피드백 부탁드립니다!
그럼 오늘도 개발 가득한 하루되세요!
- Xerath -
🤖🤖🤖🤖🤖🤖🤖
'제라스의 iOS 공부 > Swift 지식' 카테고리의 다른 글
[Swift 지식] 너 동시성 프로그래밍 제대로 알아? - 개념 비교하기 (0) | 2024.06.29 |
---|---|
[Swift 지식] UITableView Cell의 생명주기(feat.prefetch를 써보자!) (0) | 2024.05.10 |
[Swift 지식] @Bindable 딥다이브(였던 것) feat. 사실 Observable 딥다이브... (3) | 2024.04.07 |
[Swift 지식] Swift에서 프로토콜이 클래스를 상속한다고? (1) | 2024.03.31 |
[Swift 지식] removeAll(keepingCapacity: Bool)의 성능 비교 (1) | 2024.03.14 |