-
TCA 1.0 - TCA Binding (ch.04)TCA 2024. 2. 12. 09:32
안녕하세요. 그린입니다 🍏
이번 포스팅에서는 TCA의 기본개념들을 넘어 또 중요한 파트인 TCA Binding에 대해 알아보겠습니다 🙋🏻
항상 포스팅에서도 소개했지만, TCA 1.0 시리즈 학습은 아래 학습자료를 기반으로 하고 있습니다.
해당 레퍼를 기반으로 학습하면서 제 나름대로 정리해보는 포스팅이기에, 주관적인 사견이 추가됩니다 🙋🏻
그럼 바로 알아보시죠 🚀
TCA Binding
SwiftUI Binding vs TCA Binding
- SwiftUI에선 @State, @Binding, @ObservedObject들을 통해 양방향 바인딩 구현 및 상태관리를 함
- SWiftUI에서 상태 관리가 복잡해질수록 사이드 이펙트 관리에 어려움을 겪을 수 있음
- 그렇기에 TCA의 Binding은 State 관리 바인딩 도구들을 제공하여 단방향 데이터 흐름 원칙을 지킴
- 그로 인해, 사이드 이펙트 관리 및 복잡한 상태들을 일관되게 처리할 수 있음
- 간단한 뷰하고 로직을 짜는데 여러 코드가 들어가야하는것은 TCA를 사용한다면 어쩔수 없긴하다고 생각함
TCA Binding
- 가장 기본적으로 Binding(get:send:)의 형태로 TCA는 바인딩을 함
- get: State를 바인딩 값으로 변환
- send: 바인딩 값을 다시 Store에 피드백하는 Action으로 변환
- ReducerProtocol 이전에도 늘 사용하던 방식이라 익숙한 방식
// Core struct Main: Reducer { struct State: Equatable { var isOnAlarm: Bool = false } enum Action { case isOnAlarmChanged(Bool) } func reduce(into state: inout State, action: Action) -> Effect<Action> { switch action { case let .isOnAlarmChanged(isOn): state.isOnAlarm = isOn return .none } } } // View struct MainView: View { let store: StoreOf<Main> var body: some View { WithViewStore(self.store, observe: { $0 }) { viewStore in Toggle( "Alarm", isOn: viewStore.binding( get: \.isOnAlarm, send: { .isOnAlarmChanged($0) } ) ) } } }
- 이러한 형태로 구성이 됨
- Toggle 컴포넌트의 isOn 바인딩 파라미터에 바인딩 객체를 전달하는 형식
- 여기서 get을 통해서 SwiftUI 컴포넌트와 바인딩 통신을 하고 send를 통해서 Store 내부에 바인딩 값을 전달하여 상태 변경과 같은 비지니스 로직을 수행하게 됨
- 기존에는 이렇게만 썼었는데, 이것의 가장 큰 문제점이라기 보다는 귀찮은 점은 이런 바인딩되는 State가 많다면 코어에서 State, Action, Reducer에 모두 코드가 구현되어야함
- 즉, 반복 작업을 동반하게되고 리듀서가 점점 커져 관리가 복잡해질 수 있다는 점이 단점이라면 단점 🥹
다양한 TCA의 Binding tools
- 위 알아봤던 바인딩의 문제들을 해결하고자 TCA에선 다양한 바인딩 도구들을 제공하게됨
- BindingState, BindingAction, BindingReducer들이 이제 적극적으로 활용되는데, 이전에 안다뤄봤던 영역이라 자세히 알아볼 예정!
BindingState
struct Main: Reducer { struct State: Equatable { @BindingState var isOnAlarm = false } }
- 이와 같이 @BindingState 프로퍼티 래퍼를 붙여 해당 필드 값을 뷰의 UI 컴포넌트에서 바인딩 가능하도록 만들어줌
- 그럼 이제, 해당 값은 바인딩 값이 되었기에 바로 뷰에서 해당 필드 값의 조정이 가능한것!
- 다만, 외부에서 직접 변경이 당연히 가능한 형태이니 캡슐화가 잘 지켜지지 않기에 꼭 SwiftUI 기본 컴포넌트에 전달하기 위한 필드들에만 사용하는것을 권장
- 근데 이제 최신 버전에서는 매크로 등장으로 BindingState 프로퍼티 래퍼 안붙여도됨
@ObservableState struct State { var isOnAlarm = true }
- 이렇게 변경된걸로 쓰게 되면 뷰에서도 @Bindable var store: StoreOf<Feature> 처럼 사용
BindingAction
- Action enum에 BindableAction 프로토콜을 채택하여, State의 모든 필드 액션을 하나의 케이스로 병합함으로 깔끔하게 제공해줌
- 제네릭 타입을 갖는 BindingAction을 인자로 받음 -> BindingAction<State>
enum Action: BindableAction { case binding(BindingAction<State>) }
BindingReducer
- Binding Action이 들어올 때, State 업데이트 쳐주는 리듀서!
var body: some Reducer<State, Action> { BindingReducer() }
- 이것도 본 리듀서 전에 써줘야하는듯...!?
- 아래와 같이 구성한다면 이제 뷰에서 더 쉽고 직관적이게 코드를 가져갈 수 있음
Toggle( "Alarm is On", isOn: viewStore.$isOnAlarm )
- 아주아주 편하긴 함!!
- BindingReducer가 body 내부에서 작동하고 바인딩 액션이 수신되면 상태를 변경
- BindingReducer는 State와 Action 사이를 바인딩하는 역할을 가짐
- 즉, 바인딩 State 요소가 업데이트되면 BindingReducer가 해당 상태값과 액션을 수신하여 Reducer 클로저 내에서 도메인 로직을 처리해 결과를 State에 반영
- 추가로, 바인딩 시 더 다른 로직을 추가하고 싶다면 아래와 같이 binding 액션 케이스에서 구현해줄 수 있으
var body: some Reducer<State, Action> { BindingReducer() Reduce { state, action in switch action { case .binding(\.isOnAlarm): // 다른 작업 로직! case .binding(_): break } } }
Binding(get:send:) vs TCA Binding tools
- 두 바인딩 방식의 어떤 차이가 있을까?
- BindingState를 사용하면 뷰에서는 원래 익숙한 바닐라 SwiftUI 방식처럼 객체 바인딩을 할 수 있어서 편리함
- SwiftUI의 바인딩은 양방향으로 BindingState는 데이터 바인딩을 하며 내부 로직 자체는 단방향으로 동작
- BindingAction을 사용함으로 각 바인딩이 필요한 State 프로퍼티별로 액션을 기존처럼 전부 일일히 하나씩 수동으로 작업하지 않아도됨!
- 하나의 바인딩 케이스로 대체되는데 이게 너무너무 편해졌다 👍
- 이제는 단순 필드 값 바인딩을 위한 액션이 불필요하다~
- 또한 리듀서에서도 해당 액션을 그냥 생략해버려도됨
- BindingReducer도 BindingAction 프로토콜을 채택한 액션을 사용함으로써 훨씬 리듀서 자체도 라이트해짐
- 그냥 너무 편해진것 같은데..? 이것도 최신 버전에서는 또 다른 변화가 있나..?
- 최신 버전에서는 더 줄여서 표현되고 있으니.. 대체 어디까지 줄어들까.. 이러다 뷰만 남겠는데 다시 🤔
View State Binding
- 각 뷰들은 자신만의 View State를 가질 수 있음 (챕터 3의 내용)
- 스토어 외부에 있는 View State 바인딩을 위해선 ViewState 필드엔 @BindingViewState를 사용해야함
struct MainView: View { let store: StoreOf<Main> struct ViewState: Equatable { @BindingViewState var sendNotification: Bool } var body: some View { WithViewStore( self.store, observe: { bindingViewStore in ViewState( sendNotification: bindingViewStore.$sendNotification ) } ) } }
- 이렇게 사용된다고 하는데 음.. 왜 쓰지?
- 코어 단에서 하위 리듀서를 풀백 받아서 처리하는것으로도 될것 같은데 어떤 이점이 있을까!?
- TCACoordinator 라이브러리를 같이 쓴다면 뭔가 더더욱 쓸일이 없을것 같은데.. 아직 잘 모르겠다~ 🥲
- 아래와 같이 ViewState 이니셜라이저로 해놓는다면 더 편하게 사용할 수 있음
struct MainView: View { struct ViewState: Equatable { @BindingViewState var sendNotification: Bool init(bindingViewStore: BindingViewStore<Notification.State>) { self._sendNotification = bindingViewStore.$sendNotification } } var body: some View { WithViewStore(self.store, observe: ViewState.init) { viewStore in Toggle( "Send Notification", isOn: viewStore.$sendNotification ) } } }
- 스토어 외부에 있는 ViewState와 바인딩을 할 때 사용된다는건 알겠다!
소감
- 아직 명확히 이해가 안되는 부분들도 있지만, 확실하게 Binding이 너무너무 편해졌다 😀
- 기존에는 binding(get:set:)으로 귀찮고 누락되거나 헷갈려서 고통받는 나날들이 이제는 안녕
레퍼런스
'TCA' 카테고리의 다른 글
TCA 1.0 - Swift의 비동기 처리와 TCA에서의 응용 (ch.06) (81) 2024.02.20 TCA 1.0 - Dependency (ch.05) (67) 2024.02.15 TCA 1.0 - TCA의 기본 개념 (2) (ch.03) (85) 2024.02.08 TCA 1.0 - TCA의 기본 개념 (1) (ch.02) (96) 2024.02.05 TCA 1.0 - Hello, TCA (ch.01) (114) 2024.02.01