안녕하세요
오늘은 아키텍쳐에 대해 좀 살펴보려고 합니다.
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 패턴으로 만들어진 로그인 뷰컨트롤러가 생성되었습니다!
저 역시 공부하는 입장이기 때문에 헷갈리거나 약간 애매모호한 부분이 있을수도 있으니,
그럴때는 괘념치 마시고 댓글 등으로 말씀해주시면 같이 토론해나갔으면 좋겠습니다.
그럼 다음 시간에 또 만나요
안녕~
'Swift 개발 이야기' 카테고리의 다른 글
Protocol Default Implementation in Swift (0) | 2020.04.10 |
---|---|
예제로 살펴보는 MVI Design Pattern with RxSwift (2) | 2020.04.08 |
iOS13 이상에서 스토리보드 없이 시작하기! (0) | 2020.04.03 |
Insertion Sort Swift (0) | 2020.03.24 |
Swift 로 알아보는 객체지향 프로그래밍 - 2 (0) | 2020.02.19 |