본문 바로가기

RxSwift

[RxSwift] 토이 프로젝트를 통해 알아보는 RxSwift x MVVM

반응형

 

위와 같은 토이 프로젝트를 만들어 보면서, RxSwift 활용과 MVVM을 적용하는 방법에 대해서 다루고자 합니다.

이 토이 프로젝트는 실제 네트워크 통신을 하지 않았습니다,

대신 request시 global Queue에서 작업을 진행하고 완료되면 main Queue로 변환하여 결과 데이터를 넘겨주도록 진행함으로써 네트워크 통신과 유사한 환경을 만들고자 했습니다.

 

DataModel의 경우 name, age, value 값을 가지고 있으며, 테스트를 위해 다음과 같이 데이터를 설정했습니다.

  • name은 구분하기 쉽게 필터값

  • age는 20~49의 랜덤 값

  • value는 아이템 순서 값

요구 사항

(위 시연 영상 참고)

  • 리스트 형식이며, 최신순, 오래된 순, 인기순으로 정렬할 수 있다. (필터)

  • 리스트는 한 페이지에 20개씩 나오며 페이징이 된다.

  • 필터를 바꾸게 되면, 해당 필터의 첫 번째 페이지값으로 변환한다.

  • 좋아요, 구매하기를 누르면 해당 request를 요청하고 결과를 Alert로 노출하되 좋아요의 경우 결과 값을 통해 텍스트를 변경한다. (좋아요 시: ❤️, 좋아요 아닐 시: 💔)

세부 내용

 

MVVM 설계 기반으로 봤을 때, ViewModel에 필요한 User Interaction은

필터, 구매 버튼, 좋아요 버튼, 스크롤에 따른 페이지 정보가 있으며 이를 ViewModel에 바인딩하면 될 것으로 보입니다.

페이지 정보와 필터값만 있으면 ViewModel 내부에서 비즈니스 로직을 거쳐 View에서 필요한 리스트 요소들을 만들 수 있습니다.

 

ViewController Binding

ViewController에서 UserInteraction을 ViewModel에 바인딩 하는 것을 먼저 살펴보겠습니다.

 

RxCocoa는 많은 프로토콜 익스텐션과 UIKit 컴포넌트를 위한 rx 영역을 제공합니다.

(button.rx, tableView.rx 등 UIKit 컴포넌트에서 rx를 사용할 수 있습니다.)

 

일반적으로 bind(to:) 함수를 통해 UIKit 컴포넌트의 event를 구독할 수 있습니다.

여기서 to에 들어가는 타입은 ObserverType 프로토콜을 준수해야 합니다. ObserverType어디서 많이 보지 않았나요?

 

네, 대표적으로 Observable이자 Observer인 Subject가 있습니다. bind(to:)를 한다는 것은 발생하는 이벤트를 Observer가 해당 주체 Observable를 구독하는 것을 의미합니다.

 

따라서 구독을 처분할 수 있도록 Disposable이 반환되며, 이를 disposeBag을 통해 관리할 수 있습니다.

 

bind는 구독한다는 것을 의미하기 때문에 위와 같이 subscribe로 대체할 수 있습니다.

이제 위의 코드를 순서대로 설명하겠습니다.

  • 구매, 좋아요 버튼

구매, 좋아요 버튼의 경우 탭 이벤트가 발생한 이벤트를 viewModel에 전달해야 합니다. (Observer)

또한 viewModel에서는 해당 스트림을 구독하여 이벤트 발생 시, 네트워크 요청을 해야 합니다. (Observable)

때문에 Observer이자 Observable인 Subject 혹은 Relay로 구현되어 있다고 생각할 수 있습니다.

(UI Event 기반이므로 Relay가 더 적합하다고 생각합니다.)

 

Relay는 RxCocoa에서 제공하는 것으로 PublishRelay, BehaviorRelay가 존재합니다. Relay는 각 Subject를 Wrapping하여 구현되어 있습니다.
특징으로는 next 이벤트만을 다룬다는 것입니다.
completed, error는 발생하지 않고 dispose 되기 전까지 작동합니다.
next 만을 다루기 때문에 on(.next()) 형식이 아닌 accept(_ value:) 형식으로 이벤트를 발행합니다.
complete, error가 없으므로 UI Event에 사용하기 적합합니다.

https://jinshine.github.io/2019/01/05/RxSwift/3.Subject%EB%9E%80/

 

  • 필터 버튼들

세 개의 필터 버튼들을 가지고 있는 filterButtons를 순차적으로 반복하며 viewModel에 바인드합니다.

버튼이 tap 되었을 때, 그 버튼의 필터값을 알고 싶기 때문에, map 연산자를 사용하여 tap(Void)을 필터 값으로 변환합니다.

 

reactivex.io/documentation/operators/map.html

 

ReactiveX - Map operator

RxJS implements this operator as map or select (the two are synonymous). In addition to the transforming function, you may pass this operator an optional second parameter that will become the “this” context in which the transforming function will execu

