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

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

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

안녕하세요 여러분?

 

저번 시간에는 MVVM 디자인 패턴을 살펴보았는데요.

 

이번에는 약간 생소한 MVI 라는 패턴을 들고 찾아왔습니다.

 

MVI 는 Model - View - Intent 로 구성되어 있는 단방향(Uni-Directional) 아키텍쳐 중 하나인데요.

 

MVVM 의 경우 VM 이 Model 과 View 의 사이에서 양방향으로 통신하기 때문에 자칫 잘못하다간 VM 이 비대해지는 부작용이 발생할 수가 있습니다. 

 

이러한 부작용을 해결하기 위해서 MVI 의 경우는 뒤를 돌아보지 않는 단방향 상남자의 아키텍쳐라고 볼 수 있겠습니다.

 

View 에서 액션을 입력 받으면 Intent 에서 모델의 상태를 변환시키고,

 

그 변환된 상태의 모델을 뷰에 전달하여 유저에게 보여준다고 보시면 되겠습니다.

 

요런식으로 흘러간다고 보시면 될 것 같습니다

 

 

그런데 이렇게 되면 기존의 MVVM 이나 MVC 에서 Model 은 비즈니스 로직을 담당하는 친구라고 이야기를 했잖아요?

 

비즈니스 로직의 상태를 변환시킨다? 이것은 좀 말이 안되지 않나요?

 

그래서 용어 정리를 제 나름대로 다시 정리를 해봤을때 MVI 에서의 Model 은 이제 Entity 라고 생각하시면 마음이 편할 것 같습니다.

 

그리고 비즈니스 로직을 담당하는 친구는 제 임의로 Handler 라고 해서 로직단을 분리하고 시작해보도록 하겠습니다.

 

역시 예제로 보는 것이 가장 빠르겠죠? 이전 글과 동일하게 로그인을 수행하는 뷰를 이용해서 진행해보도록 하겠습니다.

 

먼저 모델을 만들어 봅시다.

 

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"
        }
    }
}


enum LoginState {
    case success(_ user: User)
    case failure(_ error: LoginError)
}

 

User 라는 모델과 로그인이 실패했을 때 에러를 만들었고,

 

이전과 다르게 enum 으로 LoginState 를 만들었습니다.

 

성공 시에는 User associate type 을 반환하고, 실패 시에는 에러를 내보냅니다.

 

이번엔 로직을 담당하는 부분을 볼까요?

 

struct LoginHandler {
    
    func requestLogin(id: String, pw: String) -> Observable<LoginState> {
        
        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()
        }
    }
    
}

 

로그인을 요청해서 성공 시에 Login State 를 리턴해줍니다. 비즈니스 로직은 이전 글과 동일한데 명칭이 헷갈리지 않게 Handler 로만 바꿨습니다.

 

사실 여기까지는 MVVM 과 별반 다를바가 없죠? 여기서 이제 Intent 를 작성해봅시다. Intent 는 VM 과 비슷하게 뷰의 행동들을 정의해놓은 부분이라고 보시면 됩니다. 하지만 위에서 이야기했던 것 중에 Intent 가 해주어야 하는 것은 View 에 변환된 Model 상태값을 전달해주어야 합니다. 따라서 state 를 관찰해주는 observer 와 전달받을 View 를 추가해주어야 합니다.

 

class LoginViewIntent {

    let stateObserver = PublishRelay<LoginState>()
    let handler = LoginHandler()
    let disposeBag = DisposeBag()

    var vc: LoginViewController?

    func bindTo(vc: LoginViewController) {
        self.vc = vc
        
        stateObserver.subscribeOn(MainScheduler.instance).subscribe(onNext: { (state) in
            self.vc?.render(state)
            }).disposed(by: disposeBag)
    }

    func loginBtnTouched(id: String, pw: String) {

        handler.requestLogin(id: id, pw: pw).subscribe(onNext: { (state) in
            self.stateObserver.accept(state)
            }).disposed(by: disposeBag)
    }
}

 

뷰에서 loginBtnTouched 가 호출되면, handler 가 작동하여 변환된 state 를 observer 에 전달하고, View 에서 해당 사항을 전달 받아 관련된 정보를 render 한다 라고 보시면 됩니다.

 

그럼 여기서 render 함수가 무엇이냐? 변환된 상태값을 그대로 뷰에 뿌려주는 것이라고 보시면 되는데요.

 

코드로 바로 보시겠습니다.

 

class LoginViewController: UIViewController {

    @IBOutlet weak var idTf: UITextField!
    @IBOutlet weak var pwTf: UITextField!
    @IBOutlet weak var loginBtn: UIButton!
    
    let intent = LoginViewIntent()
    
    let disposeBag = DisposeBag()
    
    override func viewDidLoad() {
        super.viewDidLoad()
                
        intent.bindTo(vc: self)
        self.bindTo(intent)
        
    }

	func bindTo(_ intent: LoginViewIntent) {
        loginBtn.rx.tap.bind {
            guard let id = self.idTf.text, let pw = self.pwTf.text else { return }
            intent.loginBtnTouched(id: id, pw: pw)
        }.disposed(by: disposeBag)
	}


    func render(_ state: LoginState) {
        switch state {
        case .success(let user):
            print(user)
            self.moveToMain()
        case .failure(let fail):
            print(fail)
            self.showError()
        }
    }

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

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

 

먼저 뷰컨트롤러에서 Intent 를 만들어주시고,

 

뷰의 액션을 인텐트로 전달해주어야겠죠?

 

이 부분이 bindTo 함수 입니다. MVVM 에서와 달리 id나 pw 가 바뀌는 때마다 확인 안해줘도 되고, 버튼이 눌리는 시점의 값을 그대로 인텐트로 보내줍니다.

 

그리고 인텐트 내의 핸들러에서 로직을 수행한 이후 변환된 상태값을 render 로 뿌려주게 되면?

 

저희는 또다시 VC 에서 UI 와 완벽하게 분리된 디자인 패턴을 구성할 수 있게 되었습니다.

 

MVVM 의 경우 View 에서 VM 을 binding 하고,  VM 에서 Signal 로 방출한 결과를 View 에서 UI 로 구성해주기 때문에 VM 이 굉장히 바쁘지만 MVI 에서는 단방향으로 넘어가기 때문에 제가 느끼기에는 좀 더 직관적인 것 같습니다.

 

아시겠지만 사실 이걸 MVC 로 사용하자면 코드가 더 짧아지긴 합니다.

 

바로 뷰컨트롤러의 액션에서 핸들러를 호출해주면 되긴 하는데요.. 그러면 우리가 언제나 우려하던 Massive View Controller 가 탄생하게 되겠죠?

 

요즘 디자인 패턴에 대해 여러가지 공부를 하고 있는데, 꼭 어떤 것을 고집해서 사용하겠다! 라는 것보다는 상황에 맞는 디자인 패턴을 찾는 것이 제일 중요한 것 같습니다.

 

저 역시 언제나 공부하는 입장이고 다른 분들의 글들을 그대로 차용하는 것보다 제가 사용하기에 편하게 조금 변형해서 쓰는 것이기 때문에,

 

틀린 점이 있거나 궁금하신 점이 있으시면 언제나 연락주시면 감사하겠습니다.

 

다음 시간에 또 만나요

 

안녕~~~