본문 바로가기
Swift 개발 이야기

예제로 살펴보는 MVVM Design Pattern with RxSwift

by 방화동한량 2020. 4. 7.
728x90

안녕하세요

 

오늘은 아키텍쳐에 대해 좀 살펴보려고 합니다.

 

iOS 개발자 분들이라면 기본적으로 MVC 패턴에 익숙하실텐데요

 

Massive View Con... 아,,닙니다. Model-View-Controller 패턴입니다 ;;

 

뷰컨트롤러에 여러가지 로직들이 들어가게 되어 뷰컨트롤러가 굉장히 무거워지게 되는 패턴이기 때문에

 

요즘 개발자분들은 많이 사용을 안하시려고 하는 것 같습니다.

 

그래서 요즘 핫하게 떠오르고 있는 대안이 바로 MVVM 인데요.

 

Model-View-ViewModel 로 구성되어 있는 아키텍쳐입니다.

 

MVVM 에서는 View 는 절대 Model 을 알 수 없고,

 

ViewModel 이 Model 과 통신을 해서 View 에 해당 사항을 전달해주고,(Presentation Logic)

 

전달받은 View 는 관련된 정보를 업데이트 하고,

 

유저가 UI 상에서 어떠한 액션을 수행하게 되면 이 결과를 직접 Model 로 보내주는 것이 아니라

 

ViewModel 에 전달해서 ViewModel 이 Model 에 전달하게 합니다.

 

여기서 Model 은 비즈니스 로직을 담당하는 부분이라고 생각하시면 되겠습니다.

 

이렇게 되면 UI 와 로직의 완벽한 분리가 일어나게 되겠죠?

 

즉 ViewModel 은 Model 과 View 를 연결해주며, View를 추상화시킨 개념이라고 보시면 되겠습니다.

 

그리고 여기서 View Controller 는 View 와 ViewModel 을 연결해주는 접착제라고 생각하면 조금 더 쉽게 이해가 되지 않을까 생각합니다.

 

하지만 iOS 에서는 MVVM 을 조금 더 편하게 사용하기 위해서는 RxSwift 를 사용하는 것이 좋기 때문에, MVC 에 비해 러닝커브가 좀 있습니다.

 

그렇기 때문에! 간단한 예제와 함께 알아보도록 하겠습니다.

 

id 와 password 를 받아서 로그인을 하는 아주 간단한 예제인데요.

 

먼저 Entity 를 구성해 줍니다.

 

아주 간단하게 유저는 이름만 가지고, 에러는 enum 으로 생성해줍시다.

 

struct User: Codable {
    let name: String
}

enum LoginError: Error {
    case defaultError
    case error(code: Int)
    
    var msg: String {
        switch  self  {
        case .defaultError:
            return "ERROR"
        case .error(let code):
            return "\(code) Error"
        }
    }
}

 

이제는 비즈니스 로직을 담당하는 로그인 모델을 작성해봅시다.

 

struct LoginModel {
    
    func requestLogin(id: String, pw: String) -> Observable<Result<User, LoginError>> {
        
        return Observable.create { (observer) -> Disposable in
            
            if id != "" && pw != "" {
                observer.onNext(.success(User(name: id)))
            } else {
                observer.onNext(.failure(.defaultError))
            }
            
            observer.onCompleted()
            
            return Disposables.create()
            
        }
        
    }
}

 

모델은 뷰모델에서 전달받은 id 와 pw 를 통해 User 또는 LoginError 를 리턴해줍니다.

 

여기서는 아주아주 간단하게 id 와 pw 가 전달될 시에는 무조건 로그인 성공으로 간주하고 넘어가지만,

 

원래대로라면 상세한 로직이 들어가야겠죠?

 

자, 이제 MVVM 의 꽃인 뷰모델로 넘어가보겠습니다.

 

그런데 MVC 에 익숙하신 분들이라면 생각하실 겁니다.

 

뷰는 언제 그리지??

 

하지만 위에서 말씀드린것처럼, 뷰모델은 뷰를 추상화시킨 개념이기 때문에, 먼저 그릴 필요가 없습니다.

 

뷰와 완전히 독립되어 작성될 수 있기 떄문에, 향후 로직을 테스트할 때 편리하다는 장점이 있죠.

 

