TCA - pullback
안녕하세요. 그린입니다🍏
이번 포스팅부터 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
[참고자료]
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