😤
별거 아닌 줄 알았는데 생각보다 쏘 어려운 MVVM
Model - View - ViewModel 이렇게 나뉘는 패턴임
Observable 클래스는 데이터의 변화를 감지하고, 얘를 구독하고 있는 컴포넌트에 알리는 역할을 한다. 이걸 통해서 데이터 바인딩을 구현하고, 데이터가 변경될때마다 UI가 자동으로 업데이트 될 수 있도록 한다.
간단한 로그인 화면
을 만들어보면서 MVVM과 Observable에 대해 알아보자.
이렇게 아주 간단한 로그인용 텍스트필드 2개와 결과값을 바로바로 반영해주는 레이블을 하나 만들어놓았다.
여기서 textField의 값이 바뀔때마다, 거기에 입력한 값을 바로 resultLabel에 넣으려면, 변수에 didSet을 다는 방식으로 값을 반영해주었다.
그러면 textField가 10개면 변수10개에 didSet을 달아야 하나?
먼저 Observable 클래스를 통해 이걸 해결해보자.
실시간으로 달라지는 데이터를 감지하기 위한 클래스이다.
class Observable<T> {
private var closure: ((T) -> Void)?
var value: T {
didSet {
closure?(value)
}
}
init(_ value: T) {
self.value = value
}
func bind(_ closure: @escaping (T) -> Void) {
print("bind")
closure(value)
self.closure = closure
}
}
먼저 이값 저값 다 들어갈 수 있게 제네릭으로 만들어두었고,
value의 값이 바뀔때마다 closure가 실행된다.
closure에 해당되는 액션들은 뷰 컨트롤러에서 주입해주면 된다.
우선 이메일 텍스트필드에 입력되는 값을 관찰해보자.
var emailText = Observable("")
이렇게 관찰자를 달아주고, 타입으로는 String을 지정해주었다.
emailTextField.text = emailText.value
그 다음엔 텍스트필드에 이메일 값이 들어올 수 있도록 해준다. 더이상은 emailText이 아닌 그 다음엔 텍스트필드에 이메일 값이 들어올 수 있도록 해준다. 더이상은 emailText이 아닌 emailText.value를 넣어야 한다.
emailText.bind { value in
self.resultLabel.text = value
}
그리고는 요렇게 클로저에대한 액션으로 resultLabel에게 이메일을 넣어준다.
요렇게 하면 이메일 텍스트필드에서 텍스트를 입력할때마다 바로바로 resultLabel에 잘 반영되는 걸 확인할 수 있다!
ViewModel (LoginViewModel)
뷰모델의 핵심이 뭐냐?
뷰에서 기능 단위를 분리하는 것이다.
야매로라도 알아볼 수 있는 방법으로는, import UIKit를 잠시 지워놨을 때 난리 난리 나는 빨간 에러들은 모두 UI와 관련된 코드들이다. 그 나머지를 뷰모델로 분리해주면 되겠다.
class LoginViewModel {
var inputText = Observable("")
var outputText = Observable("")
func validation(email: String) {
if email.count >= 3 {
outputText.value = email
} else {
outputText.value = "3글자 이상 입력해주세요."
}
}
}
원래 뷰에 있었떤 사용자가 입력한 이메일과 비밀번호를 관찰 가능한 객체(Observable)로 선언하자.
그리고 이메일 텍스트필드에 입력된 값들에 따라 유효한 이메일인지 아닌지를 판별하는 validation()을 만들어보자.
그리고 LoginViewModel 인스턴스를 뷰에 만들어주자
let viewModel = LoginViewModel()
동작 방식은 다음과 같겠다.
inputPassWord도 마찬가지로 관찰자를 달아주자
class LoginViewModel {
var inputText = Observable("")
var inputPassWord = Observable("")
var outputText = Observable("")
init() {
inputText.bind { value in
self.validation(email: value, pw: self.inputPassWord.value)
}
inputPassWord.bind { value in
self.validation(email: self.inputText.value, pw: value)
}
}
private func validation(email: String, pw: String) {
if email.count >= 3 && pw.count > 5 {
outputText.value = "\(email), \(pw)"
} else {
outputText.value = "3글자 이상 입력해주세요."
}
}
}
LoginViewController에서도 여기에 맞춰서, password 변수 선언한 부분을 뷰모델로 옮겼으니, password가 바뀔때마다도 뷰모델의 inputPassWord에 값을 넣어주면 된다.
한 가지를 더 해보자.
validation 값에 따라 resultLabel의 색을 바꿔보자.
그러니까 뷰모델에서 값이 달라짐에 따라 뷰에 있는 속성에 변화를 줘보자!
뷰모델에 다음과 같이 Bool값으로 컬러 결과값을 나타내는 변수를 만들어준다.
var outputColor = Observable(false)
그리고 validation 조건에 따라
if email.count >= 3 && pw.count > 5 {
outputText.value = "(email), (pw)"
outputColor.value = true
} else {
outputText.value = "3글자 이상 입력해주세요."
outputColor.value = false
}
결과값을 다르게 해서 반환하게 해보자.
viewModel.outputColor.bind { value in
self.resultLabel.textColor = value ? .systemGreen : .systemRed
}
뷰에서도 요렇게 다르게 받아주면 되겠지?
ViewModel의 inputText와 inputPassword는 View의 textField와 바인딩되어 있으며, 이 값들이 변경될 때마다 validation 메소드가 호출된다.
validation 메소드는 입력 값의 유효성을 검사하고,
결과에 따라 outputText와 outputColor를 업데이트한다.
이러한 업데이트는 다시 View에 반영되는 것이다.