TCA BindingAction 사용 방법 (SwiftUI / Composable Architecture)
TCA(Composable Architecture)를 사용하다 보면 TextField, Toggle, Picker 같은 SwiftUI 입력 컴포넌트를 어떻게 state와 연결해야 하는지 자주 고민하게 된다.
일반 SwiftUI에서는 @State나 @Binding으로 간단히 처리할 수 있지만, TCA에서는 상태 변경도 모두 action을 통해 흘러가야 하므로 입력 바인딩을 조금 더 구조적으로 다룬다.
이때 핵심이 되는 개념이 바로 BindingAction이다. BindingAction은 사용자의 입력으로 발생하는 단순한 상태 변경을 TCA 액션 흐름 안으로 자연스럽게 가져오기 위한 도구다.
이 글에서는 TCA에서 BindingAction이 왜 필요한지, 어떻게 선언하고 사용하는지, 실무에서는 어떤 구조로 설계하면 좋은지 정리한다.
BindingAction이 왜 필요한가
예를 들어 로그인 화면에 아이디와 비밀번호 입력창이 있다고 해보자. 사용자가 글자를 입력할 때마다 state가 바뀌어야 하는데, 이걸 전부 일반 액션으로 직접 만들면 코드가 금방 길어진다.
enum Action {
case usernameChanged(String)
case passwordChanged(String)
}
입력 필드가 2개면 괜찮아 보이지만, 설정 화면처럼 Toggle, TextField, Picker가 여러 개 생기면 이런 단순 입력 액션이 너무 많아질 수 있다.
그래서 TCA는 단순 바인딩 변경을 위한 전용 도구를 제공한다. 공식 바인딩 문서도 SwiftUI 바인딩을 store와 연결하기 위한 여러 도구를 설명하고 있다. :contentReference[oaicite:1]{index=1}
핵심 구성 요소
TCA에서 바인딩을 다룰 때 자주 같이 쓰는 구성 요소는 다음과 같다.
@BindingStateBindableActionBindingAction<State>BindingReducer()
이 네 가지가 같이 움직인다고 보면 이해가 쉽다.
기본 예제: 로그인 화면
가장 기본적인 예제로 로그인 화면을 만들어보자.
import ComposableArchitecture
@Reducer
struct LoginFeature {
@ObservableState
struct State: Equatable {
@BindingState var username = ""
@BindingState var password = ""
}
enum Action: BindableAction {
case binding(BindingAction<State>)
case loginButtonTapped
}
var body: some ReducerOf<Self> {
BindingReducer()
Reduce { state, action in
switch action {
case .binding:
return .none
case .loginButtonTapped:
return .none
}
}
}
}
이 구조가 TCA 바인딩의 기본 형태다. 공식 문서의 BindableAction와 BindingReducer 문서도 바로 이런 방식으로 단순한 상태 변경을 처리하도록 설명한다. :contentReference[oaicite:2]{index=2}
@BindingState의 역할
@BindingState는 “이 값은 뷰 바인딩으로부터 변경될 수 있다”는 의미를 가진다. 즉, 일반 state 프로퍼티와 달리 TextField, Toggle 같은 입력 컴포넌트와 직접 연결될 준비가 된 값이라고 보면 된다.
@BindingState var username = ""
@BindingState var password = ""
이렇게 표시해두면 바인딩 액션을 통해 안전하게 변경할 수 있다.
Action에 BindableAction 추가하기
바인딩을 지원하려면 Action이 BindableAction을 채택해야 한다. 그리고 보통 아래처럼 binding(BindingAction<State>) 케이스를 추가한다.
enum Action: BindableAction {
case binding(BindingAction<State>)
case loginButtonTapped
}
이 부분이 실제로 BindingAction이 Feature 안으로 들어오는 입구다. 사용자의 입력이 들어오면 TCA는 이 binding 액션을 통해 state를 갱신한다. :contentReference[oaicite:3]{index=3}
BindingReducer의 역할
BindingReducer()는 binding 액션이 들어왔을 때 실제로 @BindingState 값을 업데이트해 주는 reducer다.
var body: some ReducerOf<Self> {
BindingReducer()
Reduce { state, action in
switch action {
case .binding:
return .none
case .loginButtonTapped:
return .none
}
}
}
공식 문서에서도 BindingReducer는 바인딩 액션을 받았을 때 bindable state를 업데이트하는 reducer라고 설명한다. :contentReference[oaicite:4]{index=4}
SwiftUI View에서 연결하는 방법
이제 View에서 TextField와 Toggle을 store에 연결해보자.
import SwiftUI
import ComposableArchitecture
struct LoginView: View {
@Bindable var store: StoreOf<LoginFeature>
var body: some View {
Form {
TextField("아이디", text: $store.username)
SecureField("비밀번호", text: $store.password)
Button("로그인") {
store.send(.loginButtonTapped)
}
}
}
}
최근 TCA의 observation 기반 흐름에서는 이렇게 @Bindable var store와 $store.username 같은 형태가 자연스럽다. migration 문서도 최근 버전에서 바인딩 관련 사용 방식이 더 단순해졌다고 설명한다. :contentReference[oaicite:5]{index=5}
Toggle과 Picker도 같은 방식으로 연결 가능
BindingAction은 TextField만이 아니라 Toggle, Stepper, Picker처럼 값 변경이 필요한 모든 SwiftUI 입력 요소에 비슷하게 적용할 수 있다.
@Reducer
struct SettingsFeature {
@ObservableState
struct State: Equatable {
@BindingState var isNotificationEnabled = false
@BindingState var selectedTab = 0
}
enum Action: BindableAction {
case binding(BindingAction<State>)
case saveButtonTapped
}
var body: some ReducerOf<Self> {
BindingReducer()
Reduce { state, action in
switch action {
case .binding:
return .none
case .saveButtonTapped:
return .none
}
}
}
}
struct SettingsView: View {
@Bindable var store: StoreOf<SettingsFeature>
var body: some View {
Form {
Toggle("알림 받기", isOn: $store.isNotificationEnabled)
Picker("탭 선택", selection: $store.selectedTab) {
Text("홈").tag(0)
Text("기록").tag(1)
Text("설정").tag(2)
}
Button("저장") {
store.send(.saveButtonTapped)
}
}
}
}
이렇게 하면 단순 입력 변경은 BindingReducer가 처리하고, 저장 같은 명시적인 사용자 액션만 별도 액션으로 다루게 된다.
BindingAction을 쓰면 좋은 점
- 입력 필드마다 별도의 changed 액션을 만들지 않아도 된다.
- 폼 화면 코드가 훨씬 간결해진다.
- 단순 상태 변경과 비즈니스 로직 액션을 구분할 수 있다.
- 입력값이 많아도 reducer 구조가 비교적 깔끔하게 유지된다.
즉, BindingAction은 “단순 값 변경”을 다루는 데 최적화된 패턴이다.
언제 BindingAction을 쓰고, 언제 일반 Action을 쓸까
이 기준을 잡아두면 reducer 설계가 훨씬 쉬워진다.
BindingAction이 잘 맞는 경우
- TextField 입력값 변경
- Toggle on/off 변경
- Picker 선택값 변경
- 간단한 폼 상태 변경
일반 Action이 더 맞는 경우
- 로그인 버튼 탭
- 저장 요청
- 서버 호출 시작
- 화면 이동 요청
- 입력값 검증 후 추가 로직 실행
즉, 단순 값 변경은 BindingAction, 의미 있는 이벤트는 일반 Action으로 나누는 것이 가장 깔끔하다.
Binding 액션 이후 추가 로직이 필요할 때
사용자가 값을 입력한 뒤 추가 검증이나 상태 계산이 필요할 때가 있다. 예를 들어 username이 비어 있는지에 따라 버튼 활성화 상태를 바꾸고 싶을 수 있다.
이럴 때는 .binding 액션 분기에서 추가 로직을 넣거나, 최근 reducer 도구의 onChange 패턴을 활용하는 방식이 가능하다. 공식 문서에는 바인딩과 reducer 조합 도구들이 소개되어 있다. :contentReference[oaicite:6]{index=6}
@Reducer
struct SignupFeature {
@ObservableState
struct State: Equatable {
@BindingState var email = ""
var isSubmitEnabled = false
}
enum Action: BindableAction {
case binding(BindingAction<State>)
case submitButtonTapped
}
var body: some ReducerOf<Self> {
BindingReducer()
Reduce { state, action in
switch action {
case .binding:
state.isSubmitEnabled = !state.email.isEmpty
return .none
case .submitButtonTapped:
return .none
}
}
}
}
이 구조는 입력값이 바뀔 때마다 파생 상태를 계산해야 하는 폼 화면에서 자주 사용된다.
예전 방식과 최근 방식의 차이
TCA를 오래 찾아보면 예전 예제에는 ViewStore.binding(get:send:) 같은 방식이나 BindingViewStore가 자주 나온다. 반면 최근 문서에서는 observation 기반 API와 보다 간결한 바인딩 흐름이 강조된다. :contentReference[oaicite:7]{index=7}
즉, 오래된 글을 보면 바인딩 코드가 지금과 조금 다를 수 있다. 하지만 핵심 개념은 같다.
- 상태는 직접 수정하지 않는다
- 바인딩 변경도 action 흐름 안으로 넣는다
- BindingReducer가 단순 변경을 처리한다
실무에서 자주 쓰는 패턴
BindingAction은 특히 다음 화면에서 자주 쓴다.
- 로그인 / 회원가입 폼
- 설정 화면
- 검색 필터 화면
- 프로필 수정 화면
- 할 일 편집 화면
입력 컴포넌트가 많은 화면일수록 BindingAction의 장점이 더 커진다.
자주 하는 실수
1. @BindingState 없이 일반 state를 바로 바인딩하려는 경우
TCA 바인딩 패턴을 쓰려면 바인딩 대상 값에 @BindingState를 붙이는 것이 기본이다.
2. Action이 BindableAction을 채택하지 않는 경우
binding(BindingAction<State>) 케이스만 추가하고 BindableAction 채택을 빼먹으면 구조가 어색해질 수 있다.
3. BindingReducer를 넣지 않는 경우
BindingAction이 들어와도 실제 상태 업데이트를 처리할 reducer가 없으면 기대대로 동작하지 않는다.
4. 모든 액션을 binding으로 처리하려는 경우
로그인 버튼 탭, 저장 요청, 화면 이동 같은 의미 있는 이벤트는 여전히 일반 Action으로 두는 것이 좋다.
Reducer 구조 관점에서 보면
BindingAction은 reducer를 더 깔끔하게 만들기 위한 도구이기도 하다. 입력값 변경을 전부 수동 액션으로 나누지 않아도 되기 때문에, reducer는 정말 중요한 이벤트와 비즈니스 로직에 더 집중할 수 있다.
특히 폼 화면에서는 다음 구조가 가장 무난하다.
@BindingState로 입력 상태 선언BindableAction+binding(BindingAction<State>)선언BindingReducer()추가- 저장 / 제출 / 검증은 일반 Action으로 처리
정리
- BindingAction은 TCA에서 단순한 입력 변경을 action 흐름으로 처리하기 위한 도구다.
@BindingState,BindableAction,BindingReducer()와 함께 사용하는 경우가 많다.- TextField, Toggle, Picker 같은 SwiftUI 입력 컴포넌트와 잘 어울린다.
- 단순 값 변경은 BindingAction으로, 의미 있는 이벤트는 일반 Action으로 나누는 것이 좋다.
- 입력 필드가 많은 폼 화면일수록 reducer 구조를 더 깔끔하게 유지할 수 있다.
TCA (Composable Architecture) 관련 글
- TCA (Composable Architecture) 개념 정리 (Swift / iOS)
- TCA Feature 모듈 구조 설계 방법 (SwiftUI / Composable Architecture)
- TCA Reducer 구조 설계 방법 (SwiftUI / Composable Architecture)
- TCA Store scope 사용 방법 (SwiftUI / Composable Architecture)
- TCA Dependency 사용 방법 (SwiftUI / Composable Architecture)
- TCA Async / Effect 사용 방법 (SwiftUI / Composable Architecture)
- TCA BindingAction 사용 방법 (SwiftUI / Composable Architecture)
- TCA 테스트 코드 작성 방법 (SwiftUI / Composable Architecture)
- TCA NavigationStack 구조 구현 방법 (SwiftUI / Composable Architecture)
- TCA StackState 사용 방법 정리 (SwiftUI / Composable Architecture)
- TCA NavigationStackStore 사용 방법 (SwiftUI / Composable Architecture)
- TCA Delegate 패턴 화면 이동 구현 방법 (SwiftUI / Composable Architecture)
- TCA TabView 구조 설계 방법 (SwiftUI / Composable Architecture)
- TCA Optional State Navigation 패턴 정리 (SwiftUI / Composable Architecture)
'IT' 카테고리의 다른 글
| TCA Optional State Navigation 패턴 정리 (SwiftUI / Composable Architecture) (0) | 2026.03.15 |
|---|---|
| TCA NavigationStackStore 사용 방법 (SwiftUI / Composable Architecture) (0) | 2026.03.15 |
| TCA Async / Effect 사용 방법 (SwiftUI / Composable Architecture) (0) | 2026.03.15 |
| TCA Reducer 구조 설계 방법 (SwiftUI / Composable Architecture) (0) | 2026.03.15 |
| TCA Store scope 사용 방법 (SwiftUI / Composable Architecture) (0) | 2026.03.15 |