홍로그

TCA 본문

iOS

TCA

성홍민 2023. 8. 3. 18:04

📖TCA란?

iOS의 TCA(The Composable Architecture)는 SwiftUI 및 Combine을 사용하여 앱 아키텍처를 구축하는 프레임워크입니다. TCA는 앱의 로직을 크게 단순화하고 테스트 가능한 컴포넌트로 구성하여 코드 재사용과 확장성을 증가시키는 것에 초점을 맞추고 있습니다. TCA는 복잡하고 다양한 목적을 가진 앱을 구축하는 데 사용할 수 있고 앱 개발에 있어서 매일 마주하는 많은 문제들을 해결하기 위한 구체적인 방법들을 제공합니다.

State management

단순한 값 유형을 사용하여 앱의 상태를 관리하고 여러 화면 간에 상태를 공유하여 한 화면에서 발생한 변화가 다른 화면에서 즉시 관찰될 수 있도록 하는 방법입니다.

Composition

큰 기능을 작은 구성 요소로 분해하고 각 요소를 자체적이고 격리된 모듈로 추출하여 간단하게 연결하여 기능을 형성하는 방법입니다.

Side effects 

앱의 특정 부분이 외부와 상호작용하는 방식을 최대한 테스트하기 쉽고 이해하기 쉬운 방법으로 만드는 것입니다.

Testing

아키텍처로 구축된 기능을 테스트하는 것뿐만 아니라 많은 요소로 구성된 기능에 대한 통합 테스트를 작성하고 부작용이 어떻게 앱에 영향을 미치는지 이해하기 위한 종단 간 테스트를 작성하는 방법입니다. 이를 통해 비즈니스 로직이 예상대로 실행되고 있는지 확실히 확인할 수 있습니다.

Ergonomics

최소한의 개념과 간단한 API로 이 모든 것을 수행하는 방법입니다.

 

TCA의 구성요소

State

TCA의 States는 앱의 모든 상태 데이터를 나타내는 struct로, 앱의 모든 상호작용에 관련된 최신 정보를 담고 있습니다. 

import ComposableArchitecture

struct State: Equatable {
  var counter: Int = 0
  // 추가적인 애플리케이션 상태를 여기에 선언할 수 있습니다.
}

위에서 counter라는 정수 값을 State라는 struct에 선언했습니다. 앱의 기능이 추가 될수록 이 struct에 추가적인 상태 데이터를 선언할 수 있습니다. 이와 같이 모든 상태 데이터를 한 struct에 담음으로써 상태 관리를 명확하게 할 수 있습니다.

struct에 Equatable 프로토콜을 구현함으로써 상태의 동일성을 쉽게 비교하고 상태 데이터의 추적 및 관리가 용이해지며 디버깅 및 테스트에 유리한 구조를 갖게 됩니다.

Action

TCA의 Action은 enum으로 표현되며, 앱에서 발생할 수 있는 모든 사용자 상호작용, 시스템 이벤트 및 비동기 이벤트에 대한 변화를 나타냅니다. Action은 앱의 상태 변화에 따른 응답을 정의하고, 앱 내부에서 발생하는 모든 액션을 포함합니다. 한 enum의 모든 액션을 포함하기 때문에 상태 변경 과정을 명확하게 파악할 수 있습니다.

import ComposableArchitecture

enum Action: Equatable {
  case increment
  case decrement
  // 추가적인 애플리케이션 액션을 여기에 선언할 수 있습니다.
}

모든 액션을 명시적으로 선언함으로써 앱의 상태 변화와 데이터가 투명하게 관리되고, 테스트 및 디버깅이 용이해집니다. Action도 마찬가지로 Equatable 프로토콜을 구현하여 동일성을 쉽게 비교할 수 있게 하고, 테스트 케이스 작성에 유리한 구조를 제공합니다.

Environment

Environment는 reducer에게 필요한 모든 외부 서비스와 상호작용을 나타내는 타입입니다. 앱의 기능에 따라 디바이스, 네트워크, 데이터베이스 등과 같은 외부 시스템과 상호작용을 할 수 있는 구조를 제공합니다. Environment를 통해 코드와 외부 의존성을 분리하여 테스트 용이성을 향상시킬 수 있습니다.

Effect

