SwiftUI TCA - Dependency, Reducer, Effect에 대해서 알아보자!!

Dependency (의존성)

우리가 통상 개발하는 앱은 카운터 앱에 자동 카운트 기능을 넣고 싶을수도 있고, 이보다 더 복잡하고 많은 네트워크 코드가 들어가는 상황이 생긴다. 그럴때 Reducer 안에서 이러한 의존성을 가리키는 Dependency가 생기는 경우는 어떻게 처리해야할 지 알아보자.

 

예를 들어 카운터 앱에 자동 카운트 기능을 넣기 위해 Timer가 필요하게 된다. 이럴 땐 TCA에서 기본적으로 지원하는 Timer의 Dependency를 사용할 수 있다.

@Dependency(\.continuousClock) var clock

또한, API를 포함해 네트워크 코드들은 통신하는 Client와 의존성이 생기게 된다. 이때 Timer보다 더 복잡한 과정이 필요하지만 이 내용은 추후에 제대로 다시 설명해 보도록 하자.

 

아무튼 상단의 코드와 같이 Dependency로 의존성을 주입하게 되면 Reducer 안에서 이 의존성을 활용해 원하는 데이터로 편집 후 Effect로 방출하게 된다.

@Dependency(\.continuousClock) var clock
var body: some ReducerOf<Self> {
    Reduce { state, action in
        switch action {
        case .timerButtonTapped:
            state.isTimerOn.toggle()
            
            if state.isTimerOn {
                return .run { send in
                    for await _ in self.clock.timer(interval: .seconds(1)) {
                        await send(.timerTicked)
                    }
                }
                .cancellable(id: CancelID.timer)
            } else {
                return .cancel(id: CancelID.timer)
            }
        }
    }
}

이 내용만으로는 Effect와 Dependency를 한번에 이해하기는 어려운 것 같다 일단 이런 개념이다~ 정도로만 생각하고 넘어간 뒤 Reducer에 대해서 조금 더 알아보고 Effect 부터 찬찬히 다시 알아보도록하자.

 

TCA에서 Reducer가 프로토콜로 바뀐 이유에 대해서 알아보면 Reducer는 위에서 사용한 것 처럼 프로토콜의 방식이 아니었고 그냥 이니셜라이저를 이용한 방식이었다.

// 이전에 사용되던 Reducer
// Reducer 변수를 정의하고 제네릭으로 State, Action, Environment를 받습니다.
let reducer = Reducer<State, Action, Environment> { state, action, environment in
  
    switch action {
    case .incrementButtonTapped:
        state.count += 1
        return .none
    // 다른 액션들에 대한 처리도 여기에 추가할 수 있습니다.
    }
}

TCA 공식 깃허브에서 있던 옛날 방식의 Reducer의 예시이다. Reducer 변수를 생성하고 제네릭으로 State, Action, Environment를 받아서 각 액션에 대한 처리 로직을 정의했다. 이를 통해 앱 동작을 제어하고 상태를 업데이트하며, 필요하다면 액션을 반환해 Effect로 나타낸 방식이었다.

 

Swift 5.7에서 도입된 불투명 타입을 파라미터로 사용할 수 있는 기능은 TCA와 같은 패턴에서 추상화 수준을 높이고 모듈화를 더 쉽게 만들어주었다. 원래부터 프로토콜의 이점은 큰 기능을 작은 컴포넌트로 분해하는 TCA의 목표와 잘 부합하였는데 이러한 추가 기능으로 인해 Reducer를 프로토콜로 정의하는 것이 더 유용해 졌고 결과적으로 SwiftUI에서 뷰를 다루는 방식과 유사한 방식으로 도메인을 중첩하고 모듈화할 수 있게 됐다.

 

이 과정에서 기존에는 Environment 타입으로 관리하던 의존성을 @Dependency라는 프로퍼티 래퍼로 대체하게 된 것이다!


다시 돌아와서 Action이 반환하는 타입이자 Action을 거친 모든 결과물을 칭하는 Effect와 그 중 외부에서 어떠한 처리가 일어나 예상하지 못하게 얻은 결과물인 Side Effect에 대해서 조금 더 나아가 비동기 작업이나 외부 작용에서 발생하는 Side Effect들을 우리 앱의 로직에 통합하는 역할인 Store 까지 알아보도록 하자

 

위에서 말했던 것 처럼 Effect는 Reducer의 액션이 반환하는 타입으로, 액션을 거친 모든 결과물이라고 할 수 있다. 그 중 외부에서 어떠한 처리가 일어나서 얻게 된 예상과 다른 결과물은 Side Effect라고 한다.

 

Effect는 외부 시스템과 상호작용하는 작업을 나타내는데, 이를 통해 앱의 State가 변경된다. State를 직접 변경할 때의 Action과 달리, Effect는 비동기적인 작업을 수행하고 그 결과를 Action으로 반환하여 State에 반영하기 위해서 사용된다.

 

즉, Effect는 특정 Action을 실행한 후 그 결과에 따라 새로운 Action을 생성하고 이를 통해 State를 업데이트 하는 역할을 담당한다. 네트워크 호출, 데이터 로딩, 외부 서비스와의 교류 등 다양한 비동기 작업이 Effect로 분류 될 수 있다.

 