reactivex.io

 

구매, 좋아요 버튼과 마찬가지로 viewModel의 filter는 Subject 혹은 Relay라는 것을 알 수 있습니다.

Subject는 여러 개의 Observable을 구독할 수 있으므로, 모든 버튼을 구독하게 함으로써 필터 이벤트를 관리할 수 있습니다.

  • 테이블 뷰

delegate fuction인 willDisplayCell을 구독하고 해당 row를 viewModel에 전달함으로써 viewModel 내부에서 페이지 관련 처리를 할 수 있도록 합니다.

다음은 변경 사항(Output) 결과를 바인드하는 것을 살펴보겠습니다.

 

코드를 살펴보면 각 ViewModel에서 변경 사항이 발생하면 UI Update를 진행하게 됩니다.

단 itemFetchFinished 이벤트가 발행하기 전에 viewModel의 items먼저 설정되어, tableView reload시 변경된 item을 사용할 수 있습니다.

 

ViewModel Binding

다음은 ViewModel을 살펴보겠습니다.

 

ViewModel이 초기화될 경우, bind 를 통해 ViewController의 UserInteraction을 구독한 정보를 바탕으로 Output 결과를 만들어냅니다.

  • currentPage

설명하기 전에, 리스트 아이템들을 가져오려면 어떤 것이 필요한지 확인하겠습니다.

 

위 코드를 통해 페이지 넘버와 필터값을 사용하여 아이템들을 받아올 수 있다는 것을 알 수 있습니다.

다시 돌아가서 바인딩하는 부분을 설명하겠습니다.

 

아이템 리스트 요청은 어떤 것이 변경될 때 해야 할까요?

답은 페이지값이라고 할 수 있습니다.

 

필터값에도 의존한다고 생각할 수 있지만, 페이지값이 스크롤과 필터에 의존적이기 때문에 결과적으로 리스트 요청은 페이지값에 의존하게 됩니다.

 

따라서 currentPage 의 이벤트가 발행된 것을 시작으로 네트워크 요청 시 필터값이 필요하므로

map을 통해 Observable<(Int, Filter)> 타입으로 변환합니다.

 

참고

withUnretained(obj)RxSwiftExt (RxSwiftExtension)에서 제공하는 것 중 하나로,
object를 weak로 하여 retain하지 않고 obj가 nil일 경우에는 empty 상태로 변경합니다.
생성되는 Observable은 object와 기존 Element를 함께 가진 형태로 만들어집니다.

map을 사용하여 Observable<(Int, Filter)> 로 변환했으니, 이제 이 정보를 가지고 네트워크 요청을 진행해야 합니다.

이는 원본 Observable의 이벤트를 받아 새로운 Observable로 변형시켜주는 flatMapLatest를 사용하여 적용할 수 있습니다.

 

reactivex.io/documentation/operators/flatmap.html

 

ReactiveX - FlatMap operator

 

결과적으로 페이지 넘버에 대한 이벤트가 발생하면,

페이지 번호와 필터 정보를 묶어서 네트워크 요청을 진행하게 되며, 그 결과를 구독하는 형식입니다.

 

NetworkResult 타입에 따라서 성공했을 시, 아이템 설정을 하고 itemFetchFinished 에 이벤트를 발행하여 ViewController에서 UI Update를 진행할 수 있도록 합니다.

  • filter

필터가 바뀔 경우(filterChanged) 페이지를 초깃값으로 재설정하여, 리스트 받아오는 로직을 진행할 수 있도록 합니다.

distinctUntilChanged 을 통해 같은 값의 이벤트가 발생하면 무시할 수 있도록 지정할 수 있습니다.

 

skip(1) 은 한 개의 이벤트 발생하는 것을 스킵하는 것을 의미합니다.

BehaviorRelay의 경우 초깃값을 지니고 있으며 구독할 경우, 가장 최근에 발생했던 이벤트를 넘겨주게 됩니다.

하지만 첫 번째 값은 필터가 변경된 것을 의미하는 것이 아닙니다. 따라서 한 개의 이벤트를 스킵하고 다음 이벤트부터 구독함으로써 필터가 바뀌는 경우를 파악할 수 있습니다.

  • purchaseTap, likeTap

구매와 좋아요는 비슷하기 때문에 같이 설명하겠습니다. 둘 다 이벤트가 발생하면 네트워크 로직을 타고 그 결과로 성공했을 경우에만 purchase와 like의 이벤트를 발행함으로써 UI Update를 할 수 있도록 합니다.

 

네트워크 에러가 발생하는 경우는 error 에 이벤트를 발행하고, Observable Stream 에서 에러 이벤트를 발행한다면 각 Output에 해당하는 프로퍼티들도 에러를 발행하여 구독을 종료하도록 합니다.

 

Github

github.com/NohEunTae/RxMVVMSample

 

NohEunTae/RxMVVMSample

Contribute to NohEunTae/RxMVVMSample development by creating an account on GitHub.

github.com

 

반응형