반응형
LinkedIn 개발자로 성장하면서 남긴 발자취들을 확인하실 수 있습니다.
Github WWDC Student Challenge 및 Cherish, Tiramisul 등 개발한 앱들의 코드를 확인하실 수 있습니다.
개인 앱 : Cherish 내 마음을 들여다보는 시간, 체리시는 디자이너와 PM과 함께 진행 중인 1인 개발 프로젝트입니다.
10년 후, 20년 후 나는 어떤 스토리 텔러가 되어 있을지 궁금하다. 내가 만약에 아직 조금 더 탐구하고 싶은 게 있고, 궁금한 게 있다면, 그게 설사 지금 당장의 내 인생에 도움이 안 되는 것 같더라도 경험해보자. 그 경험들을 온전히 즐기며 내 것으로 만들고, 내 일에 녹여내고... 그러다보면 그 점들이 모여 나란 사람을 그려내는 선이 될 테니까.

Recent Posts
Recent Comments
Total
관리 메뉴

꿈꾸는리버리

@StateObject vs @ObservedObject 본문

오뚝이 개발자/SwiftUI

@StateObject vs @ObservedObject

rriver2 2022. 4. 28. 15:46
반응형

오늘은 @StateObject와 @ObservedObject의 차이에 대해서 알아보려고 한다.

 

@StateObject와 @ObservedObject의 등장 배경 

기존의 @ObservedObject는 SwiftUI가 View를 다시 랜더링 할 때 의도하지 않은 초기화가 발생하는 문제가 존재했다.

그리고 이 문제를 해결하기 위해서 @StateObject가 나왔다 !

 


다음과 같은 view를 만들어보면서 !! 둘의 차이와 언제 무엇을 사용해야 하는지 알아보자.

 


 ObservableObject -> ViewModel 만들기  

우선 ObservableObject 프로토콜을 준수하여 ViewModel을 만든다.

이제 이 RiverViewModel을 바라보고 있는 객체는 @Published Attribute로 선언된 data의 값이 변할 때마다 값이 바뀌었다는 시그널을 받게 된다.

var ViewCount : Int = 0

class RiverViewModel: ObservableObject {
    @Published var data = 0
    init() {
        ViewCount += 1
        print("ViewModel init \(ViewCount)")
    }
    
    func addCount5(){
        self.data += 5
    }
    
    deinit {
        print("ViewModel deinit \(ViewCount)")
    }
}

 


 @State를 지닌 최상위뷰 : RiverView  

최상위 RiverView에서는 @State Attribute로 선언된 name이 있다.

@State 로 선언을 했기 때문에 name이 변경될 때마다 하위 뷰를 포함한 RiverView뷰는 redrawn된다.

( RiverView의 하위 뷰들에는 RiverSubView2와 RiverSubView3가 있다. )

struct RiverView: View {
    @State private var name = "리버리"
    
    var body: some View {
        VStack(spacing: 10) {
            Button {
                if(name == "리버리2"){
                    name = "리버리"
                }else if(name == "리버리"){
                    name = "리버리2"
                }
            } label: {
                Text("change name")
            }
            
            Text(name)
            RiverSubView(name: name)
            RiverSubView2(name: name)
        }
    }
}

 


 @ObservedObject 

우선 RiverSubView에서는 @ObservedObject을 사용했다.

struct RiverSubView: View {
    @ObservedObject var viewmodel = RiverViewModel()
    let name : String
    
    var body: some View {
        VStack{
            Rectangle()
                .frame(height: 10)
            Text("RiverSubView")
                .font(.title)
            Text("(ObservedObject)")
                .padding(10)
            
            Text(name)
            HStack{
                Text("\(viewmodel.data)")
                    .font(.largeTitle)
                    .padding()
                Image(systemName: "plus.circle")
                    .font(.system(size: 40))
                    .foregroundColor(.red)
                    .onTapGesture {
                        viewmodel.addCount5()
                    }
                Image(systemName: "minus.circle")
                    .font(.system(size: 40))
                    .foregroundColor(.red)
                    .onTapGesture {
                        viewmodel.addCount5()
                    }
            }
        }
    }
}

 

하지만 이렇게 코드를 짜면 ... 의도하지 않은 오류가 발생하게 된다.

 

바로 RiverSubView에서 RiverView의 name(@State)이 변경되면

data(@ObservedObject의 viewmodel 값)가 원래대로 초기화가 된다....는 것...

?? ㅋ...

 

 

왜 그럴까 ?

앞서 말했던 것처럼 RiverView의 name(@State)이 변경되었을 때마다

하위 뷰들을 포함한 RiverView가 초기화가 되면서 RiverSubView의 viewmodel 또한 초기화가 된다. 

따라서 viewmodel의 data 값도 0으로 초기화 된다.



change name button을 눌러서 name(@State)을 변경할 때마다
RiverSubView의 viewmodel이 소멸되고 새로 생성되면서 해당 ViewModel의 생성자와 소멸자가 호출되기 때문에