Effect의 역할

  • 비동기 작업 관리 : 네트워크 요청, 데이터 로딩, 파일 다운로드 등 다양한 비동기 작업을 Effect를 통해 관리
  • Side Effect 분리 : Effect는 순수 함수형 프로그래밍의 원칙에 따라 Side Effect를 배제한다. 이를 통해 코드에서 State 변화를 일으키는 부분과 Side Effect를 다루는 부분을 명확하게 분리함으로써 코드의 가독성과 추론력이 향상되며, 테스트와 디버깅 과정이 용이해진다.
  • 취소 및 에러 핸들링 : Effect는 비동기 작업의 성공, 실패 및 중단을 관리하는데 사용된다. 예를 들어, 네트워크 요청 중에 발생한 오류를 적절하게 처리하고 State를 업데이트할 수 있다.
  • 순서 보장 : TCA의 Effect는 순차적으로 실행되며 그 순서가 보장된다. 이로써 State 변화와 관련된 Side Effect를 적절하게 처리하면서도 예측 가능한 결과를 얻을 수 있다.

순수 함수적인 Effect

순수 함수는 주어진 입력에 대해 항상 동일한 출력을 반환하고 외부 상태를 변경하지 않으며, Side Effect가 없는 함수를 의미한다. 그렇기 때문에 순수 함수 자체로는 비동기 작업이나 Side Effect를 처리할 수 없다.

 

그러나 TCA의 Effect는 이 문제를 해결할 수 있다. Effect는 앱의 상태를 직접 변경하지 않고 비동기 작업을 수행한 후 그 결과를 새로운 Action으로 반환하는 역할을 한다. 이렇게 생성된 Action은 Reducer에서 처리되어 State를 업데이트 한다.

 

예를 들어 네트워크 요청 같은 비동기 작업을 처리하는 경우, Effect가 요청을 수행하고 결과 데이터 또는 오류 정보 등을 포함하는 새로운 Action을 생성해 반환하는데 이 Action은 다시 Reducer에서 받아 상태 업데이트 로직을 수행하게 된다.

 

이러한 방식을 통해 TCA에서 Effect는 사이드 이펙트와 같은 비동기 작업도 순수 함수적인 방식으로 처리할 수 있도록 설계되어 있다.

이제 주요 메서드에 대해서 알아보자.

struct CounterFeature: Reducer {
    struct State: Equatable {
        var count: Int = 0
        var isTimerOn: Bool = false
    }
    
    enum Action: Equatable {
        case plusButtonTapped
        case minButtonTapped
        case timerButtonTapped
    }
    
    var body: some ReducerOf<Self> {
        Reduce { state, action in
            switch action {
            case .plusButtonTapped:
                state.count += 1
                
                return .none
                
            case .minButtonTapped:
                state.count -= 1
                
                return .none
                
            case .timerButtonTapped:
                
                return .none
            }
        }
    }
}

.none

먼저 .none은 Effect를 반환해야하는데 아무런 동작도 취하고 싶지 않을때 사용한다. 지금까지 말해왔던 비동기 처리, 데이터베이스 접근 등과 같은 의존성이 필요한 작업이 아닌 간단한 Aciton에서 진행할 수 있는 내용은 바로 진행 후 Effect를 반환하지 않는다는 .none을 사용하게 된다.

.send

파라미터로 Action을 받는 메서드로 특정 액션 이후 즉시 추가적인 동기 액션이 필요할 때 사용된다. 주로 자식 컴포넌트에서 부모컴포넌트로 데이터를 전달할때 사용된다. 또한 액션을 전달하는 동시에 애니메이션을 지정할 수 있다.

공식 문서 상 로직을 공유하기 위한 목적으로 사용하지 말라고 권장하고 있다. 즉, 여러 곳에서 동일한 로직을 사용하기 위해 .send 메서드를 사용하지 말라는 뜻인데, 이것은 코드의 중복이 발생할 수 있어 TCA의 핵심 철학인 단방향 데이터 흐름을 저해할 뿐 만 아니라 코드의 의도를 파악하기 어려울 수 있기 떄문이다.

.run

비동기 작업을 래핑하는 메서드이다. 인자로 비동기 클로저를 받아서 실행하며, 클로저 내부에서 send를 사용해 액션을 시스템에 전달할 수 있다.

// .run 메서드 내부 operation 인자

operation: @escaping @Sendable (_ send: Send<Action>) async throws -> Void

런 메서드 내부는 이렇게 구현되어있다. Action을 만들어 내보내는 것을 볼 수 있다.

case .timerButtonTapped:
    state.isTimerOn.toggle()
    
    if state.isTimerOn {
        return .run { send in
            for await _ in Timer.publish(every: 1, on: .main, in: .common).autoconnect().values {
                await send(.tick)
            }
        }
    } else {
        return .cancel(id: TimerID())
    }
    
case .tick:
    state.count += 1
    
    return .none
}

사용 예시를 한번 보면 .run 으로 비동기 작업을 수행하고 send 매개변수를 통해 액션을 전달하게 되는데 for await 으로 Timer를 비동기로 처리하는 것을 볼 수 있다.

 

참고자료 : https://axiomatic-fuschia-666.notion.site/Chapter-3-TCA-2-c56b24efb2154dad9ed8e54139247024

 

Chapter 3. TCA의 기본개념(2) | Notion

앞선 장에서 우리는 앱의 상태를 나타내는 State와 이를 변경할 수단인 Action, 그 Action의 기능을 구현하고 상태의 변경을 처리하는 Reducer을 알아보며, TCA에서의 데이터흐름에 대해서 살펴보았습니

axiomatic-fuschia-666.notion.site

 

오늘은 이정도로 알아보고 아직 갈길이 멀다.. 다음 시간엔 Cancel 등록 , Store 등등 더 자세하게 알아보는 시간을 가져보자!!!

 

'◽️ Programming > TCA' 카테고리의 다른 글

Share Run TCA 로직 구현  (3) 2024.09.30
SwiftUI TCA (3) - Binding  (0) 2024.09.23
SwiftUI TCA (2) - Effect  (0) 2024.09.15
SwiftUI TCA (1)  (0) 2024.09.09