iOS/TCA

[TCA] 03. TCA의 바인딩 방식을 알아보자.

여성일 2026. 1. 13. 20:34
728x90

바인딩?

iOS 개발에서 바인딩이라는 말을 참 많이 쓰는 것 같다. UIKit이나 RxSwift로 개발할 때도 바인딩이라는 말을 참 많이 썼는데, 바인딩이 대체 뭘까? 간단히 짚고 넘어가보자.

 

iOS 개발에서 바인딩(Binding)이란? 말 그대로 데이터와 UI 묶어(Bind) 둘 사이를 동기화하는 것을 의미한다. 자세한 내용은 바인딩 글에서 다뤄보도록 하고, TCA에서의 바인딩 방식을 알아보자.

 

Binding(get:send:)

struct MyView: View {
  let store: Store<State, Action>
    
  var body: some View {
      WithViewStore(store) { viewStore in
          TextField(
              "Name",
              text: viewStore.binding(
                  get: \.name,
                  send: Action.nameChanged
              )
          )
      }
  }
}

가장 기본적으로, Binding(get:send:)의 형태로 바인딩 한다. SwiftUI 기본 컴포넌트의 바인딩 파라미터에 binding 객체를 전달하는 형식이다. 현재 TCA에서는 잘 사용하지 않는데, 모든 상태 변경마다 개별 액션을 만들어야하고, Getter와 Sender를 수동으로 관리하기 때문에 가독성도 떨어지기 때문이다. 문제가 있다기 보단, 귀찮은 점..? 보일러 플레이트의 이유가 가장 큰 것 같다. 

 

+ WithViewStore, ViewStore가 deprecated 되면서 Binding(get:send:) 방식은 거의 사용하지 않는다.

 

BindingState + BindingAction + BindingReducer

BindingState

struct State: Equatable {
  @BindingState var name: String = ""
  @BindingState var age: Int = 0
}

State의 특정 프로퍼티가 View에서 바인딩으로 변경될 수 있음을 알리는 프로퍼티이다. 음.. 쉽게 말하면 "이 프로퍼티는 바인딩이 가능한 프로퍼티입니다!"라고 점찍어주는 프로퍼티라고 생각하면 될 것 같다. (나 바인딩 가능해요!)

 

BindingAction

enum Action: BindableAction {
  case binding(BindingAction<State>)
  case login
  case logout
}

모든 바인딩 변경을 하나의 액션 타입으로 통합하기 위한 프로토콜이다. Action enum에 BindingAction 프로토콜을 채택하여, State의 액션을 하나의 액션 타입으로 통합한다.

 

BindingReducer

@Reducer
struct Feature {
  var body: some ReducerOf<Self> {
      BindingReducer()  
        
      Reduce { state, action in
          switch action {
          case .binding:
              return .none
                
          case .login:
              return .none
          }
      }
  }
}

BindingAction을 받아와 BindingState가 붙은 프로퍼티의 값을 실제로 변경해주는 리듀서이다. 예전엔 리듀서에 .binding을 붙였지만, 지금은 BindingReducer()를 리듀서 빌더 안에 명시적으로 넣어준다.

 

struct BindingReducer<State, Action>: Reducer {
    func reduce(state: inout State, action: Action) -> Effect<Action> {
        guard case let .binding(bindingAction) = action else {
            return .none
        }

        state[keyPath: bindingAction.keyPath] = bindingAction.value
        
        return .none
    }
}

내부적으로 BindingAction의 keyPath와 Value를 사용해서 실제로 state를 변경한다. 

 

struct TextFieldView: View {
  @Bindable var store: StoreOf<TextFieldFeature>
    
  var body: some View {
      VStack {
          TextField("Name", text: $store.name)
      }
  }
}

View에서는 이렇게 사용하면 된다!