본문 바로가기

iOS

TCA(리덕스패턴)과 MVVM, 그리고 MVC [1]

TCA로 만든 앱을 출시한 이후, RxSwift로 처음 프로젝트를 만들며 문득 든 두가지 생각이 있다.

 

1. Combine과 RxSwift를 이용한 MVVM 패턴도 데이터흐름을 이벤트 스트림으로 단방향으로 보내는 패턴인데...?

2. 그 코드의 양이 캡슐화로 인해 줄었을 뿐 MVVM과 TCA 아키텍쳐 또한 일종의 신호를 뷰에서 보내줘야 하는 것 아닌가...?

 

이 두 가지 질문에 대한 답을 공부하면서 MVC, MVVM, TCA에 대한 필자 나름대로 정리가 크게 되었다. 독자님들도 읽어보시면서 세가지 아키텍쳐 패턴에 대한 나름대로의 정립을 하셨으면 좋을 것 같다!!

먼저, 2번에 대한 답을 같이 알아보자!

 

  • 그 코드의 양이 캡슐화로 인해 줄었을 뿐 MVVM과 TCA 아키텍쳐 또한 일종의 신호를 뷰에서 보내줘야 하는 것 아닌가...?(단방향에 대한 근본적인 의문)

 

단방향 아키텍쳐 패턴으로 개발을 하다보면 뷰모델 혹은 뷰스토어에 데이터를 넘겨주는 코드를 작성해주는 스스로를 보면서 사실 MVC패턴처럼 MVVM과 TCA도 뷰가 뷰모델 혹은 뷰스토어와 소통을 하고있는 것이 아닌가..? 하는 아이러니함에 빠지게 된다. 그리고 이러한 생각이 가장 강하게 든 순간이 '바인딩'을 만났을 때이다!!!

최근에는 공식문서의 @BindableState의 소개와 함께 많이 발전한 형태를 띄긴 하지만, TCA의 공식문서를 과거의 문서부터 위로 올라오며 읽던 나는 옛날형태의 코드로 작성을 하였고, 그 경험은 나에게 위의 질문과 같은 의심을 안겨줬던 것이다.

예를 들자면,

struct State {
	var bool = false
}

enum Action {
	case changeBool(Bool)
}

let reducer = Reducer<State, Action, Environment> { state, action, environment in
	switch action {
    case let changeBool(isOn):
    state.bool = isOn
    return .none
    }
}

 

위와 같이 뷰스토어를 구성시켰다고 해보자,

 

그리고 뷰에서 이 불값을 바인딩해서 뷰에서 토글로 보여주고자 한다면,

 

Toggle(
  "Change Boolean Value",
  isOn: viewStore.binding(
    get: \.bool,
    send: Action.changeBool
    )
)

 

이와 같은 코드로 나타나게 된다.....이 때 나에게 binding(get: , send: )가 굉장히 큰 위화감이 들었다. 개인적으로는 마치 MVC의 View가 Controller에 명령을 내리는 부분과도 같은 역할을 하고있는 느낌이 들었다.

 

예를들어, 

class Model {
    var bool: Bool = false
}

class View: UIView {
    var switchControl: UISwitch!

    func setup() {
        switchControl = UISwitch()
        switchControl.addTarget(self, action: #selector(switchValueChanged), for: .valueChanged)
        addSubview(switchControl)
    }

    func displayBoolValue(_ value: Bool) {
        switchControl.isOn = value
    }

    @objc func switchValueChanged() {
    // #selector에 들어갈 메서드(사용자가 토글 스위치를 변경할 때 호출되는 메서드)
    // 변경된 값을 컨트롤러에 알림
    let newValue = switchControl.isOn
    // 뷰가 직접 모델에 접근하여 상태를 변경하는 부분
    view.controller?.updateBoolValue(newValue)
    }
}

class Controller {
    let model: Model
    let view: View

    init(model: Model, view: View) {
        self.model = model
        self.view = view
    }

    func updateBoolValue(_ value: Bool) {
        model.bool = value
    }

    func updateView() {
        view.displayBoolValue(model.bool)
    }
}

 

위와 같은 예시코드(예시를 위한 코드이기 때문에 틀렸을 수도 있는 점 양해 부탁 드립니다..)에서 addtarget에 들어가는 메서드의 역할을 한다고 느껴진 것이다!!!!!!!!!!!!!!!!!!!(진짜 저만 그런가요????)

 

그래도, 제일 최근에 업데이트 된 TCA의 코드로는 다음과 같이 변경된다.

 

struct Feature: ReducerProtocol {
	struct State: Equatable {
    	@BindableState var bool = false
    }
    
    enum Action: BindableAction {
    	case binding(BindingAction<State>)
    }

	func reduce(into state: inout State, action: Action) -> Effect<Action,Never> {
    	switch action {
    	case .binding:
    		return .none
    	}
    }
    .binding()
}

 

 

TCA의 최신 코드로만 최대한 작성해보았다. 위 코드에선, ReducerProtocol(액션이 주어질때 현재 상태에서 다음 상태로의 전환되는 방법을 설명하고 후에 Store에서 실행해야하는 Effect를 설명하는 프로토콜)로 선언을 해주고 @BindableState( 구 @BindingState) 로 불값을 선언해준다.

 

그리고 뷰에서는

 

Toggle("changeBool", isOn: viewStore.$bool)

 

이와 같이 바인딩해준다.

 

공식 문서에서는

BindableAction 프로토콜 덕분에 더이상 바인딩이 되고 있는 케이스를 구체화하지 않아서 간단해진다고 하며,

(With this protocol defined, we can simplify the reducer because we no longer have to specify the binding case explicitly)

 

뷰에서는 액션이 유추될 수 있기 때문에, 원래 SwiftUI에서의 간결함에 더욱 부합하는 구문을 도입할 수 있게 되었다고 한다.

(But the real gains are seen in the view. Because the action can be inferred, we can adopt a syntax that matches the conciseness of vanilla SwiftUI!)

 

물론 개인적으로는 다음과 같이 변하면서, 이전의 의심을 살짝 지워줄 순 있는 형태로(겉보기엔) 바뀌었는데, 이것도 사실 웃기긴 하다ㅋㅋㅋ State와 Binding이 합쳐진 끔찍한 혼종이라니....ㅋㅋㅋㅋ

그래도 잘 생각해보면 이게 조금 더 TCA의 단방향 아키텍쳐의 특징에 맞게 잘 변한 것 같다고 느낀다. 필자가 느낀 바로는, 뷰에서는 State 변화를 위해 액션을 '트리거만' 하고, 그 액션이 Store에 전달되면 Reducer에 의해 상태를 변화시켜주는 단방향 구조가 더 잘 느껴지는 느낌...? 이전에는 상태를 받고 액션을 호출해주는 부분이 마치 단방향 아키텍쳐가 말장난처럼 느껴지는 의심을 안겨줬었다.

 

그러나 MVC 예시를 들기위한 UIKit 코드는 view.controller?.updateBoolValue(newValue) 와 같이 뷰에서 컨트롤러, 모델에 접근하고 있는 것을 보면 양방향성이 더 짙은 것을 알 수 있었다.

 

개인적으로는,
TCA는 뷰가 상태를 변경하는 액션(Action)을 생성하고 이를 전달하는 방식으로 신호를 보내니까 단방향, MVC에서는 뷰가 직접 모델에 접근하니까 양방향.
이라고 심플한 결론을 내려보았다. (댓글로 많이 반박해주고 알려주세요! 기다리고 있습니다!!)

 

1번에 대한 대답은 글의 양이 길어져 나누어 작성하였습니다.