처음에 UIkit을 했을때는 MVC 패턴이였다. 하지만 점점 ViewController가 하는게 많아지고 양방향이기에 유지보수도 힘들어지고 테스트도 어려웠다. 그래서 점차 MVVM 패턴으로 바뀌고 실제로 나의 사이드 거닐다 프로젝트에서는 MVVM + Combine으로 프로젝트를 하였다. ViewModel에서 input과 ouput을 View와 binding하여 뷰를 업데이트하기에 코드의 양도 줄일 수 있고, 뷰모델이 뷰와 독립적인 코드의 구조로(뷰모델이 뷰에 대한 의존성이 없다), 자체 테스트도 용이해진다.
하지만 SwiftUI를 MVVM으로 했을때는 항상 팀원하고 이야기하던게 있었다. 우리가 하는 것이 진짜 MVVM이 맞는가..?
ViewModel은 State-binding을 통해 관리를 해주었지만 swiftui로 프로젝트를 해본 사람들은 경험한 이미 선언형 UI이기 때문에 자체적으로 @State, @StateObject등과 같은 annotation이 있어 뷰에서 상태관리를 해주기에 뭔가 혼란스럽다. 뷰에서 이미 상태까지 가능한데 굳이 또 뷰모델을 추가하여 하나를 더 거친다는 느낌을 받았다. 이로써 양방향 데이터 흐름이 이루어진다.
이전에 UIKit에서는 단지 뷰에서 어떠한 액션을 뷰모델에 보내고 뷰모델에서 처리를 하고 결과를 뷰로 이벤트를 방출만!! 한다. 이로써 단방향 통신이였다. viewDidLoad에서 바인딩해주고 기다리다가 오면 뷰 스스로 변경하는 형식이였다.
하지만 스유에서는 기존의 뷰에서 가능한 것을 굳이 뷰모델로 나누면 저렇게 더 코드의 양이 많아진다. 또한 이전에 말한거처럼 데이터흐름을 양방향으로 만든다. 실제 내가 이전에 했던 Wote프로젝트 코드만 봐도 가독성이 떨어진다! 또한 environmentObject 까먹고 주입못했을땐 런타임에러가 발생한다. 물론 내가 주니어라 못한거 일수도 있다..ㅠ 물론 이후로는 MVVM을 단방향으로 만드는 방법도 나왔다. 하지만 나는 이번기회에 SwiftUI에 단방향으로 잘 맞다는 TCA 아키텍처를 공부하고 이후의 프로젝트에 적용을 해보려한다.. 뒤의 프로젝트 진짜 엄청 큰거 옵니다.. 아마 3개월이내 나와요.
TCA(The Composable Architecture)
TCA흐름도이다. 그림을 통해 알겠지만 모든 흐름은 단방향이다! ViewModel대신 Store라는 녀석이 생겼다.
구성요소
- View: 유저가 어떠한 액션을 하는 뷰 (1. 사용자가 뷰를 통해 액션을 트리거한다.)
- State: UI에 바인딩할 데이터를 구조체로 정의한 것. 다른 Reducer와 데이터 연동이 필요시 다른 Reducer의 State를 추가해 데이터 연동까지 가능.
- 왜 Equatable protocol을 채택해야할까? SwiftUI는 상태가 변경되었을때 뷰 전체를 자동으로 업뎃한다. 이전 상태와 새로운 상태를 비교할 수 있으며 정확하게 변경되었는지 여부를 판단하기 위해! => 상태가 변경되지 않았을때 불필요한 UI 업뎃 방지
- State가 클래스와 같은 참조타입일때 내부 속성의 변화가 발생해도 스유는 이를 값의 변화로 간주하지 않을 수 있기때문에변화를 체크하기위해
struct State: Equatable{
var 안무: [노래춤] = []
var errorType: ErrorType?
var isLoading = false
var anotherState: AnotherReducer.State?
}
- Action: 뷰에서 발생하는 모든 이벤트를 정의한 것.
enum Action: Equatable {
case 춤추는버튼을누르다
case 서버로요청(food: FoodModel)
case 다른곳에있는거요청(AnotherReducer.Action)
}
실제 처리하는 곳은 Reducer. 또한 다른 Reducer의 액션을 처리할 수 있어 다른뷰와 데이터 연동을 뷰와 뷰 사이 데이터 전달 없이도 가능하게 한다! UI에서 수행한 작업의 이름을 그대로 짓는것이 네이밍에 좋다!
- Dependency: 주로 외부 통신이 있는 API.
- Reducer: ReducerProtocol을 채택하여 구현하고, 뷰와 연동되는 기본 로직을 한다. 전달받은 이전의 Action을 처리하여 Effect를 리턴
- 요즘은 @Reducer 매크로를 사용하여 구현한다.
- Reduce method로 구현할때
- . 다른 리듀서와 결합이 필요없을때, 추후 잘게 작은단위로 나눠질 여지가 없을때 (그 외는 body)로하자
- Effect: 네트워크 요청, 타이머과 같이 비동기 작업을 캡슐화하여 작업의 결과를 시간에 따라 방출(결국 너가 비동기작업을 하는거니..?) Effect작업이 완료되었을때 다시 Action을 해주니까
- .none: 액션에 대해 비동기처리 필요하지 않을때 사용
- .send: 파라미터로 Action을 받는 메서드. 특정 Action이후 추가적인 동기액션이 필요할때 사용, 주로 자식컴포넌트에서 부모 컴포넌트로 데이터 전달할때 사용
- .run: 비동기 작업 래핑하는 메서드
- .cancellabel(id:), cancel(id:): id는 Effect 식별값. Effect를 취소하게 해준다.
- .merge: 여러개 Effect들 동시 실행. 순서 보장X.
- if 춤춘다 액션을 받았을때 -> API 비동기로 요청하는 .task Effect를 리턴한다 -> 이전의 Dependency를 통해 API호출하고 결과를 다시 fetchDanceResponse Action에 보내준다 -> 여기서는 코드 안썻지만 다시 fetchDanceResponse에서 API 응답을 통해 State 안무를 업데이트 해준다.
struct DanceReducer: ReducerProtocol {
struct State: Equatable {
var 안무: [노래춤] = []
var errorState: ErrorState.State?
var isLoading = false
}
enum Action {
case 춤춘다
case fetchDanceResponse(Result<[노래춤]>,ErrorTypeValue>)
}
@Dependency(\.DanceClient) var danceClient
var body: some ReducerProtocol<State, Action> {
Reduce {state, action in
struct DancesCancelId: Hashable {}
switch action {
case .춤춘다:
guard !state.isLoading else {break}
state.isLoading = true
state.errorState = nil
return .task {
.fetchDanceResponse(await danceClient.fetch())
}.cancellable(id: DancesCancelId.self)
}
}
}
}
}
- Store: 기능을 실제로 동작시키는 런타임. 여기서 사용자의 액션을 통해 reducer, effect가 실행. 여기서 변화감지를 통해 UI업뎃. 작동하는 공간이라고 생각하면 된다
- ViewStore: Store를 관찰하며 Reducer에게 Action전달한다.
Store와 ViewStore등 더 딥한것은 다음에 정리를 해야겠다. 너무 길어진다.
느낀점ㅎㅎ
1. 상태관리. 데이터 플로우를 일관된 구조로 할 수 있다. 그래서 Swiftui에 더 잘 맞는 아키텍처라고 하는게 아닐까?
2. 그런데 이렇게 되면 SwiftUI의 프로퍼티인 @Published는 언제쓰지? viewModel로 했을때는 @Published를 통해 바인딩을 해주어 뷰에서 상태변화가 이루어지게 해주었는데 TCA는 Reducer에서 State관리를 하기에 사용할수가 있나?
-.. 이부분은 공부를 해봐야겠다.
3. 물론 하나의 Reducer아니 Store?안에서 State, Action, Reducer를 정의하지만 결국 그림으로봤을때는 뭔가 더 많이 단방향통신을 하는거 아닌가..? -> 작은 규모의 앱일수록 더 오히려 복잡해질거같은데..?
4. Side effect를 통해 에러처리가 쉬워질꺼 같다?
제가 느낀점중에 틀린거나 맞는거는 댓글에서 함께 얘기해봐요!저도 아직은 잘 모르겠어서 조금더 글을 보고 글을 써봐야 알꺼같아요
'SWIFTUI' 카테고리의 다른 글
TCA-3번째시간 Dependency (0) | 2024.07.19 |
---|---|
TCA(2)-Store, ViewStore& Binding (4) | 2024.07.16 |
go back to basic - @main 플젝만들면 항상생기는 파일 이건 몰까? (0) | 2024.03.01 |
some(Opaque Type)&any Keyword (1) | 2024.02.29 |
근본으로 돌아가자(4)-@State,@StateObject,@ObservedObject (2) | 2024.02.27 |