[SwiftUI] @StateObject , @ObservedObject, @EnviromentObject의 역할과 차이점을 자세하게 알아보자

SwiftUI를 접하면서 이 Wrapper를 활용해서 데이터 변동을 관리하는 내용이 나에겐 아주아주 흥미로운 것 같다 프로젝트를 진행하며 역할을 줄때마다 재밌다..

오늘은 이전에 살짝 어떤 차이가 있나 알아봤지만 오늘은 자세하게 한번 파보려고 한다.

 

간단한 iOS 기기 목록을 나타내는 방식으로 해당 내용을 정리해 보도록 하자!

 

@StateObject

먼저 이 StateObject는 SwiftUI에서 뷰의 수명동안 객체의 상태를 소유하고 관리하는 역할을 담당한다. 그렇기 때문에 StateObject는 부모뷰에서 View가 처음 생성되어 초기화 할때 주로 사용된다는 특징을 가지고 있다!

 

먼저 더미 데이터 모델을 만들어보자

import Foundation

struct iOSdeviceModel: Identifiable {
    let id: String = UUID().uuidString
    let name: String
}

이렇게 각각 고유한 아이디를 가지는 모델을 만들어 둘 예정이다. 이렇게 각각의 id를 부여해주면 나중에 ForEach를 사용할때 별도의 id 를 지정해주지 않아도 된다.

 

그런 다음 데이터를 불러와야 하는데 메서드를 활용해 데이터를 호출하려면 MVVM에서는 ViewModel이 필요하다. ViewModel을 만들어보자!

class iOSDeviceViewModel: ObservableObject {
    
    @Published var iOSDeviceArray: [iOSdeviceModel] = []
    
    init() {
        getData()
    }
    
    func getData() {
        let iphone = iOSdeviceModel(name: "아이폰")
        let iPad = iOSdeviceModel(name: "아이패드")
        let iMac = iOSdeviceModel(name: "아이맥")
        let appleWatch = iOSdeviceModel(name: "애플워치")
        
        self.iOSDeviceArray.append(iphone)
        self.iOSDeviceArray.append(iPad)
        self.iOSDeviceArray.append(iMac)
        self.iOSDeviceArray.append(appleWatch)
    }
}

보이는 것과 같이 VM은 Class의 형태로 참조 타입으로 구성되어야 한다. 이 값을 사용할때 별도로 Struct와 같이 복사되어 사용되는 값 타입은 적절하지 않기 때문.

 

여기서 또한 주의해야하는 점은 ObservableObject는 Wrapper가 아니고 프로토콜이다. 이 후 설명할 ObservedObject와는 완전 다른 친구이므로 혼동하면 안된다!

 

이 곳에서 이제 뷰에서는 @State를 활용해 인스턴스를 만들어주었지만 Class에서는 @Published를 활용해 구현하게 된다.

@Published var iOSDeviceArray: [iOSdeviceModel] = []

아까 만들어 놨던 데이터 모델을 가지는 iOSDeviceArray를 만들어주고 init을 통해 뒤에 설명할 메서드가 실행될 수 있도록 구현한다.

 

그런 다음 데이터를 넣어주는 로직을 넣고 해당 내용을 Published되어있는 Array에 추가하도록 메서드를 구현한다면 데이터 구성은 완료된다.

 

이제 해당 데이터를 올려 줄 View를 구현해보자

struct iOSDeviceView: View {
    
    //처음 ViewModel을 초기화 할때는 StateObject로 불러오기
    
    @StateObject var viewModel: iOSDeviceViewModel = iOSDeviceViewModel()
    
    var body: some View {
        NavigationView {
            List {
                ForEach(viewModel.iOSDeviceArray) { item in
                    NavigationLink {
                        iOSDeviceView2(selectedItem: item.name)
                    } label: {
                        Text(item.name)
                    }
                }
            }
        }
        .environmentObject(viewModel)
    }
}

이렇게 뷰를 만들어 구현하였는데 여기서 지금 알아볼 @StateObject를 찾을 수 있다. 이 뷰는 가장 처음 등장하는 쉽게 말해 부모뷰의 역할을 하기 때문에 VM의 데이터를 가져올때 @StateObject의 Wrapper를 사용해 데이터를 가져오게 된다.

 

이렇게 가져온 ViewModel의 데이터를 그대로 넣어 사용하게 된다면 사진과 같이 데이터가 잘 들어오는 것을 볼 수 있다.

이와 같이 @StateObject는 부모뷰에서 데이터가 변하는 값을 지정할때 사용한다.

 

ObservedObject

그렇다면 다음은 ObservedObject를 살펴보자 ObservedObject는 객체를 소유하지 않고 객체가 변경될 때 뷰를 업데이트 하는데 사용된다!

 

StateObject의 경우는 부모뷰를 소유해 객체가 변경될 때 뷰를 업데이트 하는데 여기서 차이점을 확인할 수 있다.

 

쉽게 말해 ObservedObject는 자식뷰 그러니까 부모뷰 외에 다른 하위 뷰에서 변경되는 지점을 파악하는데 사용한다는 점이다.

 

