[SwiftUI] @ObservedObject와 @StateObject의 차이점

SwiftUI에서 @ObservedObject와 @StateObject는 데이터 흐름을 관리하는 중요한 속성 래퍼이다. 각각의 개념 예시 그리고 차이점에 대해서 알아보자.

 

@ObservedObject

먼저 @ObservedObject는 외부에서 생성되고 관리되는 관찰 가능한 객체를 뷰에 연결하는데 사용된다.

 

이 객체는 ObservedObject 프로토콜을 준수해야 하며, @Published 속성이 변경될 때 즉 객체가 변경될 때 마다 뷰를 자동으로 업데이트 하도록 한다.

class UserViewModel: ObservableObject {
		@Published var username: String = ""
		@Published var isLoggedIn: Bool = false
}

struct UserView: View {
		@ObservedObject var viewModel: UserViewModel
		
		var body: some VIew {
			VStack {
				TextField("Username", text: $viewModel.username)
				Text("Login Status: \\(viewMoel.isLoggedIn ? "Logged In" : "Logged Out"
			}
		}
}

@ObservedObject는 주로 상위 뷰에서 하위 뷰로 데이터를 전달할때 사용된다 이를 통해서 여러 뷰에서 동일한 데이터 소스를 공유할 수 있다.

struct ParentView: View {
    @StateObject private var viewModel = UserViewModel()

    var body: some View {
        VStack {
            UserView(viewModel: viewModel)
            SettingsView(viewModel: viewModel)
        }
    }
}

@ObservedObject는 약한 참조를 사용한다. 메모리 관리 측면에서 매우 중요한 부분인데 뷰가 사라질때 @ObservedObject로 선언된 객체가 자동으로 해제될 수 있도록 한다.

 

@ObservedObject는 ObservedObject의 objectWillChange 퍼블리셔를 사용하여 뷰 업데이트를 트리거 한다. @Published 속성이 변경될 때 마다 이 퍼블리셔가 이벤트를 발생시키고, SwiftUI는 이를 감지하여 뷰를 다시 렌더링 한다.

 

주의해야할 점은 뷰가 자주 재생성되는 경우에 @ObservedObject를 사용한다면 객체가 매번 새로 초기화 되어 성능 문제가 발생할 수 있다.

 

List나 ForEach 내부의 뷰에서 @ObservedObject를 사용할 때는 특히 주의해야 하는데 이러한 상황에서는 @StateObject를 사용하는 것이 더 안정적일 수 있다.

 

복잡한 뷰 구조에서 @ObservedObject를 사용할 때는 데이터 흐름을 명확히 관리해야한다. 환경 객체인 (@EnvironmentObject)나 의존성 주입을 고려해 볼 수 있다.

struct ComplexView: View {
	@ObservedObject var viewModel: ComplexViewModel
	
	var body: some View {
		VStack {
			SubView1(data: viewModel.data1)
			SubView2(data: viewModel.data2)
			SubView3(data: viewModel.data3)
		}
	}
}

@StateObject

@StateObject는 SwiftUI 중 뷰 내부에서 관찰 가능한 객체를 생성하고 관리하는데 사용되는 속성 래퍼이다.

 

iOS14 부터 도입된 이 래퍼는 뷰의 생명주기 동안 객체의 상태를 안정적으로 유지하는데 중요한 역할을 한다.

 

@StateObject는 ObservableObject 프로토콜을 준수하는 객체를 생성하고 해당 객체의 수명을 뷰의 수명과 일치시킨다. 뷰가 다시 생성되더라도 @StateObject로 선언된 객체는 유지된다.

class DataManager: ObservableObject {
    @Published var items: [String] = []
    
    func fetchItems() {
        items = ["Apple", "Banana", "Cherry"]
    }
}

struct ContentView: View {
    @StateObject private var dataManager = DataManager()

    var body: some View {
        List(dataManager.items, id: \\.self) { item in
            Text(item)
        }
        .onAppear {
            dataManager.fetchItems()
        }
    }
}

@StateObject는 뷰의 생명주기 동안 단 한번만 초기화 된다. 이는 뷰가 여러번 다시 그려지더라도 객체의 상태가 보존된다는 것을 의미한다.

 

@StateObject는 강한 참조를 사용한다. 이는 뷰가 메모리에 존재하는 동안 객체가 계속 유지됨을 보장한다.

 

@StateObject를 사용하면 뷰가 다시 생성될 때 마다 객체를 재생성하는 것을 방지할 수 있어 성능이 향상된다. 특히 복잡한 초기화 로직이 있는 객체에 유용하다.

class ExpensiveDataProcessor: ObservableObject {
    @Published var result: String = ""
    
