TCA를 적용하면 느낀점...

kio·2023년 6월 15일
0

Project

목록 보기
1/2
post-thumbnail

TCA란

The Composable Architecture 로써 Pointfree에서 만든 아키텍쳐이자 Library입니다.
또 내부적으로 Combine으로 구성되어있습니다.

TCA를 적용한 이유

mappin이라는 프로젝트를 하면서 Clean Architecture를 적용하기로 했다. 규모가 크지 않아 오히려 파일의 양이 비대해지고 쉽게 코드를 파악할순 없었지만, 배움의 효과와 직접 프로젝트에서 해보고 싶어서 적용했다.
또 SwiftUI를 적용시키기로 내정했을때, 처음엔 MVVM을 활용해서 VM을 만드려고 했지만, 재미있는 아티클을 읽었다.

글을 요약하자면 이 정도이다.


선언적 UI에서 VM의 필요성

SwiftUI는 선언적UI로써 이는 이미 VM의 의미를 상실했다.

어느덧 MVVM이 당연시 되고 있는 모바일 앱에서 VM은 로직을 모아둔 묶음에 불과하다.
즉, 우리가 선언적UI에서 필요한 것은 단방향 데이터플로우이다.


단방향 데이터 플로우와 상태관리를 해주는 것을 몸으로 느껴보고 공부하기 위해 TCA를 적용하기로 했다.
실제로 작은 프로젝트라 View와 Model 정도로도 충분히 해결할 수 있겠지만 적용을 하면서 배워가자는 느낌으로 시작했다.
충분한 이유도 아니고 단지 VM은 SwiftU에 장점을 죽이면서 시작한다는 점과 단방향플로우가 협업에서 작업의 속도를 높여줄 수도 있을 것 같다는 기대감으로 TCA를 적용했다.
(결과적인 얘기지만 처음에는 엄청 해매다가 나중에가서 TCA의 덕으로 새로운 기능의 추가를 쉽게 쉽게 했다.)

TCA를 적용하면서..

1. UIRepresentable

우선 MapKit을 사용해서 현재 지도의 가운데 점, 축척, 회적각도 등이 필요했는데 View component인 Map은 위와 같은 기능을 지원하지 않아 MKMapView를 사용해야 했다.

지도의 delegate를 통해 얻어온 정보를 Reducer에 보내고 해당 정보를 통해 가공된 state가 내려와야 하는데 어떤 값이 바뀌더라도 updateUIView가 호출되기 때문에 그 안에서 다르게 처리해줘야 했다.

enum Action: Equatable {
        
    case none
    case responseUpdate([PinCluster]) // 결과값을 들고 오는
    ...
    case showUserLocation(Bool)
}

물론 시간이 없어서 중간 action들이 흐름에 맞춰 없어져도 되는 부분들이 있지만, 급해서... 나중에 리팩토링해야겠다.

그래서 이걸 State에서 아래와 같이 선언하고

struct State: Equtable {
	var mapAction: MapView.Action = .none
}

위와 같이 updateUIView에서 mapAction의 변화를 감지하고 아래처럼 각 로직을 처리한다.

func updateUIView(_ mapView: MKMapView, context: Context) {
        
    switch store.mapAction {
    case .none:
        break
	    
	...	        
    
    default:
        break
   }
}

이렇게 되면 각 상태에 맞게 지도의 정보를 수정하거나 지도에서 특정 데이터를 가져올 수 있다.

2. Reducer의 분리?

우리는 한 화면에 많은 기능이 필요했다. 이게 어떻게 보면 HIG나 좋은 디자인은 아닐 수 있지만 interaction을 줄이자는 목표에 따라 분리했다.
크게 2가지로 나눴다.

  • 지도의 상태, 지도와의 상호작용을 관리하는 Store
  • 음악의 검색, 선택을 위한 Store

사실 이 2가지를 나눌때만 해도 Reducer끼리의 액션이나 동작이 자유롭게 서로 상호작용할 줄 알았다.

이미지

하지만 위 그림과 TCA의 흐름, 단방향이라는 특징은 변화한 State가 View도 업데이트할 수도 있고, 어떨 땐 다른 Store의 트리거도 할 수 있다면 이는 단방향이라고 하기엔 뭔가 애매한 구석이 있는 것 같기도 해서
결론은 Store끼리의 상호작용은 편하게 지원하지 않는다!

물론 위와같은 이유인지 아니면 TCA의 부족한 점인지 모르겠지만 나의 생각으로 그렇다.
하지만 이미 쏟아진 물 우리는 서로 다른 Reducer의 State변화를 감지해서 상호작용 시켜줘야했다.

struct TCABindView<SendEntityType>: View {
    
    var sendEntity: SendEntityType?
    var content: (SendEntityType?) -> Void
    
    var body: some View {
        EmptyView()
            .onChange(of: sendEntity) { sendEntity in
                content(sendEntity)
            }
    }
}

급한대로 만든 두가지 Store에 상호작용을 위한 TCABindView를 만들었다.