지금 부모뷰에서 NavigationLink를 활용해 다음 뷰로 넘어가도록 구현되어있으니 이 점을 그대로 따라가보자!

 

다음 뷰로 넘어갈라면 이제 새로운 뷰를 만들어줘야 한다. 즉 부모뷰의 하위 뷰를 만들어 구성해야한다!

struct SecondScreen: View {
    
    @ObservedObject var fruitViewModel = FruitViewModel()
    
    @Environment(\\.presentationMode) var presentationMode
    
    var body: some View {
        ZStack {
            Color.green.ignoresSafeArea()
            
            VStack (spacing: 20) {
                ForEach(fruitViewModel.fruitArray) { fruit in
                    Text(fruit.name)
                        .font(.headline)
                        .foregroundColor(.white)
                }
                
                Button(action: {
                    presentationMode.wrappedValue.dismiss()
                }, label: {
                    Text("뒤로 가기")
                        .font(.largeTitle)
                        .foregroundColor(.white)
                        .fontWeight(.bold)
                })
            }
        }
        
    }
}

이 내용을 살펴보면 이미 상위 뷰에 fruitViewModel를 가져와서 데이터를 표현해주고 있고 다음 화면으로 넘어와서 다시 데이터를 불러와야할 때 이렇게 ObservedObject를 사용해서 VM의 데이터를 가져오게 된다는 점이다.

 

이렇게 구현하면 VM에서 데이터가 변동될때도 뷰가 반응해서 값이 변경되게 된다!

 

각 상황별로 사용하는 Wrapper가 다르다는게 너무 재밌다..이게 왜 재밌지? 모르겠다..

 

EnviromentObject

마지막으로 EnvironmentObject는 뷰 계층 구조 전체에 객체를 공유하고 전달하는데 사용되는 환경 객체이다! 쉽게 말해서 StateObject는 부모뷰 , ObservedObject는 하위뷰에서 사용한다고 했을때 만약 많은 계층을 가지고 있는 형태의 구조라면 차례대로 다 거쳐서 결국에 사용해야하는 곳까지 이어져야 도착한다는 것인데

 

EnvironmentObject는 이러한 선형 구조로 데이터를 전달하는 것이 아닌 어느 위치든 내가 원하는 곳에 넣을 수 있다는 장점이 있다.

 

사진을 보면 이해가 조금 더 쉽다!

 

초록색 View가 데이터를 받아야하는 뷰라고 가정했을때 이렇게 계층을 다 거쳐서 받는 것이 아니라 원하는 곳에 내가 바로 뿌려줄 수 있다는 것!!!

 

코드를 통해 다시 알아보자

struct iOSDeviceView3: View {
    
    // @StateObject에서 선언한 viewModel를 @EnviromentObject를 통해 가져오기
    @EnvironmentObject var viewModel: iOSDeviceViewModel
    
    var body: some View {
        ZStack {
            Color.cyan.ignoresSafeArea()
            
            ScrollView {
                VStack (spacing: 20) {
                    ForEach(viewModel.iOSDeviceArray) { item in
                        Text(item.name)
                    }
                }
                .foregroundColor(.white)
                .font(.largeTitle)
            }
        }
        
    }
}

지금 보이는 iOSDeviceView3는 iOSDeviceView1 → iOSDeviceView2 → iOSDeviceView3 이렇게 뷰가 이동되는 형태의 코드 구성이다.

 

현재 그렇다면 3에 VM의 데이터를 호출하려고 한다면, 바로 바로 EnvironmentObject를 사용하면 된다는 점

// @StateObject에서 선언한 viewModel를 @EnviromentObject를 통해 가져오기
@EnvironmentObject var viewModel: iOSDeviceViewModel

이렇게 부모뷰에서 이미 선언되어있는 VM을 EnvironmentObject를 통해 가져온 상태로 이해하면 된다.

 

이렇게 값을 가져와서 데이터를 넣어주면 된다!

 

다만 한가지 더 신경써야하는 부분이 있다.

바로 부모뷰에서 이 EnvironmentObject를 사용한다는 점을 명시해야 한다는 것

var body: some View {
    NavigationView {
        List {
            ForEach(viewModel.iOSDeviceArray) { item in
                NavigationLink {
                    iOSDeviceView2(selectedItem: item.name)
                } label: {
                    Text(item.name)
                }
            }
        }
    }
    .environmentObject(viewModel)
}

부모뷰의 바디이다. 여기서 부모뷰가 가지고있는 네비게이션 뷰 객체에 @StateObject를 통해 가져온 viewModel을 EnvironmentObject로 사용할 수 있다고 지정을 해줘야 한다.

.environmentObject(viewModel)

이 점을 잊지않고 넣어준다면 바로 구현 완료된다 🙂

 

오늘은 항상 사용되고 아주 중요한 @StateObject , @ObservedObject, @EnviromentObject의 역할과 차이점을 자세하게 알아보는 시간을 가졌다.

 

나도 한번 다시 정리하면서 머리속으로 정리가 잘 된 것 같다 🙂

오늘은 여기까지!