ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [포케덱스] - TCA 적용기
    프로젝트/포케덱스 2025. 8. 20. 19:52
    728x90
    반응형

    오늘은 포케덱스 배포를 완료한 기념으로 지난 SwiftUI+MVVM 버전에서 SwiftUI+TCA로 리펙토링하게된 모든 전기를 작성해볼 것이다.

     

    TCA를 알게된 것은 작년이다.

    하지만 당장은 아키텍쳐적인 부분보다 기본기와 기존 앱들의 성능과 코드 컨벤션 개선을 하는게 우선순위가 더 높아보여서 보류 중이였다.

    그러다가

    https://gist.github.com/unnnyong/439555659aa04bbbf78b2fcae9de7661

     

    swiftui_mvvm.md

    GitHub Gist: instantly share code, notes, and snippets.

    gist.github.com

     

    해당 글을 보게 됐는데, SwitUI+MVVM은 어울리지 않는다라는 내용의 글이다.

    정리하자면 이렇다.

     

    기존 SwiftUI+MVVM의 문제점

     

     

    1. SwiftUI 자체가 State-Driven 아키텍처

     

    SwiftUI의 View는 상태와 밀접하게 연결되어 있어, 상태 변화가 곧 UI 갱신을 의미한다.

    이미 스유를 사용하는 것 만으로도 @State, @ObservedObject, @EnvironmentObject, @Binding 같은 속성 래퍼가 존재해서 ViewModel 같은 별도 계층 없이도 데이터 흐름을 관리할 수 있게 설계되어 있다.

     

    기존에 UIKit은 이런 프로퍼티 래퍼가 없어서 바인딩을 하기 위해서는 프로퍼티 옵저버나 콜백을 사용하거나 RxSwift나 Combine같은 리액티브를 도입해야하는 작업이 필요했다. 스유에서는 프로퍼티 래퍼하나로 이것이 만족되버린 것이다.

     

    따라서 “View ↔ ViewModel ↔ Model” 구조로 한 번 더 감싸는 것이 중복된 추상화가 되어버릴 수 있다는 것이다.

     

     

     

    2. ViewModel과 View의 경계가 모호해짐

     

    MVVM에서는 ViewModel이 UI 로직을 담당하고 View는 단순 표현만 하는 게 이상적이다.

    하지만 SwiftUI는 View 안에서 직접 로직을 표현하기 좋은 DSL 구조를 갖고 있다. 예를 들면 .onAppear, .task, .refreshable 같은 첨자를 사용하는 것이다.

     

    이런 특성 때문에 ViewModel에 둘 로직이 다시 View 쪽으로 흘러 들어와 책임 분리가 애매해지고, 오히려 MVVM이 무겁게 느껴질 수 있다.

     

    3. 테스트 이점이 약해짐

     

    MVVM의 장점 중 하나는 ViewModel을 독립적으로 테스트할 수 있다는 점이다.

    하지만 SwiftUI에서는 View와 State가 강하게 묶여 있어 테스트의 단위가 ViewModel 단독으로는 의미가 줄어든다.

     

    뭐 비즈니스 로직에 해당되는 상태(예를 들어 서버에서 받아온 데이터나, 로컬 DB 데이터를 담는 상태 변수)같은 경우는 ViewModel에 있는게 맞지만, View의 컴포넌트의 상태를 나타내는 변수(예를 들어 TextField나 Picker)같은 경우는 상태의 소유권이 ViewModel이 아니라 View에 있어야하기 때문에 이런 상태까지 ViewModel이 관장하기엔 다소 무리가 있다.

     

     

    어느곳에서는 비유를 이미 바퀴가 달린 보드에 날개까지 달아버리는 것이라 비유했다..

     

    이런 이유들로 기존에 SwiftUI+MVVM은 보일러플레이트가 잔뜩낀 아키텍쳐라고 생각이 바뀌게 되었고, 다른 아키텍쳐를 고안해보던 중 TCA가 생각이 났다.

     

    TCA 도입 이유

     

    1. 단방향 흐름 준수 용이

     

    TCA는 단방향 흐름을 강제하고 각각의 상태와 액션에 따라 어떤 로직을 수행할 것인지 분리하기 용이하다.

     

    View → Store(에 Action 전송) → Reducer(상태 변경/Effect 방출) → Effect(비동기) → Action 재전송 → Reducer → State 이렇게 한 방향으로만 흐르니까 상태 소유권과 변경 경로가 명시적이고 추적이 쉽고 또한 “이 값은 어디서 바뀌지?”가 아니라 “이 액션이 이 리듀서에서 이렇게 바꾼다”로 바로 찾을 수가 있다.

     

    2. 상/하위 Feature의 조립 용이

     

    TCA는 상위 Feature는 scope(state:action:)로 하위 Feature를 조립한다.

    View는 Store만 알고있고 사용자 인터랙션을 Action으로 보내는 역할까지만 하고 View의 로직은 Feature에 두어 단일 책임을 지키고, 상태의 스코프를 명확히 유지한다.

     

    3. 테스트의 간편함

     

    TestStore입력 액션 → 상태 변화/효과 → 후속 액션시간 순서까지 검증할 수 있다. MVVM에서 놓치기 쉬운 디바운스/중복호출/취소/리트라이 같은 비동기 시나리오를 재현·단정하기 용이하다.

     

    TCA를 진행하며 테스트 코드를 작성해보고 있는데, 그 중 한 부분을 들고왔다.

    서버에서 받아올 데이터를 Mock으로 만들어 정의하고

    TestStore로 테스트가 필요한 Feature를 만들어 만들어둔 Mock을 넣어서 Store를 만들어준다.

    그 Store를 바탕으로 테스트가 필요한 액션을 보내고 데이터를 업데이트하고 리시브로 해당 데이터를 받는지 테스트할 수 있다.

     

    그러면 도입 후 어떻게 됐는지 확인해보자.

     

     

    TCA 도입 후

     

    솔직히 도입 후 프로젝트를 거의 새로 만들다시피 리펙토링을 했다.

    하지만 워낙 러닝커브가 높아 아직도 더 공부하고 파고들어야한다. 그래서 기존 MVVM 로직과 TCA 코드의 로직을 비교해보기로 했다.

     

    MVVM 로직

     

    MVVM 구조를 처음 도입했을 때는 View와 ViewModel, 그리고 Manager 계층 간의 역할을 나눈다는 점에서 분명한 장점이 있을 것이라 기대했다. 하지만 실제 개발이 진행되면서 여러 문제점이 드러나기 시작했다.

     

    가장 큰 문제는 뷰에서 직접 로직을 처리하게 되면서 테스트가 어려워졌다는 점이다. 단일 책임 원칙이 무너지게 되었고, 로직이 View와 ViewModel에 뒤섞여 존재하다 보니 유지보수도 힘들어졌다. 특히 View가 직접 NetworkManager나 RealmManager와 같은 매니저 객체를 의존하게 되면서 관심사의 분리가 제대로 이루어지지 않았다.

     

    또한 ViewModel과 View가 1:1로 대응되지 않고, ViewModel이 여러 뷰에서 공유되는 경우도 많았다. 일부 로직은 여러 뷰에서 공통으로 사용되기 때문에 ViewModel 내부에 억지로 넣거나, @EnvironmentObject 같은 공유 객체를 이용해 강제로 상태를 주입하는 방식으로 해결해야 했다. 이런 구조에서는 특정 기능 하나만 수정해도 해당 로직이 흩어져 있는 여러 위치를 추적하고 변경해야 하므로 시간도 많이 소요되었다.

     

    결과적으로 View와 ViewModel 사이의 역할이 불명확해지면서 “로직을 View에 둬도, ViewModel에 둬도 어차피 동작하긴 한다”는 애매한 상황이 생겼고, 그렇게 되면 결국 ViewModel의 존재 이유가 사라지고 모든 로직이 View에 집중되기 쉽다. 이는 테스트 가능성을 아예 차단하고, 유지보수성도 현저히 떨어뜨리는 구조가 되어버린다.

     

    이 모든 문제는 MVVM이라는 패턴 자체의 문제가 아니라, 그 패턴을 어떻게 설계하고 운영했는지에 따라 달라진다고 생각한다. 하지만 분명한 것은 View와 ViewModel의 책임이 모호한 구조에서는 어떤 로직이 어디에 있어야 할지 기준이 흐려지고, 결국엔 구조 전체가 무너지기 쉽다는 것이다.

     

    이러한 문제를 해결하기 위해 나는 이후에 TCA(Composable Architecture) 구조를 도입하게 되었다.

     

     

    TCA 로직

     

    조금 더 복잡해진 것 같지만, 하나씩 뜯어보면 전혀 복잡하지 않다.

     

    기존 MVVM에서는 View와 ViewModel이 1:1로 대응되지 않아 View 쪽의 로직이 점점 커지는 문제가 있었는데, 이번 구조에서는 각 View에 딱 맞는 Feature가 대응되도록 하면서 massive해지는 문제를 말끔히 해결했다.

     

    View는 이제 로직을 전혀 모른 채 오직 Store를 통해 액션을 보내기만 하면 된다. 이 액션은 Feature 내부의 Reducer로 전달되고, 거기서 액션의 종류에 따라 로직이 분기된다. 필요한 경우 Effect가 반환되고, 이에 따라 State가 업데이트되며, View는 변경된 상태를 바인딩만 해주면 된다. 이렇게 해서 완벽한 단방향 흐름이 구현된다.

     

    이 단방향 흐름을 강제한다는 게 정말 큰 장점인 것 같다. 오랜만에 내가 작성한 코드라도 TCA 구조만 이해하고 있다면 필요한 부분을 금방금방 찾을 수 있어서 유지보수도 훨씬 쉬워진다.

     

    또 앱이라는 건 필연적으로 사이드 이펙트가 발생할 수밖에 없는데, 이건 외부 세계(서버, 로컬 저장소 등)에서 벌어지는 일이라 Feature 안에 직접 구현하면 안 된다. 그래서 외부 세계와 통신하는 Client를 따로 만들고, 이를 DependencyValues에 등록해서 주입받도록 했다. 그러면 Feature에서는 @Dependency를 통해 해당 클라이언트를 읽어오고, 이렇게 해서 자연스럽게 의존성 역전도 이뤄낼 수 있다.

     

    TCA를 연구해봤지만 아직 모르는게 많다. 그리고 사실 이런 아키텍쳐를 떠나서 내가 잘못한 부분도 많이 발견하는 것 같다.

    앞으로 열심히 연구해 내것으로 만들어버리고 싶다..

Designed by Tistory.