동작원리는 보내려는 값이 변경되면 onChange가 값을 읽고 해당 completion을 호출한다.

사용법은 간단하다.

var body: some View {
	TCABindView(store.state.someEntity) { entity in
		anotherStore.send(.someAction(entity))
	}
}

뭐 이런식으로 사용하면 된다...
이게 좋은 방법인지 이게 TCA를 어긋나는 행동인지는 조금 더 공부하고 해당 사항을 해결하는 더 좋은 방식을 생각해서 다시와야할 것 같다.

3. 설계가 어려웡

사실 기간이 짧아서 정확한 계획서를 세우고 스펙을 정확히 파악하지 못하고, 계속 계획이 바뀌는 바람에 처음에 설계하면서 코드를 짰던 모습과 많이 변했다.
나는 MVVM과 TCA를 보면서 느낀점을 뭔가 축구의 전술과 같았다.
MVVM은 뭔가 포지션에 얽매이지 않고 자유로운 느낌이고, TCA는 자기의 정확한 포지션을 지키면서 짧은 패스를 하는 느낌이었다.
이렇게 말하니 MVVM이 좋아 보이지만 그럴수도 아닐 수도 있다.
지금까지 나는 VM을 어쩌면 로직의 묶음정도로 사용해왔어서 그런 것일 수도 있다.
설계가 비교적 쉬운 편이고 한번 짠 코드는 크게 고치지 않으면서 앞으로 갈 수 있었다. ( 이건 좀더 익숙해서 그런 것일 수도 있다.)
하지만 TCA는 State를 변경하고 완료되면 다른 action을 부르고 하는 패스들의 연속이다.
그렇게되면 각 패스가 완성도가 높아야된다. 그렇지 않으면 흐름이 엇나가게 된다.

실제로 위에 mapAction은 mapAct라는 action으로 관리하고 있었는데, 이는 mapAction이 불리기만하는 action이 필요한 순간이 있었다.

enum Action: Equatable {
        
        case mapAct(MapView.Action)
        case mapActAndChange(MapView.State)
        ...
}

func reduce(into state: inout State, action: Action) -> EffectTask<Action> {
    switch action {
            
    case .mapAct(let newState):
	    switch newState {
		    case .none:
			break
		    ...
	    }
        return .none
    }
	case .mapAct(let newState):
		state.mapAction = newState
		
	    switch newState {
		    case .none:
			break
		    ...
	    }
        return .none
    }
}

사실 이것은 뭔가 이상하다.
action안에 너무 다양한 처리가 존재한다.
마치 SRP를 어긴 것처럼 state를 변경하는 부분과 그 해당 mapAction에 맞는 로직을 처리하는 것 두가지가 존재한다.
하지만 이를 잘 바꾼다면 다음과 같아야한다.

enum Action: Equatable {
        
        case mapAct(MapView.Action)
        case mapActionChange(MapView.State, willAct: Bool = false)
        ...
}

func reduce(into state: inout State, action: Action) -> EffectTask<Action> {
    switch action {
            
    case .mapAct(let newState):
	    switch newState {
		    case .none:
			break
		    ...
	    }
        return .none
    }
	case .macActionChange(let newState, let willAct):
		state.mapAction = newState
		if willAct {
			return .send(.mapAct(newState))
		}
		return .none
    }
}

자 내가 말한 짧은 패스가 뭔지 알겠죠?
이렇게 짧은 패스를 어느정도로 나눌지 이렇게 어렵다.
나는 심지어 각 mapAction도 나눠야하나 고민했을 정도이다.

느낀점

1. TCA의 장점이 스멀스멀 냄새가 난다.

TCA는 많은 부분이 제한적이고 자유롭지 못하다.
하지만 이 말이 결코 프로그래밍을 제한하진 않는다. (최소 우리 스펙에선..)
그랬을때, 장점이 보이는데...

  • 진짜로 흐름이 너무 잘보인다. 진짜 눈으로만 따라가도 너무 잘된다
    - 거기에 우리가 조금 더 설계를 잘 했으면 action이름만 듣고도 뭔지 알 수 있을거 같아... 별다른 생각을 안해도 될정도...
  • 강제성은 일정한 규칙을 만든다.
  • 한번해보고 적응하게되면 진짜 유용하게 잘 사용할 수 있을것 같다,

2. 설계는 어렵고, 의도를 알기 위해선 공부를 해야한다.

물론 Combine을 사용해 충분히 확장가능성을 열어뒀지만 아직 공부가 부족해서인지 어떻게 사용해야 하는지 파악도 잘안되고, TCA에 대한 자료자체도 부족해서 조금 힘들었지만 Redux를 공부하고, 함수형 프로그래밍을 공부하면서 점점 개발자의 의도를 알게 된 달까? 그랬던 좋은 경험이었던것 같다.

1개의 댓글

comment-user-thumbnail
2023년 7월 4일

재밌네요!

답글 달기