티스토리 뷰

반응형

 

 

2019년도부터 시작해서 4년만에 드디어 The Composable Architecture, TCA 1.0이 배포되었습니다.

point free에서 TCA 1.0 배포와 함께 TCA Basic 소개 영상을 공유했는데요. Basic 영상에서 다룬 TCA 관련 기본 기능 내용들을 알아보겠습니다.

 


TCA 1.0 Basics 기초 사용방법 알아보기
Reducer와 그 안의 State, Action, body

Reducer를 만들기 위해서 Reducer protocol을 채택하고, 그 안에 State 구조체, Action 열거형, body를 정의합니다. (TCA에서 전체적으로 사용하던 ReducerProtocol 키워드가 Reducer로 변경되었습니다.)

 

 

Action에서는 버튼을 탭하거나, 텍스트를 입력하는 등의 액션이 정의될 수 있습니다. 일반적으로 Action case 명은 사용자의 제스쳐에 기반해서 이름을 짓는것을 권장합니다.

 

 

body에서는 다양한 Reducer를 유기적으로 조합하여 동작되며, 해당 Reducer에서 비즈니스로직 등이 동작할 수 있습니다. 이 과정에서 비즈니스로직에 따라 하위 Reducer의 변경이나, 상위 Reducer의 action이 수행될 수도 있습니다.

Reduce의 trailing closure에서 action에 따른 비즈니스 로직을 동작시키고, 필요하면 state를 변경해서 그에 맞게 화면을 랜더링 할 수 있습니다. 

- Reducer에서 state변경이 이루어지면 그에맞게, Reducer와 바인딩 되어있는 View를 렌더링 됩니다.

 

한번 더 요약하자면 body의 Reduce를 통해 적절한 시점에 전송된 action에 대한 비즈니스 로직이 수행되며, 그에 맞게 state가 변경됩니다.

Reduce에서의 각각의 action에 대한 비즈니스 로직이 끝나면, 다음 action을 명시하기 위해 Effect를 반환하는데, 이때 추가적인 action이 필요 없다면, .none을 반환합니다.

 

 

Reducer를 사용할 View쪽을 이어서 보겠습니다. 여기에서 store 멤버를 선언합니다. (StoreOf<Home>) 

- StoreOf<Feature>는 Store<Feature.State, Feature.Action>를 간단하게 표현하는 별명타입입니다.

 

View에 Reducer 연결하기, Store와 ViewStore

store를 통해 직접적으로 state property를 접근하지는 못합니다. 어떤 state를 감지할지를 명시할 수 있는 ViewStore 타입으로 변환을 거친 뒤, viewStore를 통해 state를 접근하고, action을 보낼 수 있습니다.

이때 ViewStore의 observe 인자를 통해서 Reducer 특정 state만 감지하도록 할 수도 있습니다. 위의 코드는 observe parameter를 통해 해당 Reducer의 State 전체를 감지하도록 설정한 코드입니다.

- TCA 1.0부터는 observe parameter를 반드시 명시하도록 변경된 것으로 보입니다.

 

 

TCA 기반의 View 또한 Preview를 지원합니다. Store의 initialState, Reducer를 설정하면 됩니다.

* iOS17, swift 5.9부터는 viewStore대신 store를 통해 state접근 및 action 전송이 가능합니다!

 

 

Reducer 생성자에 ._printChanges()를 달면, 해당 Reducer와 관련된 action, state의 변화를 로그로 확인 가능합니다.

 


API Networking 요청, 딜레이 설정

Reducer > body의 Reduce trailing closure > API 요청 등을 할때, .run Effect를 사용가능합니다.

.run Effect의 trailing closure 내에서 API 요청을 수행하고, 그 결과를 다른 Effect로 반환할 수 있습니다. API 요청 결과에 대해 state 변경이 필요하다면, send 핸들러를 사용해서 다른 Effect를 반환해서 다른 action에서 state 처리를 할 수 있습니다.

* 기존 버전의 EffectTask는 사용할 수 없고, EffectTask는 Effect로 이름이 일괄 변경되었습니다.

* .run Effect trailing closure block에서 필요하다면 Task.sleep(nanoseconds:), iOS16 이상 타겟이라면, Task.sleep(for:)를 통해 .seconds(0.1) 와 같이 밀리나노초 대신, 초 단위로 비동기 딜레이를 설정할 수도 있습니다.

 

 

API 요청 간, 프로그래스뷰를 보여주기 위한 flag state를 적시적소에 변경해서 API 요청 로딩 중에 프로그래스뷰를 보여줄수도 있습니다. ex) ProgressView(), isLoading state

 

 

// 원하는 시점에 .run 동작을 취소하고 싶다면, .cancellable(id:)을 사용 가능
.run {
	// do something ...
}
.cancellable(id: SomeId.id)

if state.isTimerOn {
  …
} else {
  // 원하는 시점에 취소 가능
  return .cancel(id: SomeId.id)
}

.run closure 블럭 내의 작업을 원하는 시점에 취소하고 싶다면, .run 블럭 끝에 .cancellable(id:)를 설정할 수 있습니다.

작업에 대한 고유 id와 cancel(id:)를 사용해서 원하는 시점에 run 블럭에 명시한 작업을 취소할 수도 있습니다.

 


TCA Unit Testing 테스트 코드 작성하기

