안드로이드 개발자가 본 iOS TCA 살펴보기

안드로이드 개발자가 본 iOS TCA 살펴보기



개인 광고 영역

이 글은 Android 개발자인 필자가 TCA 일부를 사용해 보고 경험한 내용을 바탕으로 정리하였고, 이 방식이 맞는 것일까의 내용을 포함한다.

오류가 있을 수 있으니 오류가 있다면 적극 알려주면 좋겠다.

TCA는 iOS에서 요즘 많이 활용하고 있는 The Composable Architecture이다.

TCA는 다음을 기준으로 한다.

  • State management
  • Composition
  • Side effects
  • Testing
  • Ergonomics

하지만 실제 사용해 보면 리덕스라고 보는 것이 맞다. 웹에서는 리덕스가 너무 복잡하기 때문에 사용하지 않는 걸로 들었다.

실제 TCA도 매우 복잡한 구조를 가지고 있지만 swiftUi와 함께 사용하기에는 좋다고 한다.

이 글에서는 안드로이드 개발자가 가볍게 TCA를 사용하면서 경험한 내용을 정리해 본 글이니 참고만 하시길.(오류는 언제나 댓글로)

이 글에서는

  • 필자가 생각하는 Composable 구조는?
  • 간단한 샘플 코드를 통한 알아보기

필자가 생각하는 편한 구조

필자는 개발하기 편한 방식을 선호하는 편이다.

본 블로그에서도 여러 번 적었던 내용이지만 필자의 선호는 아래와 같다.

화면의 특정 View를 공통화한다는 가정을 해보자.

ex) back 버튼은 어느 화면에서나 이전 화면으로 돌아가는 공통의 구조를 가진다.

  • 참고 : 여기서 back 버튼을 설명하는 이유는 누가 보아도 가장 공통화 시키기 쉬운 구조라 생각하여 선택하였다.

단,

  • Back 버튼은 데이터를 로딩과 무관하게 Back만 처리할 수 있어야 한다.
  • Back 이벤트 시에는 이전 화면으로의 이동을 하거나, 상황에 따라 alert 을 질문을 받고 종료하기도 한다.

위와 같은 단서도 달아보았다.

그렇다면 필자가 생각하는 구조는 아래와 같이 작성해 볼 수 있다.

  1. View에는 Back 버튼 만을 포함하는 Composable 함수를 생성한다.
  2. View에 대한 로직을 처리하는 곳에서 이벤트를 전달받는다.
  3. router를 통해 이전 화면으로 이동하라는 처리가 일어난다.
    • 여기서 이전이라는 개념은 네비게이션
    • 네비게이션에 주입해둔 정보에 따라 Back을 처리한다.

이러면 공통의 View가 하나 나왔다. 누구나 좋아하는 아키텍처 구조도 따랐고, View만을 스스로 처리할 수 있도록 처리한다.

여기서 드는 의문점?

back을 했는데 이 이벤트는 누가 받아서 처리하는가? 스스로 처리한다고 하였다. 하지만 안 그럴 경우도 있다. 여기서는 이런 부분에 대한 고민을 적는 것은 아니다. TCA에서는 upstream 방식으로 이러한 이벤트를 상위에 연결하여 처리한다.

단 그 이벤트를 처리하지 않으면 아무런 동작도 일어나지 않는다는 점이다.

돌아와서

공통의 View는 어떤 Screen이 든 어디에 위치하든 Back 버튼의 역할을 할 수 있는 어디든 부착되어 동작할 수 있다.

단, 필요한 경우 뒤에서 처리할 수 있는 로직 흐름을 처리해 줘야 하지만 이건 옵션이며, View만 연결한다.

Composable함수() {
    HStack/Column {
        BackButton() // << 이 버튼에서 알아서 동작한다.
    }
}


TCA에서는?

TCA도 유사하게 만들 순 있다. 하지만 다음의 과정을 거쳐야 하는데, 너무 쉬운 부분만을 가져와서 설명하다 보니 설명이 부족할 수 있으며, 샘플 코드는 단순 참고해 주시길

public struct BackButtonCellView: View {
  public var store: StoreOf<BackButtonCell>
  
