안녕하세요 🙇🏻♂️
SwiftUI를 공부하다보니 SwiftUI의 디자인 패턴에는 무엇이 있을까 하다가
TCA를 발견해 포스팅 해보려고 합니다.
포스팅은 편의상 편의 말투로 진행함다 😎
개요
프로젝트를 진행하려고 프로젝트 설계를 하고 있었는데 문뜩 드는 생각이 UIKit은 RxSwift+MVVM을 채택하여 프로젝트를 구현 했었는데, SwfitUI도 MVVM패턴을 적용하면 되는건지 궁금했음.. 🧐
근데 다행히도 나와 같이 생각하는 사람들이 꽤나 많았음 ㅋ
MVVM이 불필요하다는 여러 개발자들의 의견이 있다!
- 애플 디벨로퍼 포럼을 포함한 다양한 포럼에서 개발자들은 SwiftUI에서 MVVM을 사용하는 것이 불필요하다고 주장하고 있음.!(ref. https://developer.apple.com/forums/thread/699003)
- 위 내용은 SwiftUI를 쓰면서 MVVM을 사용하는 것은 "날 수 있는 보드에 바퀴를 굳이 달아 달리게끔 변경하는 것"과 같다고 말한다."
- 즉 SwiftUI에서는 사실 Model과 View가 결합된 MV면 충분할 수 있다. (Controller 혹은 ViewModel이 필요 없다)
그래서 TCA가 뭔데?
TCA란 The Composable Architecture의 준말로
구성 가능한 아키텍처(TCA, 줄여서)는 구성, 테스트 및 인체 공학을 염두에 두고 일관되고 이해할 수 있는 방식으로 응용 프로그램을 구축하기 위한 라이브러리입니다. SwiftUI, UIKit 등, 그리고 모든 Apple 플랫폼(iOS, macOS, visionOS, tvOS 및 watchOS)에서 사용할 수 있습니다.
공식 문서의 정의를 번역 해서 인용했다.
MVVM VS TCA 동작 방식 비교
MVVM 동작 방식
이러한 방식이고 앞서 말했듯이 SwiftUI를 쓰면서 MVVM을 사용하는 것은 "날 수 있는 보드에 바퀴를 굳이 달아 달리게끔 변경하는 것"과 같다고 말한다." 했는데 그 이유는 SwiftUI는 View에서 @State와 @Binding 프로퍼티 래퍼를 사용해 데이터에 접근할 수 있는데 굳이 ViewModel을 거쳐서 로직과 패턴을 구현하기엔 too much라는 의문에 TCA가 생겨난 것이 아닐까 싶음 ☺️
또한, SwiftUI가 지향하는 데이터의 단방향 프로우와는 사뭇 다름!
TCA 동작 방식
TCA의 구성 요소
- State
비즈니스 로직을 수행하거나 UI를 그릴 때 필요한 데이터의 집합. 기존에 @State로 감싸인 데이터 변수와 비슷 - Action
사용자에 의해 뷰에서 발생하는 모든 action과 API호출 결과를 나타내는 타입. - Dependency
API 클라이언트를 비롯한 외부 시스템과 같이 어플리케이션이 필요로 하는 의존성(Dependency)을 가지고 있는 타입. - Effect
네트워크요청, 디스크에서 저장/로드, 타이머 생성, Dependency와 상호 작용과 같은 작업을 수행하는 Reduce()의 리턴값. 통신의 결과. - Reduce
Action을 전달받아 이를 처리한 결과로 State의 상태를 변경해 UI를 업데이트하는 로직. API 요청과 같은 이벤트를 Effect와 Dependency를 이용해 실행하고, 실행한 결과는 Action으로 다시 Reducer에 전달된다. - Store
실제로 기능이 작동하는 공간. Action을 트리거로 Store는 Recuer와 Effect를 실행할 수 있고, 이로 인해 Store에서 일어나는 State 변화로 UI를 업데이트 함.
ref. https://sujileelea.tistory.com/6
눈치 챈 사람도 있겠지만 ReactorKit의 동작 방식과 거의 유사하다는 것을 확인 할 수 있음 😎
TCA 간단 예제
TCA 국룰 예제로 보이는 + 버튼, - 버튼으로 값을 변경하는 예제를 만들자 ㅋ
Reducer 구현
@Reducer
struct Feature {
struct State: Equatable {
var count = 0
}
enum Action: Equatable {
case plusButtonTap
case minusButtonTap
}
func reduce(into state: inout State, action: Action) -> Effect<Action> {
switch action {
case .plusButtonTap:
// 더하기 작업 구현
state.count += 1
return .none
case .minusButtonTap:
// 빼기 작업 구현
state.count -= 1
return .none
}
}
}
@Reducer 매크로를 사용해 구조체를 한다!
해당 struct는 Reducer 프로토콜을 준수함!
위 TCA 구성요소에서 살펴봤던 State와 Action을 확인할 수 있음.
이때 State는 반드시 Equatable 프로토콜을 준수해야함
body에서 Redcuer가 State와 Action을 인자로 받아
action으로 State의 count를 변경하는 것을 확인할 수 있음
SwiftUI View 구현
store를 let으로 선언하는 부분에서 데이터의 단방향 플로우를 볼수 있음
view에서는 Action만 Reducer로 보낼 뿐 State를 직접 변경할수 없으셈
//
// ContentView.swift
// TCA-Example
//
// Created by ukseung.dev on 8/9/24.
//
import SwiftUI
import ComposableArchitecture
struct ContentView: View {
let store: StoreOf<Feature>
var body: some View {
WithViewStore(store, observe: { $0 }) { viewStore in
HStack {
Button(action: { viewStore.send(.minusButtonTap) }) {
Text("-")
.font(.system(size: 25))
.padding(30)
.foregroundColor(.blue)
.cornerRadius(8)
}
Text("\(viewStore.count)")
.font(.largeTitle)
.padding()
Button(action: { viewStore.send(.plusButtonTap) }) {
Text("+")
.font(.system(size: 25))
.padding(30)
.foregroundColor(.blue)
.cornerRadius(8)
}
}
.padding()
}
}
}
#Preview {
ContentView(store: Store(initialState: Feature.State(), reducer: { Feature() }))
}
구현 결과
결론 및 느낀 점
SwiftUI+TCA에 대해 알아보았고 ReactorKit과 굉장히 유사한 구조이기 때문에 활용 하는데 큰 어려움을 겪지는 않았고
ReactorKit과 동일하게 UnitTest에 굉장한 강점을 가지고 있기 때문에 나중에 테스트코드도 짜서 활용 해볼 예정🫡
소스코드
'iOS > SwiftUI' 카테고리의 다른 글
[SwiftUI] SwiftUI 레이아웃 기초, Stack(VStack, HStack, ZStack) (0) | 2022.09.29 |
---|