TCA

TCA - pullback

GREEN.1229 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