  public init(store: StoreOf<BackButtonCell>) {
    self.store = store
  }
  
  public var body: some View {
    BackButton() // View
      .onTapGesture {
        viewStore.send(.back)
      }
  }
}

Reducer를 구성한다. 여기에는 State, Action을 포함해야 한다.

이 Reducer는 화면의 비즈니스 처리와 동작을 처리하기 위함이다.

public struct BackButtonCell: Reducer {
  public struct State: Equatable {
            
    public init() {
      // do nothing
    }
  }
  
  public enum Action: Equatable, ErrorAction {
    public enum Delegate: Equatable { // 상위로 전달할 event
      case back
    }
    
    case back // 현재 View에 대한 이벤트
  }
    
  public init() {}
  
  public var body: some ReducerOf<Self> {
    self.core
  }
  
  // 위에서 정의한 event에 대한 동작 정의
  @ReducerBuilder<State, Action>
  var core: some ReducerOf<Self> {
    Reduce { state, action in
      switch action {
        case .back:
            return .run { send in
                await send(.delegate(.back))
            }

        case // 여기에서 사용하지 않은 이벤트들은 여기에 모두 포함
          .delegate:
            return .none
      }
    }
  }
}

여기까지는 공통의 View를 만들었다. Android에서 Compose를 활용한 이벤트 정의와 유사하다.


적용해 보자

Android에서는 어떻게 구성하였는지에 따라 다르겠지만 TCA에서는 이를 적용하려면 적용할 View/Reducer에 코드를 각각 정의해야 한다.

먼저 포함할 View에는 다음과 같다.

IfLetStore(
self.store.scope(
    state: \.backButtonCell,
    action: Some.Action.backButtonCell
),
then: BackButtonCellView.init(store:)
)

Reducer에서는 아래와 같이 포함한다.

public struct Some: Reducer {
    public struct State: Equatable {
        var backButtonCell: BackButtonCell.State? // 사용할 State 정의

        public init() {
            self.backButtonCell = .init() // 객체 생성
        }
    }

    public enum Action: Equatable { // Action 정의
        case backButtonCell(BackButtonCell.Action)
    }

    // Cell 객체 생성
    public var body: some ReducerOf<Self> {
        self.core
            .ifLet(
                \State.backButtonCell,
                action: /Action.backButtonCell
            ) {
                BackButtonCell()
            }
    }

    // 위에서 정의한 event에 대한 동작 정의
    @ReducerBuilder<State, Action>
    var core: some ReducerOf<Self> {
        Reduce { state, action in
            switch action {
                case .backButtonCell(.back): // 받은 이벤트에 대한 처리
                    return .none

                case .backButtonCell: // 나머지 모든 이벤트는 상위로 보낸다.
                    return .none
            }
        }
    }
}

만약 위 이벤트를 해당 View에서 활용하지 않는다면 다음과 같이 ReducerBuilder 정의를 수정한다.

// 위에서 정의한 event에 대한 동작 정의
@ReducerBuilder<State, Action>
var core: some ReducerOf<Self> {
    Reduce { state, action in
        switch action {
            case .backButtonCell: // backButtonCell에 대한 모든 이벤트 return.
                return .none
        }
    }
}

간단하게 작성한 코드라서 이해가 어렵진 않을 것 같다.


이벤트의 흐름

앞서 작성한 필자가 생각하는 안드로이드에서의 Composable에 대한 데이터 흐름은 다음과 같다.

  • Back 이벤트가 발생한다.
  • Back에 대한 비즈니스 로직에서 스스로 router 처리한다.
    • 필요하다면 상위로 전달하여 처리한다.

이지만 핵심은, View만 가져다 사용할 수 있는가이다.

fun SomeScreen() {
    BackButton() // 이렇게만 할 수 있는가?
}

하지만 TCA는 view와 reducer를 구성해야 하고, 이 이벤트를 활용하기 위한 보일러 플레이트 코드가 잔뜩 들어가야 한다.

특이하게도 upstream 이벤트는 A/B/C/D라는 순서대로 화면이 열렸고, D에서 이벤트가 발생하면 A/B/C 모두가 이 이벤트를 처리할 수 있다. 그 말은 .backButtonCell 이벤트를 모두가 받을 수 있다는 말이며, 분리도 가능하다.