"ViewModel init, ViewModel deinit" 이 프린트로 출력이 된다.

 

이 문제를 어떻게 해결할까?

이제 swiftUI 고수님들은 고도화된 구조에서도 문제를 찾아 fix가 가능하지만

나같은,, 지렁이들은... 데이터 바인딩 문제가 얽히고 섥혀 있을 때 문제를 찾기 어렵다. 

 

그리고 나 같은 지렁이들을 도와주기 위해 stateObject가 나왔다!!! 🥳

 


 @stateObject  

 RiverSubView2에서는 앞서 말한 문제를 해결하기 위해서 @ObservedObject 대신 @stateObject을 사용한다.

struct RiverSubView2: View {
    @StateObject var viewmodel = RiverViewModel()
    let name : String
    
    var body: some View {
        VStack{
            Rectangle()
                .frame(height: 10)
            Text("RiverSubView2")
                .font(.title)
            Text("(StateObject)")
                .padding(10)
            
            Text(name)
            HStack{
                Text("\(viewmodel.data)")
                    .font(.largeTitle)
                    .padding()
                Image(systemName: "plus.circle")
                    .font(.system(size: 40))
                    .foregroundColor(.red)
                    .onTapGesture {
                        viewmodel.addCount5()
                    }
                Image(systemName: "minus.circle")
                    .font(.system(size: 40))
                    .foregroundColor(.red)
                    .onTapGesture {
                        viewmodel.addCount5()
                    }
            }
        }
    }
}

 

이제 RiverSubView2에서는 name이 변경되어도 data의 값이 유지되는 것을 확인할 수 있다.

 

stateObject가 뭐길래?

stateObject를 사용하면 SwiftUI가 viewmodel을 View와 별개의 메모리 공간에 저장해서 안전하게 보관한다.

이처럼 viewmodel이 객체화가 되어 따로 관리가되기 때문에 RiverView가 redrawn되더라도 viewmodel의 값은 초기화되지 않는다.


정리를 하자면, RiverView의 name이 변경되었을 때 하위 뷰들을 포함한 RiverView가 초기화되더라도
viewmodel는 별개로 관리가 되고 있기 때문에 data(@ObservedObject의 viewmodel 값)가 유지된다.
 



change name button을 누를 때마다
이 RiverSubView2의 viewmodel은 새로 생성되지 않기 때문에
"ViewModel init, ViewModel deinit" 이 뜨지 않는 것을 확인할 수 있다. 

 


 무조건 stateObject를 쓰는 게 좋을까요? 

나도 처음에는 그냥 @ObservedObject대신에 @StateObject를 쓰면 되겠구나 했는데... 아래 글을 확인해보면 

 정리 ( 마지막 단락 ) 

->  애플은 Observable Object를 처음 초기화 할 때는 StateObject를 사용해서 View와 별개의 메모리 공간에 데이터를 저장하도록 하고, 이 객체화된 데이터를 넘겨 받을 때에는 @ObservedObject이나 @EnvironmentObject을 이용해서 게층 전달을 하도록 추천한다.

( @EnvironmentObject 블로그 포스팅)

 

 

아마 계속해서 StateObject를 사용하면 viewmodel이 여러개 객체화가 되어 따로 관리되어 메모리 낭비가 되기도 하고

부모 뷰에서 따로 데이터를 관리하기 때문에 앞서 @ObservedObject를 사용했을 때의 문제가 발생하지 않기 때문에 

하위 뷰들에서 굳이 따로 @StateObject를 사용해서 객체화시킬 필요가 없는 것 같다.

 

 


@stateObject와 @ObservedObject 적절히 사용하기 

애플이 하라는 대로 코드를 짜보자

 

우선 RiverSubView2에서는 @StateObject로 viewmodel을 객체화시켰다.

그리고 이 객체화 시킨 viewmodel을 RiverSubView2의 하위뷰인 RiverSubView3로 전달했다.

struct RiverSubView2: View {
    @StateObject var viewmodel = RiverViewModel()
    let name : String
    
    var body: some View {
        VStack{
            Rectangle()
                .frame(height: 10)
            Text("RiverSubView2")
                .font(.title)
            Text("(StateObject)")
                .padding(10)
            
            Text(name)
            HStack{
                Text("\(viewmodel.data)")
                    .font(.largeTitle)
                    .padding()
                Image(systemName: "plus.circle")
                    .font(.system(size: 40))
                    .foregroundColor(.red)
                    .onTapGesture {
                        viewmodel.addCount5()
                    }
                Image(systemName: "minus.circle")
                    .font(.system(size: 40))
                    .foregroundColor(.red)
                    .onTapGesture {
                        viewmodel.addCount5()
                    }
            }
            RiverSubView3(viewmodel: viewmodel)
        }
    }
}

 

 

상위 뷰인 RiverSubView2에서 @StateObject로 객체화시킨 viewmodel는

하위뷰인 RiverSubView3에서 @ObservedObject로 받아서 사용한다.
이때 RiverSubView3에서는 @StateObject의 객체화 된 viewmodel의 변화를 감지하기 때문에

