본문 바로가기

Combine과 SwiftUI

기술적인 이야기/애플 플랫폼 개발 2020. 1. 28.
반응형

컴바인을 활용해서 실무에 좀 더 유용한 내용을 알아봅시다. Combine을 활용하는 가장 직접적인 예는 아마도 이 글에서 살펴볼 Swift 5.1부터 제공되는 Property Wrapper와 이를 이용해 만들어진 SwiftUI의 몇 가지 기능이 이에 해당할지도 모르겠습니다.

이 글에서 살펴볼 것은 네 가지 키워드입니다. 바로 State, Binding, Observed Object, 그리고 Environment Object입니다. 이 키워드들은 Combine을 이용해 구현한 Property Wrapper를 응용해서 만들어진 것들이지요.

이 글은 Swift 5.1, Xcode 11.3을 기준으로 작성하였습니다.

State

State는 이름처럼 상태 그 자체를 나타냅니다. 영어로는 Source of Truth라고도 하기는 하던데... 뭐 하여간 상태가 변하면 UI도 이에 맞게 변해야겠지요. 실제로 사용되는 키워드는 @State입니다. 아래 예제를 봅시다.

struct ContentView: View {
    @State private var name = "World"

    var body: some View {
        VStack {
            Text("Hello, \(name)!")
                .padding()
            Button(
                action: { self.switchName() },
                label: { Text("Switch") }
            )
        }
    }

    func switchName() {
        if name == "World" {
            name = "Universe"
        } else {
            name = "World"
        }
    }
}

위 예제는 버튼을 누를 때 마다 name 프로퍼티의 값을 바꾸는데 이에 따라 뷰도 자동으로 내용이 업데이트되는 것을 확인할 수 있습니다. State로 명시된 프로퍼티인 name과 UI가 아주 긴밀하게 움직이는 모습을 볼 수 있지요. Combine 식으로 표현하자면, State로 표기된 퍼블리셔(Publisher)의 변화에 따라 서브스크라이버(Subscriber)인 View가 그 변화에 대응하고 있는 것입니다.

@State는 해당 View 내에서만 액세스 가능합니다. 따라서 @State로 적용할 프로퍼티는 가급적 private로 선언하는 것이 좋습니다. 다른 말로 표현하자면, State는 결국 특정 View의 특징 혹은 소유물이라는 의미로도 해석할 수 있습니다. 그래서 해당 프로퍼티를 소유하고 있는 View는 마음대로 액세스 할 수 있다면 외부의 다른 View는 액세스 하지 못 하거나 혹은 읽기만 가능해지는 디자인이 만들어집니다.

Binding

State는 특정 View 소유의 프로퍼티이자 상태를 명시할 때 사용합니다. 그렇다면 이와는 다르게 남의 View의 State를 가져와서 자기 자신이 사용하게 되는 경우도 있을 수 있습니다. Binding은 이렇게 남의 소유의 State를 자신과 묶어줄 때 사용합니다.

Binding은 두 가지 형태로 표현됩니다. 하나는 $ 마크로 자신의 State를 남에게 연결시켜주는 경우, 그리고 남의 State를 연결하기 위해 @Binding을 사용하는 경우입니다.

struct MyToggleButton: View {
    @Binding var value: Bool

    var body: some View {
        Button(action: {
            self.value.toggle()
        }, label: {
            Text(self.value ? "Hello" : "World")
        })
    }
}

struct ContentView: View {
    @State private var value = false

    var body: some View {
        VStack {
            MyToggleButton(value: $value)
        }
    }
}

위 예제도 버튼을 누를 때마다 값이 토글 되는 버튼을 표시하는 예제입니다. 다만 차이가 있다면 해당 값을 자신이 아닌 다른 View가 바꾼다는 것이지요. 즉 ContentView 소유의 value 프로퍼티를 $ 마크를 붙여서 연결하도록 MyToggleButton에 넘겨주었고, MyToggleButton이 이 연결을 받아서 값을 마음대로 바꾸고 있습니다.

결과적으로 State의 소유권을 잠시 남에게 양도해 줄 수 있는 기능이라고 볼 수 있겠네요.

ObservableObject and ObservedObject

