XLOG

[DataFlow] State, Binding, StateObject, ObservedObject, EnvironmentObject 는 무엇이고 차이는? 본문

Swift/SwiftUI

[DataFlow] State, Binding, StateObject, ObservedObject, EnvironmentObject 는 무엇이고 차이는?

X_PROFIT 2024. 7. 1. 13:15

1. State

Single source of truth 로 View 에서 PropertyWrapper 로 정의 한다. 그로 인해 View 와 따로 독립적으로 SwiftUI 가 데이터를 관리해 주며 생명 주기는 View 의 생명주기와 동일하다. State 값의 변화를 view에 알려주면 View 는 re-render 한다.

단 해당 State 가 정의된 View 의 body 내부에 직접적으로 state 값을 활용하여 View 의 내용이 달라지는 요소가 없다면 re-render 하지 않는다.

import SwiftUI

struct ParentView: View {
    
    @State var count: Int = 0
    
    var body: some View {
        VStack {
            let _ = Self._printChanges()
            
            Text("Text")
                .padding()
                .background(Color.randomColor)

            
            Text("\(count)")
                .padding()
                .background(Color.randomColor)
            
            Button(action: { count += 1 }) {
                Text("+")
                    .padding()
                    .background(Color.randomColor)
            }

        }
        .padding()
        .background(Color.randomColor)
    }
}

View 는 정상적으로 state 값의 변화를 확인하고 body 전체를 re-render 해준다. 하지만 Text("\(count)") 부분을 뺀다면...

 

 

import SwiftUI

struct ParentView: View {
    
    @State var count: Int = 0
    
    var body: some View {
        VStack {
            let _ = Self._printChanges()
            
            Text("Text")
                .padding()
                .background(Color.randomColor)
            
            Button(action: { 
            	print("call button action")
            	count += 1 
            }) {
                Text("+")
                    .padding()
                    .background(Color.randomColor)
            }

        }
        .padding()
        .background(Color.randomColor)
    }
}​

 

body 내부 view에서 button action을 제외하고 count 를 사용하지 않기에 view 에서는 state 값의 변화를 catch 하지 못하고 re-render를 하지 않는 것을 확인할 수 있다.

2. Binding

Binding 은 State 로 정의한 SSOT(Single source of truth) 의 값을 하위 뷰로 전달할 때 사용한다. 하위 View에 State PropertyWrapper 를 사용하게 되면 하위 뷰도 독립적이 Source of truth 를 생성하지만 Binding 으로 정의하여 값을 전달받게 되면 상위View 의 Source of truth 를 바라보게 된다.

struct ParentView: View {
	@State private var count: Int = 0
    
    var body: some View {
	    ChildView(count: $count)
    }
}

struct ChildView: View {
	@Binding var count: Int
    
    var body: some View {
    	Text("\(count)")
    }
}

 

3. StateObject

ObservableObject의 State 버전이라고 생각할 수 있다. 생명주기도 View 와 동일하다. ObservableObject 내부에 Published 프로퍼티의 값의 변화가 발생하면 SwiftUI 는 View 를 업데이트 시킨다. 하지만 확인을 하는 과정 중에 알게된 사실인데, StateObject 는 State 와 달리, Published 값의 변화가 있다면 무조건 view 를 update(re-render) 시킨다.

import SwiftUI

struct ParentView: View {
    
    @StateObject var vm: PracticeViewModel = PracticeViewModel()
    
    var body: some View {
        VStack {
            let _ = Self._printChanges()
            
            Text("Text")
                .padding()
                .background(Color.randomColor)
    
            Button(action: {
                print("call button action")
                vm.count += 1
            }) {
                Text("+")
                    .padding()
                    .background(Color.randomColor)
            }
        }
        .padding()
        .background(Color.randomColor)
    }
}

 

 

4. ObservedObject

View 내부에서 ObservableObject 의 Published property 의 변화를 View에 알려 View를 업데이트 시킬 때 사용한다. 하지만 StateObject 와의 차이가 있다. StateObject 의 경우 따로 데이터를 관리하며 View의 생명주기와 같은 생명주기를 갖는다. 하지만 ObservedObject 의 경우 re-render 될 때마다 새로운 값을 할당 받는다. 그래서 주로 StateObject 를 하위View의 값을 전달할 때 사용한다. 즉 Binding 과 비슷하다.

import SwiftUI

struct ParentView: View {
    
    @StateObject var vm: PracticeViewModel = PracticeViewModel()
    
    var body: some View {
        VStack {
            let _ = Self._printChanges()
            
            Text("Text")
                .padding()
                .background(Color.randomColor)
    
            Button(action: {
                print("call button action")
                vm.count += 1
            }) {
                Text("+")
                    .padding()
                    .background(Color.randomColor)
            }
            
            ObservedObjectChildView()
        }
        .padding()
        .background(Color.randomColor)
    }
}

struct ObservedObjectChildView: View {
    
    @ObservedObject var vm: PracticeViewModel
    
    init(vm: PracticeViewModel? = nil) {
        print("ChildView init")
        self.vm = vm ?? PracticeViewModel()
    }
    
    var body: some View {
        VStack {
            let _ = Self._printChanges()
            Text("Child View : \(vm.count)")
                .padding()
                .background(Color.randomColor)
            
            Button(action: {
                print("call button action")
                vm.count += 1
            }) {
                Text("+")
                    .padding()
                    .background(Color.randomColor)
            }
        }
    }
}

 

child View 의 init 이 불리며 ObservedObject 의 값이 새로 할당을 받게 되고, 그러면서 View 도 update 가 되게 된다. 

struct ObservedObjectChildView: View {
    
    @ObservedObject var vm: PracticeViewModel
    
    init(vm: StateObject<PracticeViewModel>? = nil) {
        print("ChildView init")
		self._vm = vm ?? StateObject(wrappedValue: PracticeViewModel())
    }
    
    var body: some View {
    '''
    '''
    }   
}

 

StateObject 의 경우 childView의 init 이 불리더라도 View 에서는 ViewModel 의 변화를 감지하지 못하고, 값 또한 유지 되는 것을 볼 수 있다.

 

5. EnvironmentObject

ObservableObject 를 View 계층 내부에서 전역적으로 사용할 수 있게 해준다.

import SwiftUI

@main
struct DataFlowPracticeApp: App {

	@StateObject private var vm = PracticeViewModel()
    
    var body: some Scene {
        WindowGroup {
            ContentView()
            	.environmentObject(vm)
        }
    }
}


struct ContentView: View {
    
    @EnvironmentObject var vm: PracticeViewModel
    
    var body: some View {
        VStack {
            let _ = Self._printChanges()
            
            
            Button(action: {
                print("call button action")
                vm.count += 1
            }) {
                Text("+")
                    .padding()
                    .background(Color.randomColor)
            }
       
            
            Text("\(vm.count)")
                .padding()
                .background(Color.randomColor)
            
        }
        .padding()
        .background(Color.randomColor)
    }
}

 

ContentView 하위 View 어디에서든지 @EnvironmentObject 를 통해 접근이 가능하다.