3. MVVM: Model-View-ViewModel (2)

Seoyoung Lee·2023년 12월 2일
0

MVVM Application

이제 MVVM 패턴을 적용해서 투두 앱을 만들어보자! 이전 글과 마찬가지로 새롭게 알게 된 내용이나 기억하고 싶은 내용 위주로만 작성할 예정이다.

MVVM Layer

Model

  • 비즈니스 로직과 관련된 모든 것들
  • 모델 안에서 일어난 변화를 ViewModel이 알아야 한다. 이를 위해 ViewModel의 init 메소드 안에서 옵저버를 등록한다.

Model 디렉토리 안에 있는 하위 디렉토리들은 아래와 같다.

📂 Core Data

  • CoreDataManager.swift 파일이 있는 곳

📂 Models

  • 데이터베이스 엔티티를 모델로 변환하는 모델들을 포함한다. 이를 구현할 때 필요한 프로토콜도 포함한다.

📂 Services

  • 데이터베이스에 데이터를 업로드하는 로직과 관련된 클래스들을 포함한다.
  • 그외에도 데이터베이스로부터 데이터를 받는 클래스, 이를 모델로 변환하는 클래스들도 포함한다.

📂 Extensions

  • 이 프로젝트에서는 UIColor, NSManagedObject 익스텐션들을 포함한다.
    • NSManagedObject는 테스트를 할 때 사용됨

📂 Constants

  • 앱 내에서 사용되는 상수 파라미터들을 포함한다.

View

  • View와 ViewController 파일들이 있는 곳
  • MVVM에서 컨트롤러는 화면간 내비게이션과 데이터 전달(델리게이트 패턴 등) 기능만을 가지고 있다는 것 잊지 말기!

ViewModel

  • Model과 View를 연결하는 ViewModel들만을 포함한다.

RxSwift 잠깐 복습하고 넘어가기

RxSwift는 비동기적으로 코드를 작성할 수 있게 도와주는 라이브러리다. 새로운 데이터를 받으면 동작할 코드를 연속적이고 독립적으로 간단하게 작성할 수 있다.

Observables and Observers

  • Observable: 변경이 생기면 알림을 방출한다.
  • Observer: Observable을 구독해서 알림을 받는다.

Observable은 한 개 이상의 Observer를 가질 수 있다.

Input/Output Approach

Input/Output 접근법은 서로 다른 컴포넌트와 이벤트를 바인딩하고 RxSwift를 더 체계적으로 사용하기 위한 방법이다.

  • Input: View에서 일어나는 모든 이벤트와 인터랙션. ViewModel에 영향을 주는 것들이다. (텍스트 입력, 버튼 터치 등)
  • Output: 모델에서 일어나는 변화들. 얘네들은 반드시 View에 반영되어야 한다.
class ExampleViewModel {
	var output: Output!
	var input: Input!

	struct Input {
		let text: PublishRelay<String>
	}

	struct Output {
		let title: Driver<String>
	}
	
	init() {
		let text = PublishRelay<String>()
	
		let capsTitle = text
			.map({
				return text.uppercased()
			})
			.asDriver(onErrorJustReturn: "")
		input = Input(text: test)
		output = Output(title: capsTitle)
	}
}

Input/Output 접근법을 사용해서 ViewModel을 구현한 예시이다.

  • PublishRelay
    • 구독한 이후 발생하는 값들을 방출한다.
    • 예제에서는 UITextField 에서 입력한 문자열 값이 방출될 것이다.
  • Driver
    • 메인 스레드에서 동작하는 Observable이다. → 기본적으로 Observable은 백그라운드 스레드에서 동작한다고 한다. 그래서 View를 업데이트하기 위해 Driver를 사용하는 것..!!!
    • 예제에서는 UITextField 에서 받아온 값을 UI 컴포넌트에 전달한다.

나중에 알게 되었는데,
ViewModel 초기화 시점에 Input의 스트림을 생성하지 않고 View에게 Input을 전달받고 Output 스트림을 생성해서 리턴하는 메소드를 정의하는 방법도 있다고 한다.
참고1, 참고2

그럼 ViewModel과 View는 어떻게 바인딩해줄까?

class ExampleView {
	func bind() {
		textfield.rx.text
			.bind(to: viewModel.input.text)
			.disposed(by: disposeBag)

		viewModel.output.title
			.drive(titleLabel.rx.text)
			.disposed(by: disposeBag)
	}
}
  • UITextFieldtext 파라미터를 ViewModel의 text 변수(Input)와 바인딩한다. 사용자가 텍스트필드에 텍스트를 입력하면 ViewModel이 이를 받게 된다.
  • ViewModel Output의 title 변수를 UILabeltext 파라미터와 바인딩한다. 사용자가 타이핑을 하면 ViewModel의 text가 대문자로 변환되고, View에 전달되어서 화면에 표시된다.

…🤔

  • Subscribe / Bind / Drive에 대한 차이점 정리는 요기로.. Rx에 익숙해지면 나도 더 자세히 공부해봐야지!!!
  • bind(to:)는 새 구독을 생성하고 항목들을 Observer들에게 보내주는 메소드이다.
  • driveDrive 에서만 정의되며, bind(to:) 대신 사용할 수 있다고 한다.. → 그니까 텍스트필드의 텍스트를 구독해서 뷰모델의 input에 알려주고, 뷰모델의 output을 구독해서 타이틀라벨의 텍스트에게 알려주는…..

🤯 너무 헷갈린다.. 그래서 누가 누굴 구독하는 거야?

  1. bind(to:) 메소드는 다음과 같은 형태로 작성되어 있다.

    public func bind(to relays: PublishRelay<Element>...) -> Disposable {
        bind(to: relays)
    }

    이 메소드는 새 구독을 생성하고 항목을 publish relay들에게 보내는 메소드라고 한다.

    안에 있는 bind 메소드가 어떻게 구현되어 있는지 더 뜯어보자.

    private func bind(to relays: [PublishRelay<Element>]) -> Disposable {
        subscribe { e in
            switch e {
            case let .next(element):
                 relays.forEach {
                    $0.accept(element)
                }
            case let .error(error):
                rxFatalErrorInDebug("Binding error to publish relay: \(error)")
            case .completed:
                break
            }
        }
    }

    bind 메소드를 호출하는 Observable을 구독하고, 새 항목이 방출되면 파라미터로 받은 relay에게 그 항목을 전달하는 것 같다!

    to 라는 파라미터 이름을 방출된 항목을 받는 친구를 가리키는 거라고 생각해야겠다.

  2. drive() 는 종류가 다양하다. Observer와 Relay 모두 파라미터로 받을 수 있다.

    public func drive<Observer: ObserverType>(_ observers: Observer...) -> Disposable where Observer.Element == Element {
        MainScheduler.ensureRunningOnMainThread(errorMessage: errorMessage)
        return self.asSharedSequence()
                   .asObservable()
                   .subscribe { e in
                    observers.forEach { $0.on(e) }
                   }
        }

    이 메소드도 새 구독을 생성하고 항목을 observer에게 보내주는 메소드라고 한다. 대신 메인 스레드에서만 호출된다.

결론은 bind(to:)drive() 모두 메소드를 호출하는 Observable을 구독하는 것이고, 파라미터로 받은 Observer가 변경된 값을 전달받는 듯 하다!

profile
나의 내일은 파래 🐳

0개의 댓글