SwiftUI와 Combine은 함께 사용되면서 더욱 강력한 상태 관리와 반응형 데이터 흐름을 제공합니다. Combine에서는 단방향 데이터 흐름을 통해 데이터를 관리하는데, 이를 이해하고 SwiftUI의 속성 래퍼인 @StateObject, @ObservedObject, @State, @Binding 등을 활용하면 데이터 흐름을 간결하고 명확하게 유지할 수 있습니다. 이번 글에서는 Combine의 기본 개념과 SwiftUI의 상태 관리를 위해 사용되는 속성 래퍼들을 순서대로 살펴보겠습니다.
단방향 데이터 흐름은 데이터가 한 방향으로만 흐르게 하여 상태를 명확하게 관리하는 개념입니다. 일반적으로 모델에서부터 뷰까지 한 방향으로 데이터가 전달되고 뷰에서 발생한 이벤트가 다시 모델에 영향을 미치는 구조를 가지게 됩니다. SwiftUI와 Combine을 사용하면 이 흐름을 자연스럽게 구현할 수 있습니다.
Combine에서는 Publisher와 Subscriber를 통해 이 데이터 흐름을 관리합니다.
데이터를 생성하고 전달하는 객체입니다.
데이터를 받아서 사용하고, 상태를 업데이트합니다.
예를 들어 뷰 모델(ViewModel)에서 Publisher가 데이터를 방출하고 SwiftUI 뷰가 이를 구독하여 뷰를 업데이트하는 식으로 흐름이 구성됩니다. 이러한 단방향 데이터 흐름 덕분에 데이터와 상태가 한 곳에서 일관성 있게 관리되며 코드의 예측 가능성과 유지보수성이 높아집니다.
import Combine
import SwiftUI
class MyViewModel: ObservableObject {
@Published var data: String = "Initial Data"
func fetchData() {
data = "Updated Data"
}
}
이 예제에서 MyViewModel은 @Published 속성으로 상태를 관리하며, data 값이 변경될 때마다 뷰에 자동으로 반영됩니다.
@ObservableObject는 Combine의 ObservableObject 프로토콜을 준수하는 클래스를 사용해 데이터 상태를 관리하는 속성 래퍼입니다. SwiftUI는 ObservableObject에 선언된 @Published 속성이 변경될 때마다 뷰를 자동으로 업데이트합니다.
뷰 모델에 @ObservableObject를 적용하면 데이터가 변경될 때마다 뷰가 자동으로 갱신됩니다.
@Published로 선언된 속성이 업데이트되면 SwiftUI 뷰에 자동으로 반영됩니다.
Combine을 제대로 이해하기 위해 알아야 할 주요 개념들을 간단히 설명하겠습니다.
데이터를 생성하여 방출하는 객체로, SwiftUI와 함께 사용하면 뷰가 반응적으로 업데이트됩니다. Just, PassthroughSubject, CurrentValueSubject 등의 다양한 타입이 있습니다.
데이터를 받아서 처리하는 객체입니다. SwiftUI에서는 주로 sink를 사용하여 데이터를 구독합니다.
데이터 흐름 중간에 데이터 가공, 변환 등의 처리를 합니다. map, filter 등이 있습니다.
구독을 취소하거나 메모리에서 해제하는 데 사용됩니다.
데이터 작업을 수행할 스레드를 지정합니다.
위의 Combine을 활용한 예시는 다음과 같습니다.
let publisher = Just("Hello Combine")
publisher
.map { $0.uppercased() }
.sink { value in
print("Received:", value)
}
Combine과 함께 사용할 때 SwiftUI에서 제공하는 속성 래퍼(@StateObject, @ObservedObject, @State, @Binding)를 활용하면 데이터 흐름이 더욱 간결해집니다.
@StateObject는 뷰가 처음 생성될 때 객체를 한 번만 초기화하고 소유권을 가집니다. 주로 뷰의 생명 주기 동안 상태를 관리하고 새로운 데이터 객체를 소유해야 할 때 사용됩니다.
struct ContentView: View {
@StateObject private var viewModel = MyViewModel() // 객체 생성 및 소유
var body: some View {
Text(viewModel.data)
}
}
ContentView는 @StateObject로 viewModel을 한 번만 초기화하고, 뷰가 다시 그려지더라도 동일한 객체를 사용합니다.
@ObservedObject는 외부에서 생성된 객체를 참조할 때 사용됩니다. 객체의 소유권을 가지지 않으며 주로 부모 뷰에서 생성된 ViewModel을 자식 뷰에서 참조할 때 사용합니다.
struct ParentView: View {
@StateObject private var viewModel = MyViewModel() // 부모에서 생성
var body: some View {
ChildView(viewModel: viewModel) // 자식 뷰로 전달
}
}
struct ChildView: View {
@ObservedObject var viewModel: MyViewModel // 외부 객체 참조
var body: some View {
Text(viewModel.data)
}
}
ParentView가 viewModel을 생성하고, ChildView는 @ObservedObject를 통해 이 객체를 참조하여 데이터 변경 사항을 반영합니다.
@State는 뷰 내부에서만 사용하는 개인적인 상태를 관리할 때 사용됩니다. 다른 뷰에서는 접근할 수 없고, 해당 뷰 내에서만 상태가 유지됩니다.
struct CounterView: View {
@State private var count = 0 // 개별 뷰 전용 상태
var body: some View {
Button("Increment") {
count += 1
}
}
}
위 코드에서 count는 CounterView 내에서만 사용되며, 뷰가 업데이트될 때마다 버튼 클릭에 따라 상태가 변경됩니다.
@Binding은 부모 뷰에서 자식 뷰로 상태를 전달하고 자식 뷰에서도 이 상태를 수정할 수 있도록 해줍니다. 부모 뷰의 상태를 자식 뷰가 직접 제어해야 할 때 사용됩니다.
struct ParentView: View {
@State private var isOn = false
var body: some View {
ToggleView(isOn: $isOn) // 바인딩을 통해 상태 전달
}
}
struct ToggleView: View {
@Binding var isOn: Bool // 부모 상태를 바인딩
var body: some View {
Toggle("Switch", isOn: $isOn)
}
}
ParentView의 isOn 상태는 ToggleView에 바인딩되어, 자식 뷰에서 값을 변경할 수 있습니다.
Combine의 단방향 데이터 흐름과 SwiftUI의 @StateObject, @ObservedObject, @State, @Binding은 앱의 상태와 데이터 흐름을 명확하게 관리하고 뷰가 변화하는 데이터를 반응적으로 반영할 수 있도록 도와줍니다.
위의 개념들을 이해하고 올바르게 사용하면 SwiftUI와 Combine을 활용한 더욱 깔끔하고 유지보수하기 쉬운 앱을 개발할 수 있습니다.