name 값이 변화하더라도 data 값이 초기화되지 않는다.

struct RiverSubView3: View {
    @ObservedObject var viewmodel : RiverViewModel
    
    var body: some View {
        VStack{
            Divider()
            Text("RiverSubView3")
                .font(.title2)
            Text("(RiverSubView에서 viewmodel을 ObservedObject로 받아옴)")
                .padding(10)
            
            HStack{
               Text("\(viewmodel.data)")
                    .font(.largeTitle)
                    .padding()
                Image(systemName: "plus.circle")
                    .font(.system(size: 40))
                    .foregroundColor(.red)
                    .onTapGesture {
                        viewmodel.addCount5()
                    }
                Image(systemName: "minus.circle")
                    .font(.system(size: 40))
                    .foregroundColor(.red)
                    .onTapGesture {
                        viewmodel.addCount5()
                    }
            }
        }
    }
}

 

 

 


 정리 

처음 viewmodel의 값의 인스턴스를 만들때는

@stateObject로 객체화를 시켜 따로 관리하도록 만들고,
이후 하위뷰로 전달할 때에는 @ObservedObject를 사용해서

부모 뷰의 @stateObject로 객체화 된 viewmodel을 감시한다.

 


 전체 코드 

import SwiftUI

var ViewCount : Int = 0

class RiverViewModel: ObservableObject {
    @Published var data = 0
    init() {
        ViewCount += 1
        print("ViewModel init \(ViewCount)")
    }
    
    func addCount5(){
        self.data += 5
    }
    
    deinit {
        print("ViewModel deinit \(ViewCount)")
    }
}

struct RiverView: View {
    @State private var name = "리버리"
    
    var body: some View {
        
        VStack(spacing: 10) {
            Button {
                if(name == "리버리2"){
                    name = "리버리"
                }else if(name == "리버리"){
                    name = "리버리2"
                }
            } label: {
                Text("change name")
            }
            
            Text(name)
            RiverSubView(name: name)
            RiverSubView2(name: name)
        }
    }
}

struct RiverSubView: View {
    @ObservedObject var viewmodel = RiverViewModel()
    let name : String
    
    var body: some View {
        VStack{
            Rectangle()
                .frame(height: 10)
            Text("RiverSubView")
                .font(.title)
            Text("(ObservedObject)")
                .padding(10)
            
            Text(name)
            HStack{
                Text("\(viewmodel.data)")
                    .font(.largeTitle)
                    .padding()
                Image(systemName: "plus.circle")
                    .font(.system(size: 40))
                    .foregroundColor(.red)
                    .onTapGesture {
                        viewmodel.addCount5()
                    }
                Image(systemName: "minus.circle")
                    .font(.system(size: 40))
                    .foregroundColor(.red)
                    .onTapGesture {
                        viewmodel.addCount5()
                    }
            }
        }
    }
}

struct RiverSubView2: View {
    @StateObject var viewmodel = RiverViewModel()
    let name : String
    
    var body: some View {
        VStack{
            Rectangle()
                .frame(height: 10)
            Text("RiverSubView2")
                .font(.title)
            Text("(StateObject)")
                .padding(10)
            
            Text(name)
            HStack{
                Text("\(viewmodel.data)")
                    .font(.largeTitle)
                    .padding()
                Image(systemName: "plus.circle")
                    .font(.system(size: 40))
                    .foregroundColor(.red)
                    .onTapGesture {
                        viewmodel.addCount5()
                    }
                Image(systemName: "minus.circle")
                    .font(.system(size: 40))
                    .foregroundColor(.red)
                    .onTapGesture {
                        viewmodel.addCount5()
                    }
            }
            RiverSubView3(viewmodel: viewmodel)
        }
    }
}

struct RiverSubView3: View {
    @ObservedObject var viewmodel : RiverViewModel
    
    var body: some View {
        VStack{
            Divider()
            Text("RiverSubView3")
                .font(.title2)
            Text("(RiverSubView에서 viewmodel을 ObservedObject로 받아옴)")
                .padding(10)
            
            HStack{
                Text("\(viewmodel.data)")
                    .font(.largeTitle)
                    .padding()
                Image(systemName: "plus.circle")
                    .font(.system(size: 40))
                    .foregroundColor(.red)
                    .onTapGesture {
                        viewmodel.addCount5()
                    }
                Image(systemName: "minus.circle")
                    .font(.system(size: 40))
                    .foregroundColor(.red)
                    .onTapGesture {
                        viewmodel.addCount5()
                    }
            }
        }
    }
}

struct RiverView_Previews: PreviewProvider {
    static var previews: some View {
        RiverView()
    }
}
반응형

'오뚝이 개발자 > SwiftUI' 카테고리의 다른 글

SwiftUI : Custom Color  (0) 2022.05.24
SwiftUI : dark mode preview  (0) 2022.05.24
onAppear vs onRecieve  (0) 2022.05.01
@EnvironmentObject ????  (0) 2022.05.01
AppStorage  (1) 2022.04.29
Comments