앞서 본 @State@Binding은 스위프트의 값(Value) 타입에만 사용할 수 있다는 특징이 있습니다. 그렇다면 클래스의 경우는 어떻게 할 수 있을까요? 이를 위해서 ObservableObject라는 프로토콜과 @ObservedObject라는 Property Wrapper가 제공됩니다.

class MyData: ObservableObject {
    @Published var name = "World"
    @Published var buttonTitle = "Switch to Universe"

    func switchName() {
        if name == "World" {
            name = "Universe"
            buttonTitle = "Switch to World"
        } else {
            name = "World"
            buttonTitle = "Switch to Universe"
        }
    }
}

struct ContentView: View {
    @ObservedObject var data = MyData()

    var body: some View {
        VStack {
            Text("Hello, \(data.name)!")
                .padding()
            Button(
                action: { self.data.switchName() },
                label: { Text(self.data.buttonTitle) }
            )
        }
    }
}

@ObservedObject가 되기 위해서는 반드시 ObservableObject 프로토콜을 따르는 클래스여야 합니다. 또한 각 프로퍼티 값이 바뀌었을 때 제대로 업데이트가 된 것을 모니터링하려면 해당 프로퍼티에는 @Published를 붙여서 퍼블리셔로 쓸 수 있도록 해야 합니다.

이름처럼 반드시 클래스여야 하기 때문에 오브젝트 공유가 가능해진다는 장점이 있다는 것을 기억합니다. 그 외에는 State와 거의 비슷해 보이네요. 대신 외부 객체를 연결(Binding)시킬 때 $를 붙이지 않아도 됩니다.

참고로 ObservedObject 프로토콜은 과거에는 BindableObject라는 이름이었습니다. 비슷하게, @ObjectObserving은 과거에는 @ObjectBinding이라는 이름이었습니다. 그리고 요즘은 didSet을 통해 업데이트를 알려줘야 한다거나 objectWillChange를 구현해 줘야 하는 등의 일은 @Published를 통해 자동으로 채워지므로 꼭 필요하지는 않습니다. 혹시나 과거 버전의 튜토리얼을 참고하신다면 이 변화에 주의합시다.

EnvironmentObject

@EnvironmentObject@ObservedObject 와 비슷하게 쓸 수 있지만, 다른 특징으로 공유 환경을 만들어 준다는 점이 있습니다. 물론 이름이 오브젝트(Object)가 들어가 있기 때문에 클래스 타입만 사용할 수 있겠지요?

class SharedData: ObservableObject {
    @Published var configName = "default"
    ...
}

struct ContentView: View {
    @EnvironmentObject var sharedData: SharedData
    ...
}

struct FooView: View {
    @EnvironmentObject var sharedData: SharedData
    ...
}

코드 의미를 보면 sharedDataContentViewFooView에서 공유하려는 오브젝트임을 알 수 있습니다. 여기서 한 가지만 더 추가되면 양쪽의 sharedData 프로퍼티는 싱글턴 오브젝트처럼 공유할 수 있게 됩니다.

Xcode에서 SwiftUI 프로젝트를 생성했다면 SceneDelegate.swift라는 파일이 생성되어 있을 것입니다. 여기에 보면 UIHostingController를 생성할 때 ContentView(최초의 뷰) 인스턴스를 넣어주는 부분이 있습니다. 여기를 약간 고치면 됩니다.

var sharedData = SharedData()
...
window.rootViewController =
    UIHostingController(rootView: ContentView()
                                      .environmentObject(sharedData))

이렇게 하면 위에서 생성된 sharedData는 공유 오브젝트가 되어서 모든 뷰에서 공유할 수 있게 됩니다. 물론 @ObservedObject의 특징도 같이 사용할 수 있습니다.

마무리

Combine Framework에 대한 글을 쓸 때는 정말 이걸 뭐에 써먹나 했더니만 왜 이걸 만들었는지를 SwiftUI를 공부하다 보면 머리를 탁 치게 만듭니다. 물론 Rx를 베꼈다는 평을 받고는 있지만, 써드파티 라이브러리가 가지는 단점인 '플랫폼 버전 변화에 따른 대응을 기다릴 필요'가 없다는 큰 장점은 버리기 힘들지요. 물론 아직 현업에 쓰기엔 약간 부족하기도 하고 OS 최소 버전 제한 등 여럿 걸리는 점이 없지는 않지만 그래서 더더욱 SwiftUI의 미래가 기대됩니다.

관련 글

728x90
반응형

댓글