바로 들어가 보겠습니다.

 

모델을 작동시키기 위해선 먼저 id 와 pw 가 있어야 할 것이고, 로그인을 시도할 버튼 액션이 있을 것이라고 할 수 있습니다.

 

그것을 그대로 작성해보겠습니다.

 

 

struct LoginViewModel {
        
    let idTfChanged = PublishRelay<String>()
    let pwTfChanged = PublishRelay<String>()
    let loginBtnTouched = PublishRelay<Void>()
    
    let result: Signal<Result<User, LoginError>>
        
    init(model: LoginModel = LoginModel()) {
        
        result = loginBtnTouched
        	.withLatestFrom(Observable.combineLatest(idTfChanged, pwTfChanged))
            .flatMapLatest { model.requestLogin(id: $0.0, pw: $0.1)}
            .asSignal(onErrorJustReturn: .failure(.defaultError))
        
    }
    
}

 

처음에 말씀드린것처럼 뷰는 모델을 알수가 없습니다. 그렇기 때문에 뷰에서 뷰모델로 입력받은 값들을 전달해야 하는데요.

 

idTfChanged 는 id TextField 에서 전달한 값을, pwTfChanged 는 pw TextField 에서 전달한 값을, loginBtnTouched 는 버튼이 터치되는 시점을 뷰에서 전달해준다는 것입니다.  

 

그리고 뷰모델이 모델과 통신해서 얻은 결과값은 다시 뷰에 전달해 주어야 하는데, 이것이 바로 result 로 선언된 부분입니다.

 

뷰는 작성하지 않았어도 로직상으로는 아무 문제가 없죠?

 

Relay 는 이제 뷰->뷰모델로 보낼 때 사용되고,

Signal 의 경우 값을 방출하는 것이기 때문에 뷰모델 -> 뷰 에서 사용한다고 보시면 되겠습니다.

 

자, 이제 이 모든 것을 뷰에 연결해봅시다.

 

우리가 추상화시켰던 것들을 뷰에서 구현한다면 역시 두개의 텍스트필드와 하나의 버튼이 있어야겠죠?

 

스토리보드에서 추가하고 IBOutlet 으로 연결해줍시다.

 

class LoginViewController: UIViewController {

    @IBOutlet weak var idTf: UITextField!
    @IBOutlet weak var pwTf: UITextField!
    @IBOutlet weak var loginBtn: UIButton!
    
    let viewModel = LoginViewModel()
    let disposeBag = DisposeBag()
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        bindViewModel()

    }
    
    func bindViewModel() {
        self.idTf.rx.text.orEmpty
            .bind(to: viewModel.idTfChanged)
            .disposed(by: disposeBag)
        
        self.pwTf.rx.text.orEmpty
            .bind(to: viewModel.pwTfChanged)
            .disposed(by: disposeBag)
        
        self.loginBtn.rx.tap
            .bind(to: viewModel.loginBtnTouched)
            .disposed(by: disposeBag)
        
        viewModel.result.emit(onNext: { (result) in
            
            switch result {
            case .success(let user):
                print(user)
                self.moveToMain()
            case .failure(let err):
                print(err)
                self.showError()
            }
        
        }).disposed(by: disposeBag)
    }

    func moveToMain() {
        print("MOVE")
    }

    func showError() {
        print("ERROR")
    }
}

 

이후 bindViewModel 함수 내에서 뷰에서 발생하는 액션들을 추상화시킨 뷰모델의 Relay 들과 각각 bind 를 해줍니다.

 

그리고 ViewModel 에서 방출되는 Signal 의 결과 값에 따라 이동 또는 에러를 보여주는 함수를 연결해주면!

 

아주 깔끔하게 RxSwift - MVVM 패턴으로 만들어진 로그인 뷰컨트롤러가 생성되었습니다!

 

저 역시 공부하는 입장이기 때문에 헷갈리거나 약간 애매모호한 부분이 있을수도 있으니,

 

그럴때는 괘념치 마시고 댓글 등으로 말씀해주시면 같이 토론해나갔으면 좋겠습니다.

 

그럼 다음 시간에 또 만나요

 

안녕~