ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • TCA - pullback
    TCA 2022. 9. 19. 10:59

    안녕하세요. 그린입니다🍏

    이번 포스팅부터 TCA에 대해 조금씩 학습하고 공유하려합니다🙌

    TCA의 기본 개념인 State, Action, Reducer를 먼저 할 수도 있겠지만 그냥 제가 포스팅 하고 싶은 순으로 할 예정이라

    차근 차근 TCA를 학습해 나가는데는 순서가 다를 수 있습니다..!

    오늘은 첫번째 주제로 pullback이라는 메서드에 대해 알아볼께요!

     

    TCA?

    우선 TCA는 The Composable Architecture의 약자입니다.

    어떤 아키텍쳐인지 자세한 소개는 아래 포스팅을 참고해주세요!

    먼저 보고오면 좋습니다🙌

    https://green1229.tistory.com/138?category=936861 

     

    Composable Architecture

    안녕하세요. 그린입니다🟢 이번 포스팅에서는 Composable Architecture에 대해 학습해보겠습니다🧑🏻‍💻 왜 알아보게 되었는지? 앞으로는 SwiftUI와 사용자 이벤트를 통한 뷰의 업데이트 등 상태 값

    green1229.tistory.com

     

    pullback의 필요성

    위 TCA를 사용해보시는 분이라면 초반에 이런 고민을 하실거에요.

    하위 코어에서의 액션을 상위 코어에서 어떻게 컨트롤 해주거나 영향을 받아 상태 값을 변경할 수 있을까?

    이럴때 사용되는 메서드가 pullback이라는 메서드 입니다.

    그럼 어떻게 이게 가능한것인지 어떤 원리인지 어떻게 사용하는지 아래에서 천천히 알아보시죠.

     

    pullback이란?

    pullback의 정의는 이렇습니다.

    @available(
        *,
        deprecated,
        message: "'pullback' no longer takes a 'breakpointOnNil' argument"
      )
      public func pullback<GlobalState, GlobalAction, GlobalEnvironment>(
        state toLocalState: CasePath<GlobalState, State>,
        action toLocalAction: CasePath<GlobalAction, Action>,
        environment toLocalEnvironment: @escaping (GlobalEnvironment) -> Environment,
        breakpointOnNil: Bool,
        file: StaticString = #fileID,
        line: UInt = #line
      ) -> Reducer<GlobalState, GlobalAction, GlobalEnvironment>

    보시면 state, action, environment를 기본적으로 인자로 받아야합니다.

    breakpointOnNil은 이제 위 @available 프로퍼티 래퍼를 보면 알겠지만 더 이상 사용하지 않습니다.

    그렇기에 무시되어도 상관 없습니다.

    file과 line도 인자로 받을 수 있지만 실제적으로 사용되는 경우가 없음으로 자세한 설명은 생략하겠습니다.

    간단히 디버그 시 로그를 찍어줄때 정보 역할을 합니다.

    그렇다면 남는건 결국 state, action, environment입니다.

     

    그렇기에 pullback은 이러한 오버로딩을 가지고 있습니다.

    public func pullback<GlobalState, GlobalAction, GlobalEnvironment>(
        state toLocalState: WritableKeyPath<GlobalState, State>,
        action toLocalAction: CasePath<GlobalAction, Action>,
        environment toLocalEnvironment: @escaping (GlobalEnvironment) -> Environment
      ) -> Reducer<GlobalState, GlobalAction, GlobalEnvironment> {
        .init { globalState, globalAction, globalEnvironment in
          guard let localAction = toLocalAction.extract(from: globalAction) else { return .none }
          return self.reducer(
            &globalState[keyPath: toLocalState],
            localAction,
            toLocalEnvironment(globalEnvironment)
          )
          .map(toLocalAction.embed)
        }
      }

    이 세 친구들에 어떤 값을 줘야할까요?

    우선 이 pullback의 정의를 보면 이 세 친구를 기본적으로 받아 상위 리듀서로 끌어들여 역할을 수행하게 해줍니다.

    즉 우리가 원하는 하위 코어의 액션을 상위 코어의 액션으로 관심사를 가져와 state 상태 변경 및 상위의 다른 액션으로 매칭 및 사이드 이펙트 처리 등 다양하게 해줄 수 있는거죠.

     

    그럼 pullback을 사용해볼 환경부터 구축해보고 사용해볼까요?

     

    pullback 사용 환경 구축

    우선 상위/하위 뷰 및 코어가 있어야 겠죠?

    빠르게 간단한 프로젝트 하나를 생성해 시작해볼께요.

    빠르게 빠르게 해야하니 SPM으로 TCA(The Composable Architecture)부터 추가합니다.

    아..! 그전에 어떤걸 상/하위 관련시켜 구동 시킬건지 설명부터 하는게 나을것 같네요.

    보시면 제일 큰 폰트인 "Is Changed Sub Title?" 로 된 부분이 main 즉 상위 뷰/코어 영역입니다.

    그리고 밑에 작은 폰트와 버튼은 sub 즉 하위 뷰/코어 영역입니다.

    편의 상 한 씬에서 구성했습니다.

    (네비게이션을 사용해 다른 씬에서 사용해도 결과는 같아요!)

    하위의 버튼을 클릭 했을 시 하위의 title을 변경시키고 이 하위의 액션을 상위에서 감지하여 상위의 title도 변경시킬거에요.

    즉 우리가 하고 싶은 계층 간 액션 전달이 가능해지죠!

     

    그럼 우선 subView/Core, 하위 뷰/코어부터 건들겠습니다.

    왜냐면 상위에서 이 뷰/코어를 얹어줘야 하니까요!

     

    SubView

    // SubView (하위 뷰)
    
    import ComposableArchitecture
    import SwiftUI
    
    struct SubView: View {
      var store: Store<SubState, SubAction>
      
      var body: some View {
        WithViewStore(store) { viewStore in
          VStack(spacing: 20) {
            Text(viewStore.subTitle)
              .font(.body)
            
            Button(
              action: { viewStore.send(.changeTitleButtonTapped) },
              label: { Text("Click for Change title") }
            )
          }
        }
      }
    }

    아주 간단합니다.

    텍스트와 사용자 액션을 받아 줄 버튼으로 구성됩니다.

     

    SubCore

    // Sub Core (하위 코어)
    
    import ComposableArchitecture
    
    struct SubState: Equatable {
      var subTitle: String = "What's the Sub Title"
    }
    
    enum SubAction {
      case changeTitleButtonTapped
    }
    
    struct SubEnvironment { }
    
    let subReducer = Reducer<SubState, SubAction, SubEnvironment> { state, action, env in
      switch action {
      case .changeTitleButtonTapped:
        state.subTitle = "Tapped Sub title Button"
        return .none
      }
    }

    이것도 아직 간단해요.

    TCA 써보셨다면 익숙하실 겁니다.

    State에는 title의 상태 값을 넣어줍니다.

    action은 만들어진 버튼이 탭되었을때 케이스를 줘요.

    사이드 이펙트 및 별도 처리는 없으니 넣어줄게 없습니다.

    그리고 마지막으로 리듀서를 만들어 줍니다.

     

    이제 Main, 상위 구현으로 넘어가시죠!

     

    MainView

    // MainView (상위 뷰)
    
    import ComposableArchitecture
    import SwiftUI
    
    struct MainView: View {
      var store: Store<MainState, MainAction>
      
      var body: some View {
        WithViewStore(store) { viewStore in
          VStack(spacing: 30) {
            Text(viewStore.mainTitle)
              .font(.title)
            
            SubView(
              store: store.scope(
                state: \.subState,
                action: MainAction.subAction
              )
            )
          }
        }
      }
    }

    상위 뷰를 보시죠.

    우선 상위에서도 title 하나가 존재해요.

    저 title은 하위에서 버튼이 클릭되면 변경할거에요.

    그리고 SubView를 얹어줍니다 끝!

     

    MainCore

    // MainCore (상위 코어)
    
    import ComposableArchitecture
    
    struct MainState: Equatable {
      var mainTitle: String = "Is Changed Sub Title?"
      var subState: SubState = .init()
    }
    
    enum MainAction {
      case subAction(SubAction)
    }
    
    struct MainEnvironment { }
    
    let mainReducer = Reducer.combine([
      subReducer.pullback(
        state: \.subState,
        action: /MainAction.subAction,
        environment: { _ in
          SubEnvironment()
        }
      ) as Reducer<MainState, MainAction, MainEnvironment>,
      Reducer<MainState, MainAction, MainEnvironment> { state, action, env in
        switch action {
        case .subAction(.changeTitleButtonTapped):
          state.mainTitle = "Changed Sub title!"
          return .none
          
        case .subAction:
          return .none
        }
      }
    ])

    pullback을 사용하여 하위의 액션 및 state를 다루기 위해선 상위에서 각 state와 action에서 값을 갖고 있어야합니다.

    위와 같은 방식으로 SubState, SubAction 타입을 state, action 정의에서 다뤄줍니다.

    그 다음 리듀서에서 이제 중요한 오늘의 주제인 pullback이 나왔어요.

    즉 해당 main리듀서는 리듀서가 두개 속하는거에요.

    하나는 하위 리듀서 그리고 자신의 상위 그대로의 리듀서!

    그래서 리듀서를 combine 시키죠?

    그 안에 하위 리듀서를 호출해 pullback으로 끌어들입니다.

    각 state, action, env에 상위에서 만든 state, action을 매칭 시킵니다.

    state는 키패스로 action은 casePath로 가져와요.

    casePath가 생소하다면 아래 포스팅을 참고해주세요!

    https://green1229.tistory.com/233

     

    CasePath

    안녕하세요. 그린입니다🟢 이번 포스팅에서는 CasePath에 대해 학습해보겠습니다🙌 저는 주로 요새 Composable Architecture(TCA)를 사용하는데 타 reducer를 pullback 받아올때 keypath와는 다른 casepath를 볼..

    green1229.tistory.com

    그 다음 메인 리듀서에서 하위 액션 타입의 액션을 만들었잖아요?

    그 액션의 기능을 구현해주면 됩니다.

    subAction은 하위 액션 전체이기에 하위 액션에서 특정한 한 액션 케이스를 다룰 경우 위처럼 구현 해주면 됩니다.

    하위의 changeTitleButtonTapped 액션 외 다른 액션들의 경우 상위에서 아무런 기능을 해주지 않을 경우 모두 묶어 return .none 처리 해주면 됩니다.

    위 처럼 말이죠🙌

     

    아주 아주 쉽게 pullback을 자유자재로 사용하여 뷰 및 코어 간 구성을 해줄 수 있습니다!
    TCA를 사용함에 있어 없어서는 안될 메서드이니 꼭 익혀야 되고 익혀질 수 밖에 없을거에요👋

     

    마무리

    TCA 너무 기계적으로 사용하고 있어서 간단한것도 다시 포스팅 하려니 생각보다 글 솜씨가 없네요ㅠㅠ

    TCA 자료가 너무 없어서 정리해야지 정리해야지 했는데 이번에 하나씩 해보려 합니다!

    근데 TCA를 사용하는 분들이 적을것 같긴 하지만요..🤪

    위 연습용 프로젝트 코드들은 아래 제 깃헙에 있으니 참고하셔도 좋습니다ㅎㅎ

    https://github.com/GREENOVER/playground/tree/main/pullback

     

    [참고자료]

    https://pointfreeco.github.io/swift-composable-architecture/Reducer/#reducer.pullback(state:action:environment:breakpointonnil:file:line:) 

     

    ComposableArchitecture - Reducer

    public func signpost( _ prefix: String = "", log: OSLog = OSLog( subsystem: "co.pointfree.composable-architecture", category: "Reducer Instrumentation" ) ) -> Self Instruments the reducer with signposts. Each invocation of the reducer will be measured by a

    pointfreeco.github.io

    'TCA' 카테고리의 다른 글

    TCA - Debugging  (2) 2022.10.06
    TCA - Scope  (7) 2022.09.26
    CasePath  (0) 2022.04.06
    Composable Architecture로 랜덤 통신 구현하기  (0) 2021.07.17
    Composable Architecture  (5) 2021.06.11
Designed by Tistory.