IOS/SwiftUI

TCA - What is ViewStore

gangmin 2023. 10. 8. 22:25

안녕하세요! 미니입니다. 이번에는 TCA로 프로젝트를 진행하면서 학습한 내용을 정리해보려고 합니다. 많은 것을 정리하는 것은 아니고, 개념적인 내용에 대해서 설명하려고 합니다. TCA를 조금 써보면 ViewStore라는 친구를 바로 만나게 되는데 내부적으로 어떤 동작이 일어나는 지 궁금해져서 학습한 내용을 공유해보려고 합니다.

우선 공식 문서 부터 조져보자구욧!

ViewStore는 상태의 변화와 액션 발생에 대해서 관찰할 수 있는 객체이다. 일반적으로 SwiftUI의 뷰나 UIView, UIViewController 타입에서 사용할 수 있다. 하지만, 상태를 관찰하고 액션을 전달하는 것이 적합한 곳이면 어디든 사용할 수 있다.


상태를 관찰할 수 있다는 말에서 Combine과 SwiftUI에서 많이 보던 친구라는 것이 느껴지시나요? 저는 ObservableObject 라는 친구가 생각 났습니다.
이렇게 여쭤본 이유는 이 친구이기 때문이에요. ViewStore는 다른 친구가 아니라 ObservableObject를 채택한 상속 불가능한 참조 타입입니다.

@dynamicMemberLookup
public final class ViewStore<ViewState, ViewAction>: ObservableObject {
    public init<State>(
    _ store: Store<State, ViewAction>,
    observe toViewState: @escaping (_ state: State) -> ViewState,
    removeDuplicates isDuplicate: @escaping (_ lhs: ViewState, _ rhs: ViewState) -> Bool
  ) {
    self._send = { store.send($0, originatingFrom: nil) }
    self._state = CurrentValueRelay(toViewState(store.state.value))
    self._isInvalidated = store._isInvalidated
    self.viewCancellable = store.state
      .map(toViewState)
      .removeDuplicates(by: isDuplicate)
      .sink { [weak objectWillChange = self.objectWillChange, weak _state = self._state] in
        guard let objectWillChange = objectWillChange, let _state = _state else { return }
        objectWillChange.send()
        _state.value = $0
      }
  }
}

ObservableObject라는 것은 납득이 가능하지만, dynamicMemberLookup이라는 어노테이션을 모르기에 찾아보려고 합니다.

dynamicMemberLookup

영어는 무적권 직독직해!! 동적 / 멤버 / 조회 라는 해석을 할 수 있겠죠? 좀 더 유식하게 말을 바꿔보시죠! 동적으로 값들을 조회할 수 있다. 와 같이 해석할 수 있습니다. 실제로 저희가 많이 사용하던 Subscript를 문자열이나 key값을 대괄호안에 작성해서 값을 얻게 되지만, dynamicMemberLookup 을 활용하게 되면, 도트를 통해서 접근할 수 있게 됩니다.

@dynamicMemberLookup
struct Person {
    var name: String
    var age: Int

    subscript(key: String) -> String {
        switch key {
        case "info":
            return "\(name) : \(age)"
        default: 
            return "nothing"    
        }
    }

    subscript(dynamicMember key: String) -> String {
        switch key {
        case "member":
            return "\(name) : \(age)"
        default: 
            return "nothing"    
        }
    }
}

var p = Person(name:"Sweet", age:17)
p["info"]
p.member

그렇다는 건 ViewStore는 내부적으로 키값을 생성하지 않고 도트를 통해서 속성에 접근하거나 값을 얻을 수 있다는 이야기가 되겠군요.

self.viewCancellable = store.state
      .map(toViewState)
      .removeDuplicates(by: isDuplicate)
      .sink { [weak objectWillChange = self.objectWillChange, weak _state = self._state] in
        guard let objectWillChange = objectWillChange, let _state = _state else { return }
        objectWillChange.send()
        _state.value = $0
      }

생성자 내에서 이부분을 보게 되면, 어떻게 상태가 변경되었을 때 알려주는 거지? 와 같은 궁금증을 풀 수 있습니다. store에 생성되어 있는 state는 Publisher 타입이기 때문에 sink를 통해서 변경에 대한 이벤트를 받을 수 있습니다. 이를 통해서 내부적으로 값이 변경되었다는 이벤트를 다시 한번 방출 시키고 변경된 값을 기존의 값으로 업데이트하게 됩니다. 이런 방식을 통해서 저희는 View에서 액션을 전달하고, 상태가 변경될 때마다 뷰를 다시 그릴 수 있는 것입니다.

오늘은 글이 많이 짧지만, 다음에는 TCA를 통한 예제를 가지고 오려고 합니다. 기대해주세욧!!