본문 바로가기
iOS Dev/TCA

[TCA]@BindingState를 쓰다가 자꾸 값이 초기화 되는 문제..(feat: TextField)

by Lark.q 2025. 2. 14.
반응형

 

 

TCA에서 State에 선언한 값을 @BindingState 로 사용하게 되면, 실시간으로 값을 받게 될 수 있다. 보통 UI에서 값을 store로 바로바로 직접 전달 받아야할때 사용한다.  보통 기본적으로는 아래처럼 사용하게 된다. 

//Feature
 struct State: Equatable {
     @BindingState var birth: String = "" // ✅ TextField와 바인딩할 값
 }
 
 
 enum Action: BindableAction, Equatable {
    case binding(BindingAction<State>)  // ✅ 자동 상태 업데이트를 위한 바인딩 액션
    case confirm
}

 var body: some ReducerOf<Self> {
    BindingReducer()  // ✅ 자동 바인딩 적용
    Reduce { state, action in
        switch action {
        case .binding:
            return .none  // 별도의 상태 업데이트 필요 없음! 자동으로 업데이트됨.
        case .confirm:
            print("입력된 생년월일: \(state.birth)")
            return .none
        }
    }
}


//UI
TextField("어쩌구", text: viewStore.$birth) ✅ UI에서 사용.

 

 

그런데 한화면에 TextField를 여러개 사용하고 키패트 타입(number,defult)도 각각 다르다보니, 다른 텍스트필드로 이동할때마다 자꾸 값들이 유지되지 않고 초기화 되는 것이다 ;..

 

처음에는 텍스트필드에 .onChange를 활용해서 액션으로 넘겨줬는데...(아래 코드)

 

.onChange(of: viewStore.birth) { text in

     viewStore.send(.onChangeTextField(SignUpFeature.filedType.birth, text))

}

 

//Feature의 Action부분

            case let .onChangeTextField(type, text):

                switch type {

                  case .birth: 

                  case .time:

                 case .region: ...

 

근데 타입도 다르게 잘들어오고 그러는데도 값이 자꾸 휘발되었다.. 왜지 ?... state.birth = text했는데 다른 텍스트필드를 탭하거나 키보드가 종료되면 그냥 갑자기 ""으로 다시 초기화 되는 것이다. TCA특성에서 자주 보이는 현상이라고 한다. 

 

 

🔥 원인 분석 (왜 값이 사라지는가?)

TCA에서는 State가 불변(immutable)하게 관리된다. 즉, 상태가 바뀌면 새로운 상태 객체가 생성되고 View가 다시 그려지는 방식이야. 그래서 여러 개의 TextField를 사용할 때 새로운 상태가 적용될 때 기존 값이 날아가는 현상이 발생할 수 있다고 한다.

 

 

🚨 원인 1: State 전체가 변경될 때 기존 값이 덮어씌워짐

TCA에서는 새로운 값이 들어오면 기존 State를 복사하여 업데이트하는데, Reduce에서 특정 필드만 업데이트하지 않고 전체 State를 덮어쓰는 방식으로 업데이트하면 값이 초기화될 수 있음.

 

그래서 내가 썼던 아래의 case는 

            case let .onChangeTextField(type, text): state.birth = text  
 

정상적으로 동작해야하지만, Reducer에서 텍스트 필드가 새 상태 값을 만드는 바람에 자꾸 birth값이 초기화됨. 

 

UI에서

WithViewStore는 Store에서 State를 구독하고 변경이 있을때마다 UI를 업데이트 하는 역할을 하는데, $0는 전체상태를 구독하기 때문

    WithViewStore(self.store, observe: { $0 }) { viewStore in

이런 식으로..

그래서, 그럼 전체를 업데이트 하지말고 각각 분리해서 구독하자(?) 라고도 할 수 있는 게 아래의 방법..

 

 

🚨 원인 2: WithViewStore의 observe 범위 문제

WithViewStore에서 observe할 때, State 전체를 감지하고 있으면 불필요한 리렌더링이 발생하여 

TextField가 다시 생성되면서 값이 초기화될 수도 있어서.. observe 범위를 해당 필드만 감시하도록 설정할 수도 있다. 

 

WithViewStore(self.store, observe: { $0.birth }) { viewStore in // ✅ birth만 감시
TextField("생년월일", text: viewStore.binding(\.$birth)) }
 
 
WithViewStore(self.store, observe: { $0.time }) { viewStore in // ✅ time만 감시
TextField("태어난 시", text: viewStore.binding(\.$time)) }

 

 

하지만, 나는 정말 가독성이 중요한 사람이라.. 위 방법은 싫었고, 텍스트 필드 외에도 내가 state해야 할 값들이 많아서..

이 방법은 패스.. 왜 안되는 지 이해용으로는 좋았다..

 

 

🚨 원인 3: viewStore.binding(\.$birth)이 아닌 viewStore.birth를 직접 수정할 경우

나는 직접 바인딩을 해왔는데, binding을 활용하라는 방식이다. 그런데 저렇게 해도 여전히 초기화됨..

TextField("생년월일", text: $viewStore.birth) // 🔴 직접 바인딩 X
 
 
TextField("생년월일", text: viewStore.binding(\.$birth)) // ✅ TCA 방식으로 바인딩
 
 
 
 
 

 

🔧 id 이용 ...

 

//Feature
    enum Action:BindableAction, Equatable {
        case binding(BindingAction<State>)
	}

//UI
TextField("19920530",text: viewStore.binding(get: { $0.birth },send: { SignUpFeature.Action.binding(.set(\.$birth, $0)) }))
.id("birth-\(viewStore.birth)")

TextField("17:30",text: viewStore.binding(get: { $0.time },send: { SignUpFeature.Action.binding(.set(\.$time, $0)) }))
.id("time-\(viewStore.time)")

 

TextField 각각에 바인딩을 정의한 것과 맞춰 연결해주고, .id를 통해 고유하게 관리되도록 하였다. 

그런데 이렇게 하니까 한글자 넣을때마다 키보드 종료되고 값도 사라짐 ㅋㅋㅋㅎㅎㅎㅎ.... 

 

 

 

 

🔧 그래서 어떻게 해결 했나...

 

 

결론적으로는, UI에서 State로 text들을 .onChange로 관리하고,  확인 버튼 눌렀을 때의 최종 값을 store의 방식으로 하는 게 속편하다.. 

생각해 보면 왜 한글자 한글자 관리하는 걸 굳이 store에서 하려고 했을까..??? 근본적인 물음.. 물론 그게 아키텍처상 깔끔했겠지만 복잡도도 증가하고 애초에 TextField특성을 고려했을 때는? 잘 맞지 않은 방식같았다. 

UI변경이 매번 일어 나는 건 그냥 UI에서 @State로 해결하는 게 더 단순화 된 방법 일지도... 생각을 하게되었고...

 

 

//UI

    let store: StoreOf<SignUpFeature>

    @State var birth: String = ""

    @State var time: String = ""

    @State var region: String = ""

 

 

결국 이런식으로 처리하게 되었다는 결론.. 아주 깔끔..

Action부분에서 .onChangeTextField 도 없앰..

 

반응형