유닛테스팅은 iOS 커뮤니티에서 중요하게 다루는 주제입니다. 이번에는 TCA의 testabiltiy 측면을 알아보겠습니다.

@MainActor
func testFeature() async {
  let store = TestStore(initialState: Feature.State()) {
    Feature()
  }
}

Test Target의 테스트코드를 작성할 파일에서 XCTest뿐만 아니라, ComposableArchitecture를 import 해줍니다. TCA방식의 테스트를 위해 TestStore를 사용 가능합니다.

여기에서 initialState로 테스트하고자 하는 Reducer의 초기 상태와 Reducer 자체를 설정하여 테스팅을 준비할 수 있습니다.

 

 

// TestStore에서 특정 action을 전송하고, 액션 결과로 기대되는 state를 설정해서 테스팅이 가능합니다.
await store.send(.incrementButtonTapped) {
  $0.count = 1 // count state가 1이 되었는지 확인
}
await store.send(.decrementButtonTapped) {
  $0.count = 0 // count state가 0이 되었는지 확인
}

TestStore의 인스턴스를 통해 특정 action을 send 할 수 있고, 그에 따라 state값이 적절하게 변경되는지를 확인 가능합니다. 이때 조건을 충족하지 않으면 TCA에서는 그에 맞는 적절한 에러 문구를 보여줍니다

테스팅 시 주의할 점은, 명시한 테스트코드 동작으로 앱의 action이 정상적으로 마무리 되어야 합니다. 만약 무한루프나, 특정 동작이 지속된다면, 테스팅 실패 및 실패 문구가 발생할 수 있습니다.

테스팅 시에도 또한 Task.sleep(for:)을 사용해서 딜레이가 발생하는 시나리오를 만들 수 있습니다. 예를들면 특정 API 요청, 1초 딜레이 후, 원하는 결과나 이벤트를 올바르게 받을 수 있는지 확인할 수 있습니다.

이 과정에서도 중간에 정상적으로 처리하지 않은 action이 있다면, 실패문구를 보여줍니다. (특정 action을 처리하지 않았다거나...)

 

 


사이드이펙트 처리에 사용되는 Dependency, Testing 활용방법

사이드이펙트 처리에 사용되는 Dependency라는 개념이 있습니다. CoreData, Networking 등의 작업을 처리할때 사용할 수 있습니다. Dependency또한 Testing에 활용할 수도 있습니다.

Reducer 내에서는 @Dependency propertywrapper를 정의해서 사용할 수 있습니다. 해당 Dependency 인스턴스가 Reducer 내 사이드이펙트 관련 작업과 관련된 로직을 분담하도록 할 수 있습니다.

func test() async {
  let store = TestStore(initialState: Feature.State()) {
    Feature()
  } withDependencies: {
  	// Test 환경에서의 dependency 설정
    $0.uuid = .incrementing
  }
  
  // 테스팅 과정에서 dependency를 사용하는데, withDependencies를 통해 미리 적절하게 설정하지 않으면 테스팅이 실패할 수 있다.
  store.exhaustivity = .off(showSkippedAssertions: true)

  await store.send(.addButtonTapped) {
    $0.values = [Model()]
  }
}

Dependency를 Testing에도 사용할 수 있는데, TestStore 생성자의 withDependencies의 closure parameter를 통해서 테스팅에 사용하려는 Dependency 를 설정한 뒤, 테스팅에 사용할 수 있습니다.

Dependency를 통해 네트워킹 등의 테스트를 할때 고민사항이 생길 수 있습니다. 외부로부터 받은 네트워킹 결과 값이 어떻게 될지를, 언제 도착할지를 예상할 수 없는 것이죠. 이런 상황에서 TestStore 생성자의 withDependencies parameter에서 특정 Dependency의 예상되는 요청 및 응답 로직을 테스팅 목적에 맞게 명시하여 활용할 수 있습니다. 

* 네트워킹 요청 시에 Dependency를 통해 Error throwing이 발생하는 경우에 이를 고려해서 테스트 코드를 작성해주어야 합니다. ex) 에러가 발생할 것으로 보인다면 XCTExpectFailure() 를 사용 할 수도 있습니다.

 


지금까지 TCA 1.0 Basics에서 다루던 내용을 정리해봤습니다.

위 내용을 글만 읽으면 이해하기 어려운 내용이 많습니다. 🥲 TCA tutorial의 내용과 매우 흡사하니, TCA tutorial과 함께 TCA Basics내용을 참고하셔도 좋을 것 같아요.

또한, TCA(The Compoasble Architecture) 메인 Repository에 관련 example 프로젝트가 많이 제공되고 있으니, 해당 프로젝트 구조, 예제 코드를 함께 확인하면서 이해를 하는 것을 권장합니다.

도움이 되셨다면 구독과 함께 많은 의견 환영합니다. 감사합니다!

 


References

 

Episode #243: Tour of the Composable Architecture 1.0: The Basics

The Composable Architecture has reached a major milestone: version 1.0. To celebrate this release we are doing a fresh tour of the library so that folks can become comfortable building applications with it in its most modern form. We will start with a simp

www.pointfree.co

 

 

반응형
댓글
반응형
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2024/05   »
1 2 3 4
5 6 7 8 9 10 11
12 13 14 15 16 17 18
19 20 21 22 23 24 25
26 27 28 29 30 31
글 보관함