어떻게 보면 장점이지만 이 데이터 흐름을 생각 없이 쓸 수 있다는 이야기다.

그럼 그만큼 복잡도 역시 매우 높아질 수 있다.


누구나 이해하기 쉬운 흐름이란?

여기서 한 번 더 생각해 볼 부분은 누구나 이해하기 쉬운 데이터 흐름을 만들 수 있는가이다.

A unidirectional data flow(UDF), 단방향 데이터 흐름을 이야기할 수 있다. TCA가 좋긴 하지만 UDF 적으로 본다면 너무 어려워진다는 단점이다. 이런 이유로 웹에서도 리덕스를 사용하지 않는 거 아닐까 싶다.

개발 편의성 만을 본다면 알아야 할 것이 너무 많다.

이런 아키텍처도 알아야 하고, 데이터의 흐름도 이해해야 하고, 이 흐름의 끝이 어딘지도 찾으려면 타고 타고 이게 어디에서 처리하고 아니길래 왜 이런 동작을 할 것인가?에 대한 고민이 많아진다.

설계적인 관점에선 재사용 구조를 최대한 만들어주는 것이 당연히 좋다. 그래야 개발에서 편하기 때문에.

조립식 집도 집이기 때문에 조립을 잘하는 것도 매우 좋은 설계다.

필자는 어떤 아키텍처도 잘 만들어봐야 팩토리 패턴이라고 생각한다.

그만큼 팩토리처럼 잘 가져다 활용할 수 있는 구조를 만들어야 하는데, TCA는 보일러 플레이트 코드가 너무 많이 생긴다는 단점이 있다.

거기에 업 스트림(event)/다운 스트림(data) 활용을 적절하게 분리하지 않는다면 산으로 갈 수 있다.


안드로이드에서 활용해 볼 법한 것은?

필자는 MVI 패턴을 선호하지 않는다. 어차피 리엑트이고, 리엑트의 개념들을 가져온 것이다.

만약 MVI 패턴의 Reducer까지 잘 적용하였다면 MVI 라기보단 리엑트를 잘 적립한 개념이라 생각한다.

하지만 대부분 MVI 패턴을 사용한다고 하면 대부분 다음 정도가 적용되어 있다.

  • Action과 State 적용
    • UiState 적용
    • Action view to event

이 내용은 구글 안드로이드 아키텍처와 크게 다르지 않다.

차라리 circuit 과 같은 라이브러리를 활용한다면 Reducer 개념을 사용하는 것을 추천한다. 아니면 Flow를 활용해 Reducer 개념을 추가하여도 좋다.


마무리

필자는 개발에 있어서 적용이 쉽고 간결한 걸 선호하는 쪽을 선택한다. 무작정 간결한 것이 아닌 좀 더 쉽게 적용해 볼 법한 것을 선호하는 것이다.

TCA는 아직 그 정도는 아니란 생각이 든다. 웹에서는 리덕스를 보통 사용하지 않는다고 하는데, 구조 자체가 너무 복잡해지는 경향이 강해 보여서인듯하다.

그리고 이런 이벤트 처리에 있어서 딱히 장점이 보이지 않는 것도 이유이기도 한데, delegate를 통해 이벤트를 받아서 처리할 수 있는 편함은 있지만 이 이벤트 통로는 앱의 하나라고 생각한다면, 실수로 구현하지 않을 수도 있고, 실수로 구현하지 않아도 이상하게 동작하게 되는 케이스도 발견될 가능성이 커 보인다.

그럼 어떻게 디버깅이 편할까? EventBus의 디버깅이 어려웠던 것처럼 TCA 역시 비슷한 느낌을 받았다.

그렇다고 해도 재미있는 아키텍처임은 틀림없고, UDF를 잘 구성하여 고민해 본다면 충분히 좋은? 아키텍처라고 생각한다.

생각보다 매우 재미있었기에 이런 글도 작성해 본다.



About Taehwan

My name is Taehwan Kwon. I have developed Android for 6 years and blog has been active for eight years.

Comments