같이 일하시는 분이 TCA가 핫하다고 하기도 하고, 아키텍처 공부하면서 관심이 생겼어서 조금씩 공부하고 있다.
막 파볼려고 공부한건 아니고 TCA가 어떤거고, 간단하게 샘플코드 짜면서 TCA에 대해 인지만 하려고 했으나..
구글링해서 나오는 TCA들을 토대로 코드를 작성하다보면 TCA 업데이트가 상당해서 코드에러를 굉장히 많이 봤다.
Deprecated된 코드들도 많고.. 그러다보니 궁금증도 생기고, 찾다보니 TCA공식문서에서도 튜토리얼도 굉장히 자세히 나와있고,
SwiftUI에 대해서도 공부가 필요하고, Swift언어도 공부가 필요하고.. TCA공식문서도 계속 찾다보니, 정리할 필요가 있겠다 싶었고,
또 나처럼 무작정 덤벼들었다가 시간낭비 안했으면 해서 글을 작성해본다.
TCA 공식 문서
문서가 굉장히 잘되어있다고 생각한다.
아래로 스크롤하다보면 튜토리얼도 있다.
TCA 튜토리얼
가볍게 TCA의 개념들을 맛보고,
개념자체는 쉬운데, 코드레벨로는 꽤 난이도가 있다고 생각하기 때문에 튜토리얼로 느낌을 파악하면 좋은것 같다.
모두 해보면 좋다고 생각한다. 시간도 굉장히 짧아서 금방 할수있다.
TCA를 이용하여 앱을 만든다면, 필수적인 지식들이 담긴 튜토리얼이라고 생각한다.
1 -1 은 Reducer를 만드는 방법을 배우고,
1-2는 Effect 타입을 처리하는 방법.
1-3 은 TCA를 Test코드 작성하는 방법
1-4는 Reducer와 Reducer를 결합하는 방법 1 (부모-자식)
2-1은 Reducer와 Reducer를 결합하는 방법 2 ( 옵셔널타입 )
2-2는 sheet, alert 여러개가 사용되는 경우 효과적으로 관리하는 방법
2-3 하위case는 배제하고, 상위레이어의 test하는 방법 (특정 케이스만 집중적으로 )
2-4 Navigation 구현하는 방법
TCA 마이그레이션 가이드
지원이 상당하여 버전업이 굉장히 많이 발생하는데, 그에 따른 마이그레이션 가이드도 있다. (현재 최신버전은 1.14 )
TCA 버전별 문서
나름 팁인데, 버전별로 문서가 나뉘어있다.
아래처럼 가장 {버전}에 적어주면 되는데, 최근버전이 main 이고, 1.10.0 을 넣어도되고, 0.59.0, 0.41.0 을 넣어도된다. 버전에 따라 문서가 바뀐다. 그래서 어떤버전에 기능이 추가되었는지등을 확인해볼 수 있다. (간혹 안나오는 버전도 있음)
https://pointfreeco.github.io/swift-composable-architecture/{버전}/documentation/composablearchitecture
https://pointfreeco.github.io/swift-composable-architecture/main/documentation/composablearchitecture
https://pointfreeco.github.io/swift-composable-architecture/0.59.0/documentation/composablearchitecture
TCA 추상적 개념
The Composable Architecture 의 약자로, TCA라고 부른다. 직역하면 구성가능한 아키텍처 ?
이젠 정석이 된듯한, 뷰와 뷰모델을 분리하는것을 중점으로 두고, 분리가 가능하도록 설계됐으며 특히 SwiftUI와 가장 부합한 아키텍처라고 생각한다. SwiftUI에 특화된 api도 많이 존재하고..
개념자체는 간단하다.
TCA는 State, Action, Reducer, Store로 구성된다.
State는 뷰에서 사용하는 상태, 변수라고 생각하면 된다.
Action은 말그대로 액션으로, State를 변경시키기 특정 로직을 수행하기 위한 트리거 액션 등을 담는다.
Reducer는 Action을 입력받아 비즈니스로직을 수행하는 곳이다. 여기서 State도 변경하기도 한다. 핵심이라고 볼수있다.
Store는 이제 SwiftUI의 View와 Reducer를 연결해주고 옵저빙을 도와주는 역할이라고 보면된다.
TCA 코드레벨 개념
Reducer
최신버전 기준으로,Reducer를 만들기위한 ReducerProtocol이란게 존재한다.
아까봤을때 개념에서는 State,Action, Reducer가 있는데, 코드상으로는 Reducer안에서 State, Action, 비즈니스로직을 셋다 가지고 있다.
ReducerProtocol을 채택하기 위해서는 State, Action , body를 구현하면 된다.
보통 Reducer를 Feature라고 부르는듯하다. 더 나아가서 리듀서(Feature)별로 모듈분리도 가능하다.
body는 액션을 받아서 비즈니스로직을 처리하는 부분이라고 보면된다.
간단하게 영화리스트를 가져오고 영화리스트 정보를 가지는 리듀서이다.
리듀서만 봐서도 어떤 역할을 하는지 분명하게 알 수 있다.
View에서는 State의 movie를 옵저빙하고, 리듀서에게 fetchMovie액션만 전달하면 된다.
그러면 리듀서의 body에서 액션을 처리하여 movie를 가져오게 된다.
실제로는 네트워크 통신이나 비동기통해서 movie를 넣어주겠지만... 지금은 코드느낌만 보면된다.
@Reducer
struct MovieListFeature {
struct State {
var movie: [Movie]
}
enum Action {
case fetchMovie
}
var body: some ReducerOf<Self> {
Reduce { state, action in
switch action {
case .fetchMovie:
state.movie = [Movie()]
return .none
}
}
}
}
struct Movie: Identifiable {
var id = UUID()
var name: String = ""
}
코드에서는 @Reducer매크로를 사용했다. ( @Reducer 매크로는 1.4.0 버전부터 지원하게 됐다. )
이 매크로를 통해서 ReducerProtocol을 채택하게 해주며, 더 나아가서는
아래처럼도 Reducer로 만들수 있다.
Navigation을 이용할경우 아래처럼 사용하는 듯 하다.
재밌는 점은 case별로 Reducer를 가지고 있어서, State, Action을 따로 만들어주지 않아도 컴파일단계에서 만들어준다.
@Reducer
enum Path {
case redFeature(RedFeature)
case blueFeature(BlueFeature)
case orangeFeature(OrangeFeature)
}
0.41 미만버전까지는 ReducerProtocol이 없었고, Reducer를 상수로 가졌었다.
느낌은 비슷하나, 상수로 가지는게 영 별로였다. 암튼 최근 버전 코드들이 이제좀 안정을 갖는것 같다.
// State
struct CounterState: Equatable {
var count: Int = 0
}
// Action
enum CounterAction: Equatable {
case increment
case decrement
}
// Environment
struct CounterEnvironment {
}
// Reducer
struct CounterReducer {
static let reducer = Reducer<CounterState, CounterAction, CounterEnvironment> { state, action, environment in
switch action {
case .increment:
state.count += 1
return .none
case .decrement:
state.count -= 1
return .none
}
}
}
Effect
여기서부터 슬슬 어려울 수 있다.
Reducer의 body안에 return .none이라는 코드가 있다. 원래는 Effect.none 이다.
비즈니스 로직수행후에 return 타입은 Effect타입이다.
비즈니스로직에는 네트워킹 같은 비동기 로직이 필수적일거다.
영화리스트도 마찬가지로 비동기적으로 영화를 가져와야한다.
하지만 저 구문안에서 async await 구문을 바로 사용할 수 없다.
비동기구문을 사용한다고 한들 그안에서 inout state의 상태를 변경할 수 없다.
또는 액션을 처리하는 과정에서 액션을 새로 전달해야하는 케이스도 있을것이다.
그런 경우를 위해서 Effect타입을 도입했다.
액션을 처리할때 비동기작업이 필요한경우, 비동기작업을 수행할수있도록 도와주며, 다시 Action으로 전달하게 끔하는 역할을 한다.
input state를 변경할 수 없으니, 새로 Action을 전달하여, 새로운 Action을 처리하는 비즈니스 로직을 추가하는것이다.
아래처럼, retunr Effect.run 을 이용하여 비동기 실행후, 새로운 movieResponse액션을 전달한다.
그리고 movieResponse을 처리하는 로직을 추가한다.
@Reducer
struct MovieListFeature {
struct State {
var movie: [Movie]
}
enum Action {
case fetchMovie
case movieResponse([Movie])
}
var body: some ReducerOf<Self> {
Reduce { state, action in
switch action {
case .fetchMovie:
return .run { send in
let (data, _) = try await URLSession.shared.data(from: URL(string: "movie")!)
let newMovie = [Movie()]
await send(.movieResponse(newMovie))
}
case .movieResponse(let newMovies):
state.movie = newMovies
return .none
}
}
}
}
send에 await을 사용해야하는 이유 ?
중요하진 않지만, 궁금해서...
send에는 왜 await을 붙여야하는지가 궁금했고,
.run의 클로져는 async로 되어있다는것을 알수있었고,
send는 struct Send 타입인데, 어떻게 메소드처럼 처리하는지 궁금했고,
Send에서 callAsFunction을 구현해서 메소드처럼 사용할수있다는걸 알게됐고,
Send는 @MainActor 여서 비동기적으로 처리되므로, await을 붙여야한다라는걸 알게됐다.
@MainActor
public struct Send<Action>: Sendable {
let send: @MainActor @Sendable (Action) -> Void
public init(send: @escaping @MainActor @Sendable (Action) -> Void) {
self.send = send
}
/// Sends an action back into the system from an effect.
///
/// - Parameter action: An action.
public func callAsFunction(_ action: Action) {
guard !Task.isCancelled else { return }
self.send(action)
}
/// Sends an action back into the system from an effect with animation.
///
/// - Parameters:
/// - action: An action.
/// - animation: An animation.
public func callAsFunction(_ action: Action, animation: Animation?) {
callAsFunction(action, transaction: Transaction(animation: animation))
}
Effect 타입 종류
Effect 타입에는 .none , .publisher, .run 이 있다.
비동기 작업을 다룰때는 .run을 이용하고,
Combine의 Publisher를 이용할때는 Effect.publisher를 이용하여 전달하면 된다.
( Effect.task 도 있었지만, Deprecated 되었다. )
public struct Effect<Action> {
@usableFromInline
enum Operation {
case none
case publisher(AnyPublisher<Action, Never>)
case run(TaskPriority? = nil, @Sendable (_ send: Send<Action>) async -> Void)
}
}
extension Effect {
/// An effect that does nothing and completes immediately. Useful for situations where you must
/// return an effect, but you don't need to do anything.
@inlinable
public static var none: Self {
Self(operation: .none)
}
public static func run(
priority: TaskPriority? = nil,
operation: @escaping @Sendable (_ send: Send<Action>) async throws -> Void,
catch handler: (@Sendable (_ error: Error, _ send: Send<Action>) async -> Void)? = nil,
fileID: StaticString = #fileID,
filePath: StaticString = #filePath,
line: UInt = #line,
column: UInt = #column
) -> Self
extension Effect {
/// Creates an effect from a Combine publisher.
///
/// - Parameter createPublisher: The closure to execute when the effect is performed.
/// - Returns: An effect wrapping a Combine publisher.
public static func publisher(_ createPublisher: () -> some Publisher<Action, Never>) -> Self {
Self(operation: .publisher(createPublisher().eraseToAnyPublisher()))
}
}
Dependency
0.41 이전버전까지는 Environment도 개념에 있었는데, 외부 디펜던시들을 담는곳이라고 생각하면된다.
0.41 버전부터는 이러한 디펜던시들을 관리하도록 도와주는 라이브러리를 따로만들었고, @Dependency 프로퍼티레퍼로 이용할 수 있다.
( SwiftUI의 @Environment를 굉장히 참고한듯하다. 코드 이름이나 사용방법이나.. )
위의 코드를 리팩토링해보면,
MovieList를 가져오는 로직을 Dependency로 만드는 방법이다.
DependencyKey 프로토콜을 채택하고, DependencyValues에 extension하여 movieListClient를 추가한다.
struct MovieListClient {
var fetch: () async throws -> [Movie]
}
extension MovieListClient: DependencyKey {
static var liveValue: MovieListClient {
Self {
let (data, _) = try await URLSession.shared.data(from: URL(string: "movie")!)
let newMovie = [Movie()]
return newMovie
}
}
}
extension DependencyValues {
var movieListClient: MovieListClient {
get { self[MovieListClient.self] }
set { self[MovieListClient.self] = newValue }
}
}
그리고 아래처럼 프로퍼티레퍼로 추가하여 사용할 수 있다.
이것의 장점은, TCA튜토리얼 1-3에 나오는데, Test코드 작성이 용이하게 해준다.
@Reducer
struct MovieListFeature {
struct State {
var movie: [Movie]
}
enum Action {
case fetchMovie
case movieResponse([Movie])
}
@Dependency(\.movieListClient) var movieListClient
var body: some ReducerOf<Self> {
Reduce { state, action in
switch action {
case .fetchMovie:
return .run { send in
let newMovie = try await movieListClient.fetch()
await send(.movieResponse(newMovie))
}
case .movieResponse(let newMovies):
state.movie = newMovies
return .none
}
}
}
}
Dependency 주입하기
테스트코드에서는 TestStore에서 withDependency로 주입해주었는데,
실제코드에서는 Store에서는 안넣어줄테니.. 문서에서는 Reducer에서 따로 주입이 가능했다.
dependency에 파라미터가 keyPath, value인데, keyPath의 value와 동일해야한다고 한다.
@inlinable
@warn_unqualified_access
public func dependency<Value>(
_ keyPath: WritableKeyPath<DependencyValues, Value>,
_ value: Value
)
그럼 공식문서 예제코드처럼 .mock으로 만들려면...
var body: some Reducer<State, Action> {
Scope(state: \.feature, action: \.feature) {
Feature()
.dependency(\.apiClient, .mock)
.dependency(\.userDefaults, .mock)
}
MovieListClient를 아래처럼 리팩토링하면 가능할것 같다!
static var mock와 real로 MovieListClient타입을 새로 만들어준다.
struct MovieListClient {
var fetch: () async throws -> [Movie]
static var mock: MovieListClient = MovieListClient(fetch: {
[Movie(name: "베테랑"), Movie(name: "에일리언")]
})
static var real: MovieListClient = MovieListClient {
let (data, _) = try await URLSession.shared.data(from: URL(string: "movie")!)
let newMovie = [Movie(name: "리얼")]
return newMovie
}
}
extension MovieListClient: DependencyKey {
static var liveValue: MovieListClient = Self.real
}
extension DependencyValues {
var movieListClient: MovieListClient {
get { self[MovieListClient.self] }
set { self[MovieListClient.self] = newValue }
}
}
그래서 이제 MovieListFeature를 이용하는 부모리듀서에서 아래처럼 .real로 넣어줄 수 있다.
var body: some ReducerOf<Self> {
Reduce { state, action in
...
}.ifLet(\.movieList, action: \.movieList) {
MovieListFeature()
.dependency(\.movieListClient, .real)
}
}
Scope
앱에는 리듀서들이 넘쳐날텐데, 이 리듀서들을 연결하는 방법도 필요하다.
부모리듀서안에 자식리듀서들을 다루는 방법이다.
MovieListFeature를 가지는 AppFeature를 하나 만들어둔다.
State와 Action에 각각 MovieListFeature.State, MovieListFeature.Action을 둔다.
그리고 body안에 Scope로 각각 State, Action을 keyPath로 연결해둔다.
클로저에는 자식 (MovieListFeature) 리듀서를 초기화해주면 된다.
* 예제를 살펴보면 클로저에 간혹 초기화가 없는 코드가 있는데, 그때는 해당 리듀서를 알고있는경우에는 따로 추가하지 않아도 된다.
* 예를 들어, 지금은 MovieListFeature.State, MovieListFeature.Action으로 각각 따로 정의만 했었고, MovieListFeature자체를 사용하지 않았었다. 밑에서 다룰 StackState & Navigation을 보면 이해가 될것이다.
* body구문이 내부적으로 @resultsBuilder를 이용하고 있어서, 조합에 맞는 리듀서를 생성해낸다.
@Reducer
struct AppFeature {
struct State {
var movieList: MovieListFeature.State
}
enum Action {
case movieList(MovieListFeature.Action)
}
var body: some ReducerOf<Self> {
Reduce { state, action in
return .none
}
Scope(state: \.movieList, action: \.movieList) {
MovieListFeature()
}
}
}
ifLet
movieList가 옵셔널일때는 컴파일에러가 나므로, ifLet을 이용하여 자식 리듀서를 둘수 있다.
@Reducer
struct AppFeature {
struct State {
var movieList: MovieListFeature.State?
}
enum Action {
case movieList(MovieListFeature.Action)
}
var body: some ReducerOf<Self> {
Reduce { state, action in
return .none
}.ifLet(\.movieList, action: \.movieList) {
MovieListFeature()
}
//
// Scope(state: \.movieList, action: \.movieList) {
// MovieListFeature()
// }
}
}
Store & View
이제 View단에서 리듀서를 연결하는 방법이 필요하다.
간단하게 store를 하나두고, View에서는 store를 보도록 한다.
또한 store에 옵저빙하기 위해서는 리듀서의 State를 @ObservableState을 이용하여 Observable 프로토콜을 채택하게 해둔다.
- iOS 17 & TCA 1.7부터 지원
- 이전에는 store를 ViewStore를 이용하여 Store를 옵저빙가능한 상태로 만들고 했었다.
struct MovieListView: View {
var store: StoreOf<MovieListFeature>
var body: some View {
VStack {
List {
ForEach(store.movie) { movie in
Text(movie.name)
}
}
Button("fetch Movie") {
store.send(.fetchMovie)
}
}
}
}
@Reducer
struct MovieListFeature {
@ObservableState
struct State {
var movie: [Movie]
}
...
}
Alert, Sheet
위예제를 간단히해서, 아래 fetchMovie를 클릭하면, 리스트를 가져오고,
셀의 버튼을 클릭하면 sheet를 띄우는 화면이다.
Alert나 Sheet같은 View들은 바인딩을 사용하고 있다.
Sheet자체가 바인딩 파라미터가 non-optionl일때 내부적으로 sheet을 띄운다고 되어있다.
따라서 바인딩상태를 제공해야하는데, store를 @Bindable로 둔다.
간단하게 MovieDetailView도 만들어둔다.
struct MovieListView: View {
@Bindable var store: StoreOf<MovieListFeature>
var body: some View {
VStack {
List {
ForEach(store.movie) { movie in
HStack {
Text(movie.name)
Button("Movie Detail") {
store.send(.clickMovieDetial(movie))
}
}
}
}
Button("fetch Movie") {
store.send(.fetchMovie)
}
}
.sheet(item: $store.scope(state: \.movieDetail, action: \.movieDetail)) { store in
MovieDetailView(store: store)
}
}
}
struct MovieDetailView: View {
var store: StoreOf<MovieDetailFeature>
var body: some View {
Text(store.movie.name)
}
}
Feature도 수정한다.
State와 Action을 수정한다.
MovieDetail을 옵셔널로 가지면서, sheet와 같은곳에서 사용하기 위해 @Presents를 붙이고,
Action에는 PresentationAction으로 한번더 자식 액션을 감싼다.
그리고 셀을 클릭했을때, state.movieDetail을 초기화하는 코드도 둔다.
또한 MovieDetailFeature 과도 연결해주어야하는데, 옵셔널타입이므로, ifLet을 이용한다.
간단하게 MovieDetailFeature도 구현해둔다.
목데이터를 이용하므로, MovieListClient도 수정해둔다.
@Reducer
struct MovieListFeature {
@ObservableState
struct State {
var movie: [Movie]
@Presents var movieDetail: MovieDetailFeature.State?
}
enum Action {
case fetchMovie
case movieResponse([Movie])
case clickMovieDetial(Movie)
case movieDetail(PresentationAction<MovieDetailFeature.Action>)
}
@Dependency(\.movieListClient) var movieListClient
var body: some ReducerOf<Self> {
Reduce { state, action in
switch action {
case .fetchMovie:
return .run { send in
let newMovie = try await movieListClient.fetch()
await send(.movieResponse(newMovie))
}
case .movieResponse(let newMovies):
state.movie = newMovies
return .none
case .clickMovieDetial(let movie):
state.movieDetail = MovieDetailFeature.State(movie: movie)
return .none
case .movieDetail:
return .none
}
}.ifLet(\.$movieDetail, action: \.movieDetail) {
MovieDetailFeature()
}
}
}
struct Movie: Identifiable {
var id = UUID()
var name: String
}
@Reducer
struct MovieDetailFeature {
@ObservableState
struct State {
var movie: Movie
}
enum Action {
case bookingMovie
}
var body: some ReducerOf<Self> {
Reduce { state, action in
switch action {
case .bookingMovie:
return .none
}
}
}
}
extension MovieListClient: DependencyKey {
static var liveValue: MovieListClient {
Self {
return [Movie(name: "베테랑"), Movie(name: "에일리언")]
// let (data, _) = try await URLSession.shared.data(from: URL(string: "movie")!)
// let newMovie = [Movie()]
// return newMovie
}
}
}
App진입점에서는 아래와같이 Store를 초기화해주면 된다.
@main
struct TCASampleApp: App {
var body: some Scene {
WindowGroup {
MovieListView(store: .init(initialState: MovieListFeature.State(movie: []), reducer: {
MovieListFeature()
}))
}
}
}
if let store
위에서는 sheet를 이용한 방법말고, 뷰에서 옵셔널인경우에 대해서도 필요하다.
AppView를 하나만들고,
AppView에는 버튼을 두어, 버튼을 클릭하면 안보이던 MovieListView를 보여주는 작업을 할거다.
AppFeature를 만들고,
내부적으로 MovieListFeature.State, Action을 각각 만들어두고,
옵셔널이니 body에는 ifLet으로 둔다.
AppView에서는 if let movieListStore 을 이용하여 옵셔널 store를 가질수있게된다.
@Reducer
struct AppFeature {
@ObservableState
struct State {
var movieList: MovieListFeature.State?
}
enum Action {
case movieList(MovieListFeature.Action)
case appStart
}
var body: some ReducerOf<Self> {
Reduce { state, action in
switch action {
case .appStart:
state.movieList = MovieListFeature.State(movie: [])
return .none
case .movieList:
return .none
}
}.ifLet(\.movieList, action: \.movieList) {
MovieListFeature()
}
}
}
struct AppView: View {
var store: StoreOf<AppFeature>
var body: some View {
VStack {
if let movieListStore = store.scope(state: \.movieList, action: \.movieList) {
MovieListView(store: movieListStore)
} else {
VStack {
Text("App View")
Button("app Start") {
store.send(.appStart)
}
}
}
}
}
}
StackState & NavigationStack
요 내용은 이전글에 작성해두었다.
SwiftUI의 NavigationPath를 이용하여 Navigation을 관리하는 방법인데, TCA버전으로 StackState를 이용하여 구현할수있다.
https://vapor3965.tistory.com/121
개인적인 생각
swiftUI의 다른 아키텍처를 보질않았어서 그렇지만, 기존 아키텍처들에 비하면 TCA가 swiftUI에 꽤나 잘 어울린다라고 생각한다.
또 업데이트버전을 보면 굉장히 swift 최신버전에도 지원하려는 모습이 보이고, swiftUI 관련 view에 대해서도 지원을 하려고 하는걸 보면 괜찮아 보인다.
클린 아키텍처기반으로 한 개념을 보면, 의존성을 제거하고, 분리가 핵심이라고 생각이든다.
하지만 앱이라는 플랫폼상 어쩔수없이 트리구조를 갖을수 밖에 없다라고 생각이 든다. ( 웃기지만, 트리가 가장 아름다운 구조라고 생각됌ㅎ) 앱은 뷰가 필요한데, 뷰들은 무조건 트리다. 뷰안에 자식뷰가 있을수 밖에 없다.
네비게이션도 마찬가지. 네비게이션안에 뎁스들이 계속 있는데, 여러갈래로 뎁스가 나뉘어질 수 있고, 이는 트리구조다.
그렇다면 분리가 된 객체들도 결국에는 트리구조로 다루어져야한다.
분리되어있지만, 통신채널은 필요하다. 자식은 이벤트를 받을수 있다. 이 이벤트가 부모에게 알려야할수도 있다. (무조건 생긴다)
아키텍처들이 결국은 다 분리가되면서도, 자식은 부모에게 어떻게 이벤트를 전달하고, 이를 어떻게 처리하는지가 관건인것 같다.
그러면에서 TCA도 결국, 리듀서별로 분리가 되고, scope를 통하여 자식이벤트를 가로채고, scope로 자식리듀서들을 관리하고.
지금 대안으로는 swiftUI로 개발한다면 TCA가 가장 적합하지 않나? 라는 생각이 든다.
완전히 뷰와 리듀서가 분리돼서 리듀서로 온전히 비즈니스로직들을 관리하는 점은 마음에 든다. 뷰를 보지않아도, 리듀서만 보면 어떤 액션들이 있고, 어떤 흐름이 볼수있다고 생각이 든다.
다만 단점으로는, TCA가 아키텍처이지만, TCA에서 만든 코드들을 사용해야하는 진입장벽이 있는 듯하다.
개념자체는 쉽지만, 이를 코드로 사용하려면 TCA에서 만든 코드들을 사용해야한다. 개인적으로는 다소 난해했다. 보다보니 익숙해졌지만..
이미 swiftUI에서 제공하는 코드들만 해도 기존 UIKit과는 생소한 코드들이 많은데, 여기에 TCA에서 제공하는 코드까지도 복잡하니, 더 어렵게 느껴졌던것 같다.
또, scope가 많아질수록, 관리하는 코드들이 꽤... 난해하다고 해야하나..
아래코드는 튜토리얼에 있던코드인데, 솔직히 가독성이 좋은건지 잘모르겠다. 계속 쓰다보면 익숙해질것 같긴한데, 뭐..그렇다.
Reducer
switch action {
...
case .destination(.presented(.addContact(.delegate(.saveContact(let contact))))):
...
}
또 단점은, 라이브러리가 무겁다라는 생각이 든다. 클린빌드할때마다 꽤 시간이 걸린다. m3 맥북프로 14인치인데, 샘플프로젝트에 TCA라이브러리 하나있는데, 27초 걸린다. ( TCA라이브러리 받아오면 관련 라이브러리가 엄청 많긴하다. 13개 있는듯 ) (xcframework로 만들어서 한번 사용해봐야겠다. 아니 필수인가 ? )
그리고 코드를 작성하다보면 컴파일타임에 알지못한다는 에러가 자주 뜬다. 당연히 내가 코드를 제대로 작성못한것도 있긴하지만, 정말 제대로 작성해야 한다는 점과, 반대로 조금만 꼬여도 타입추론이 실패하는건지.. 암튼 코드작성할때도 마냥 쉽지는 않았다.
한편으로는 장단점이지만, TCA가 굉장히 업데이트가 잦다. 그만큼 지원을 많이 해주는거여서 좋긴한데, TCA를 본지 한달? 정도 된것 같은데 그사이에 마이너버전이 2개나 올라갔다. 분명 1.12로 시작했는데, 어느순간 1.13, 1.14 까지 올라왔다. 버전업이 되면서 코드들도 많이 바뀌기도 하고해서.. 구글링하다보면 예전코드들도 많고.. 하지만 공식 튜토리얼이 굉장히 잘되어있으므로 ㅎ
'아키텍처' 카테고리의 다른 글
TCA(1.14) - StackState & NavigationStack 간단 구현 (1) | 2024.08.30 |
---|---|
아키텍처 공부중.. RIBs, TCA 트리구조 ? (0) | 2024.08.02 |
아키텍처 공부 amitshekhariitbhu/iOS-Viper-Architecture (1) | 2024.03.29 |
아키텍처 공부 kudoleh/iOS-Clean-Architecture-MVVM (0) | 2024.03.28 |
클린 아키텍처 - Clean Architecture (1) | 2024.03.27 |