티스토리 뷰
지극히 저의 주관적인 생각입니다.
MVVM의 장점 : Testable하다. -> ViewModel이 View와 독립적이다!
MVVM
View에서의 Action을 ViewModel로 전달합니다. 이후 ViewModel은 해당 로직에 맞는 작업을 다른 Model에게 요청하고, 이의 응답을 ViewModel State에 업데이트합니다.
View는 ViewModel State를 구독하여 State의 변화가 있을 때 View를 업데이트합니다.
오늘도 +, - 버튼을 넣어보겠습니다. :)
저번 MVC글에서는 오토레이아웃을 적용했는데.. SnapKit없이 쓰기에 정말 간단한 예제임에도 layout관련 코드가 너무 늘어다더라구요..
그래서 이번엔 스토리보드를 사용하였습니다.
- View ( ViewController )
import UIKit
class ViewController: UIViewController {
@IBOutlet weak var countLabel: UILabel!
@IBOutlet weak var plusButton: UIButton!
@IBOutlet weak var minusButton: UIButton!
private let viewModel = ViewModel()
func bind() {
viewModel.countUpdated = { [weak self] count in
guard let self = self else { return }
self.countLabel.text = "\(count)"
}
}
override func viewDidLoad() {
super.viewDidLoad()
bind()
countLabel.text = "0"
plusButton.setTitle("+", for: .normal)
minusButton.setTitle("-", for: .normal)
plusButton.addAction(UIAction(handler: { [weak self] action in
guard let self = self else { return }
self.viewModel.plusButtonTapped()
}), for: .touchUpInside)
minusButton.addAction(UIAction(handler: { [weak self] action in
guard let self = self else { return }
self.viewModel.minusButtonTapped()
}), for: .touchUpInside)
}
}
먼저 ViewModel과 binding해줍니다. 말씀 드렸던 ViewModel State를 View가 구독하는 로직이라 생각하시면 될 듯 합니다.
(State가 변했을 시 어떤 View를 업데이트 할 지 미리 클로져로 지정해 둡니다.)
다음 View의 버튼이 눌림에 따라 ViewModel에게 어떤 액션이 일어났는지 알려줍니다.
- ViewModel
import Foundation
final class ViewModel {
var state: ViewState = ViewState() {
willSet(state) {
if let countUpdated = countUpdated {
countUpdated(state.count)
}
}
}
var countUpdated: ((Int) -> Void)?
func plusButtonTapped() {
state.count += 1
}
func minusButtonTapped() {
state.count -= 1
}
struct ViewState {
var count = 0
}
}
ViewModel에서는 View의 액션에 맞게 로직을 취한 후 State를 업데이트합니다.
그럼 끝!
이제 테스트를 위한 구조로 리펙토링 해볼까요?
- ViewModelType
protocol ViewModelInput {
func plusButtonTapped()
func minusButtonTapped()
}
protocol ViewModelOutput {
var countUpdated: ((Int) -> Void)? { get set }
}
protocol ViewModelType: ViewModelInput, ViewModelOutput {
var state: ViewModel.ViewState { get set }
}
ViewModel의 입, 출력을 나누었습니다.
또 ViewModel은 ViewModelType을 채택하도록 추가하였습니다.
private let viewModel: ViewModelType = ViewModel()
ViewController에서도 ViewModel이 아닌 ViewModelType에 의존하게 하였습니다.
지금은 ViewModel()을 하고 있지만 실제 사용 시 의존성주입을 통해 주입 받을 것입니다.
그럼 추상화를 함으로써 갖는 이득인 Stub객체를 만들어보겠습니다.
import Foundation
@testable import WhatIsMVVM
final class ViewModelStub: ViewModelType {
var state: ViewModel.ViewState = ViewModel.ViewState()
func plusButtonTapped() {
state.count += 1
}
func minusButtonTapped() {
state.count -= 1
}
var countUpdated: ((Int) -> Void)?
}
테스트코드입니다.
import XCTest
@testable import WhatIsMVVM
class WhatIsMVVMTests: XCTestCase {
var viewModel: ViewModelType!
func test_ViewModel에서_plusButtonTapped가_호출되었을때_count가_늘어난_후_ViewStateCount는_업데이트된다() throws {
//give
viewModel = ViewModelStub()
//when
viewModel.plusButtonTapped()
//then
XCTAssertEqual(viewModel.state.count, 1)
}
}
MVVM, MVC에 대한 주관적인 회고
처음 스위프트를 공부할 때 정말 근본없는 상태에서 Rx부터 훑기 시작했었고, 채용공고에 나와있는 RxSwift + MVVM을 본 후, 간단하게 나와있는 예제를 흉내내며 개발했었습니다.
그러고 RxSwift + MVVM이 정말 편하고 좋다 말하였습니다.
RxSwift없이 MVVM을 구현할 줄 몰랐으며 MVVM의 장점이 무엇인지도 잘 몰랐습니다.
면접에서도 왜 MVVM을 쓰냐 물어 본다면 "View로부터 ViewModel이 독립적이다. Massive ViewController를 해결할 수 있다."라고 대답하는게 전부였습니다.
어느 블로그를 가든 쉽게 볼 수 있는 형식적인 말들에 넘지 않는다고 생각합니다.
MVC에서도 View와 독립적인 Usecase를 작성할 수 있으며 역할 별 객체를 잘 분리하여 Massive ViewController 또한 해결할 수 있다고 생각합니다.
또한 추상화를 많이하게 되면 가독성은 떨어지고 구조복잡도는 올라가게 되죠.
하지만 MVVM의 ViewState는 어떠한 로직이 잘 행하여 졌는지 알기 좋은 방법이며, ViewModel을 바인딩하는 View의 방식 또한 좋다고 생각합니다.
상황에 따라 본인이 더 선호하는 방식의 아키텍쳐를 선택하면 좋을 것 같습니다.
'iOS > 스위프트' 카테고리의 다른 글
[Swift][TDD] 테스트 주도 개발! + (테스트를 해야 하는 이유) (0) | 2022.09.08 |
---|---|
[iOS][CS,OS] class, struct의 차이는 무엇일까? (0) | 2022.09.05 |
[Swift][MVC] 애플 MVC 야무지게 사용해보기. (1) | 2022.07.19 |
[Swift][DI] 의존성 주입 라이브러리. (0) | 2022.07.12 |
[iOS][Moya] Moya 네트워크 테스트 (with: Rx를 곁들인) (0) | 2022.06.29 |