    init() {
        // 복잡하고 시간이 걸리는 초기화 과정
        print("Expensive initialization")
    }
    
    func process() {
        // 데이터 처리 로직
    }
}

struct ProcessingView: View {
    @StateObject private var processor = ExpensiveDataProcessor()

    var body: some View {
        Text(processor.result)
            .onAppear {
                processor.process()
            }
    }
}

@StateObject 를 사용하면 뷰가 다시 그려질 때마다 데이터가 초기화 되는 것을 방지할 수 있다. 이는 사용자 경험을 향상 시키고 예기치 않은 데이터 손실을 방지한다.

 

@StateObject는 주로 뷰 내부에서 객체를 생성할 때 사용된다. 그러나 때로는 외부에서 생성된 객체를 주입받아야 할 수도 있다.

swiftCopystruct ConfigurableView: View {
    @StateObject private var viewModel: ViewModel

    init(viewModel: ViewModel? = nil) {
        _viewModel = StateObject(wrappedValue: viewModel ?? ViewModel())
    }

    var body: some View {
        // 뷰 내용
    }
}

@StateObject는 종종 @Published, @Binding, 그리고 @EnvironmentObject와 함께 사용된다. 이렇게 조합하여 복잡한 데이터 흐름을 관리할 수 있다.

class AppState: ObservableObject {
    @Published var user: User?
}

struct RootView: View {
    @StateObject private var appState = AppState()

    var body: some View {
        if let user = appState.user {
            UserView(user: user)
        } else {
            LoginView()
        }
    }
}

@StateObject는 뷰의 프로퍼티로만 사용해야한다. 지역 변수나 계산된 프로퍼티에서는 사용할 수 없다.

뷰의 Init()메서드에서 @StateObject를 직접 초기화 하지 않도록 주의해야 한다.

 

@ObservedObject와 @StateObject의 차이점

생명 주기 관리

  • ObservedObject : 뷰가 재생성될 때 마다 새로운 인스턴스가 생성될 수 있다. 뷰의 생명주기와 독립적으로 관리된다.
  • StateObject : 뷰의 생명주기 동안 한번만 생성되고 유지된다. 뷰가 재생성되어도 동일한 인스턴스가 유지된다.

소유권과 생성 위치

  • ObservedObject : 외부에서 생성되고 관리되는 객체에 사용된다. 주로 상위 뷰나 다른 컴포넌트에서 생성된 객체를 전달받을 때 사용한다.
  • StateObject : 뷰 내부에서 생성되고 관리되는 객체에 사용된다. 해당 뷰가 객체의 소유자가 됩니다.

메모리 관리

  • ObservedObject : 약한 참조를 사용한다. 메모리 관리가 더 유연하지만 객체가 예기치 않게 해제될 수 있다.
  • StateObject : 강한 참조를 사용한다. 뷰가 존재하는 동안 객체가 메모리에서 해제되지 않음을 보장한다.

성능 특성

  • ObservedObject : 뷰가 자주 재생성되는 경우 성능 저하가 발생할 수 있다. 객체가 자주 재초기화될 수 있어 비용이 큰 초기화 로직이 반복될 수 있다.
  • StateObject : 뷰가 재생성되어도 객체를 유지되므로 성능이 더 안정적이다. 복잡한 초기화 로직을 가진 객체에 특히 유용하다.

데이터 일관성

  • ObservedObject : 뷰가 재생성될 때 데이터가 초기화될 위험이 있다. 데이터의 일관성을 유지하기 위해 추가적인 로직이 필요할 수 있다.
  • StateObject : 뷰가 재생성되어도 데이터가 안정적으로 유지된다. 데이터의 일관성을 자동으로 보장한다.

적합한 사용 방식

  • ObservedObject : 여러 뷰에서 공유되는 데이터 모델에 적합하다. 의존성 주입 패턴을 구현할 때 유용하다.
  • StateObject : 뷰 특정적인 상태나 뷰 모델을 관리할때 적합하다. 싱글톤 패턴이나 전역 상태 관리에 유용하다.

초기화 방식

  • ObservedObject : 주로 생성자를 통해 외부에서 주입 받는다.
  • StateObject : 뷰 내부에서 직접 초기화 한다.

SwiftUI의 업데이트 메커니즘과 상호작용

  • ObservedObject : SwiftUI의 뷰 업데이트 주기에 더 민감할 수 있다. 부모 뷰의 상태 변화에 따라 재생성될 가능성이 높다.
  • StateObject : SwiftUI의 뷰 업데이트 주기와 독립적으로 동작한다. 뷰모뷰의 상태 변화에 영향을 받지 않고 안정적으로 유지된다.