Effect는 앱 로직과 외부 서비스 및 자원 사이에서 값을 전달하고, reducer에서 독립적으로 처리할 수 없는 비동기 작업이나 외부 시스템과의 상호작용을 나타내는 타입입니다. reducer에 의해 이벤트 처리와 State 관리가 발생할 때, Side Effect가 일어날 수 있음을 명시하고 처리할 수 있도록 합니다.

Reducer

State와 Action을 입력으로 받아 새로운 상태를 만드는 순수 함수입니다. Reducer는 State와 Action간의 관계를 바탕으로 앱의 데이터와 로직을 처리합니다.

import ComposableArchitecture
import Foundation

struct State: Equatable {
  var counter: Int = 0
}

enum Action: Equatable {
  case increment
  case decrement
  case incrementAsync
  case asyncIncrementResult(Result<Int, Error>)
}

struct Environment {
  var asyncIncrement: () -> Effect<Int, Error>
}

func asyncIncrement() -> Effect<Int, Error> {
  return Effect<Int, Error>.future { promise in
    DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) {
      promise(.success(1))
    }
  }
}

let reducer = AnyReducer<State, Action, Environment> { state, action, environment in
  switch action {
  case .increment:
    state.counter += 1
    return .none
  case .decrement:
    state.counter -= 1
    return .none
  case .incrementAsync:
    return environment.asyncIncrement()
      .map(Action.asyncIncrementResult)
  case let .asyncIncrementResult(.success(value)):
    state.counter += value
    return .none
  case .asyncIncrementResult(.failure):
    return .none
  }
}

let store = Store(
  initialState: State(),
  reducer: reducer,
  environment: Environment(asyncIncrement: asyncIncrement)
)

asyncIncrement() 함수는 counter 증가를 비동기로 수행하며 Effect<Int, Error> 값을 반환합니다. Environment를 reducer의 제네릭 타입으로 지정하고 reducer에서 사용합니다. .incrementAsync Action이 발생하면 envrionment.asyncIncrement() 함수가 호출됩니다. 이러한 구조를 사용하면 외부 서비스와 의존성을 분리할 수 있어 테스트를 용이하게 만들고 디버깅을 쉽게 할 수 있습니다. 뿐만 아니라, 다양한 구현이나 구성 요소를 제공하여 앱의 유연성을 높입니다.

 

reducer는 State, Action, Environment의 제네릭 타입을 사용합니다. reducer의 switch문에서는 action에 따라 State를 업데이트하는 로직이 구현되어있습니다. reducer를 사용함으로써 State의 변경 로직이 모듈화되고 예측이 쉬워집니다. 이를 통해 앱 코드의 유지 및 관리가 간편해지고, 테스트 및 디버깅이 훨씬 쉬워집니다.

Store

Store는 앱 State와 Reducer를 저장하고, 뷰와 reducer 사이에서 상태 변경을 조율하는 컨테이너 역할을 합니다. Subscriber에게 State의 변화를 전달하고, 뷰에 Effectf를 결합하는 작업을 수행합니다. 

import ComposableArchitecture
import SwiftUI

struct ContentView: View {
  let store: Store<State, Action>

  var body: some View {
    WithViewStore(self.store) { viewStore in
      VStack {
        Text("Counter: \(viewStore.counter)")
        HStack {
          Button("Increment") {
            viewStore.send(.increment)
          }
          Button("Decrement") {
            viewStore.send(.decrement)
          }
        }
      }
    }
  }
}

ContentView struct는 Store<State, Action> 타입의 store 프로퍼티를 가져옵니다. WithViewStore(self.store)를 사용하면 Store를 뷰와 연결할 수 있습니다. 이를 통해 뷰가 State를 구독하고, State의 변화를 감지합니다. 이 구조를 사용하여 counter의 값을 뷰에 표시하고 Increment 및 Decrement 버튼으로 액션을 보냅니다. 버튼을 통해 액션을 호출하면 Store의 상태가 업데이트되며, 새로운 상태가 뷰에 전달됩니다. 

 

반응형

'iOS' 카테고리의 다른 글

fileprivate과 private의 차이  (0) 2023.08.10
Concurrency Programming  (0) 2023.06.22
DeepLink  (0) 2023.06.20
Combine AnyCancellable  (0) 2023.06.19
WWDC23 SwiftData  (0) 2023.06.14