빙수왕의 개발일지

유투브 샘플앱으로 무작정 RxSwift + MVVM 사용해보기 본문

개발/iOS

유투브 샘플앱으로 무작정 RxSwift + MVVM 사용해보기

빙수킹 2021. 6. 16. 16:18

우선.. 나는 MVVM 구조에 대해서만 알고 있었고 RxSwift를 잘 모르는 상태였다.

KxCoding에서 RxSwift를 반정도 들었었는데 이건 그냥 듣고 따라만해서는 감이 오지 않았다.

뭔가 감이 오도록 예제 샘플 아무거나 따라하고싶어서 유투브에 검색함ㅎㅎ.. 유투브짱!!

 

아래 유투브가 가장 먼저 나와서 이걸 보고 따라했다. (친절한 강의 영상 감사함니다)

여기서 만드는 앱은 NewsApi를 사용하여 List에 뿌리는 앱이다.

그리고 만들고 나니.. 내가 배운 이론으로 생각했을 때, 바꿀 부분들이 보여서 리팩토링을 진행했다.

-> 는 또 공부 더 하니까 내가 한 리팩토링이 정답이 아니였다..ㅠ.ㅠ

 

https://www.youtube.com/watch?v=Ckxngx2w3ZQ 

 

일단 열심히 다 듣고 따라했고.. 코드는 아래에 MVVMRxTutorial 로 올려놓았다.

https://github.com/getBingsoo/SampleCodes

 

getBingsoo/SampleCodes

연습장. 이것저것 샘플앱 만들어보기. Contribute to getBingsoo/SampleCodes development by creating an account on GitHub.

github.com

 

 

기존 코드 분석

 

대충 구조를 정리하자면

RootViewController -> RootViewModel -> ArticleService 요렇게 바라보는 방식이고,

코드를 통해 자세히 알아보자.

 

ArticleService

: Api 호출 후 응답으로 받은 Article 리스트를 Observable형태로 만든다.

어떻게? create 타입메서드를 통해서. 참고로 create는 정석적으로 Observable 생성하는 방법인데, 이거 말고도 방법이 많다. (from등의 수많은 연산자들이 있다..) 일단 난 초보니까 정석으로 만들었다.

* 만들 때 하는 것들  create, 내용에 각 상황에 따른 next, complete, error 이벤트를 정의함 (onNext 같은 것들은 이걸 구독하는 Observer가 정의하게 된다.)

* 여기서 잠깐  여기서는 단지 observable을 만들었을 뿐이다. 그럼 여기서는 방출할 순서만 정하고,

언제 이벤트가 전달되는가?

"옵저버가 옵저버블을 구독할 때!!!" -> 구독하는 부분은 VC 쪽에 있다.

 

아래 코드에서 보면 받아온 리스트를 옵저버블로 만들고

에러일 때 onError, 정상일 때 onNext, 작업이 끝났을 때 onCompleted를 호출해준다. (옵저버의 것)

func fetchNews() -> Observable<[Article]> {
    return Observable.create { (observer) -> Disposable in

        self.fetchNews { result in
            switch result {
                case .failure(let error):
                    observer.onError(error)
                case .success(let articles):
                    observer.onNext(articles)
            }

            observer.onCompleted()
        }

        return Disposables.create() // 옵저버가 필요없어졌을 때 알아서 메모리에서 없애주는 친구
    }
}

 

RootViewModel

뷰에 보여주기위한 내용만 간추리기 위해서인가??

그냥 map으로 가공해서 (Article to ArticleViewModel)뷰로 던지고 있음. 내 생각엔 뷰모델이 프로퍼티로 이 리스트를 저장하고 있어야 할 것 같다.

-> 음.. 좀 더 공부해보니 저장해도 되고, 그냥 View, ViewModel 어디에도 Observable을 저장하지 않고 View의 viewDidLoad에서 한 번 최초로 구독하는 코드만 넣어놔도 괜찮다.

func fetchArticles() -> Observable<[ArticleViewModel]> {
    articleService.fetchNews().map { articles in
        articles.map { article in
            ArticleViewModel(article: article)
        }
    }
}

 

 

ArticleViewModel

ArticleViewModel은 아래와 같은 그냥 class인데 article에서 필요한것만 뽑아서 모델로 만든거다.

이름이 ViewModel이라서 헷갈렸는데, Model로 봐도 되지 않을까 싶다(?)

-> 더 공부하고 나서 안건데 ViewModel은 View에서 보일 모델이라는 뜻으로.. 내가 뜻을 편협하게 알고있었던 것 같다.

또 Model은 각 UseCase의 결과로 Data Layer(DB, Network 등)에서 얻어오는 형태를 뜻하는 것인데, 나는 기존에 Model은 그냥 struct형태로 된 껍질을 의미한다고 생각했다. 그래서 뷰에서 보여지는 껍질 형태도 동일한 Model이라고 생각했다. 구현하는 사람마다 해석이 다르겠지만, ViewModel을 어떤 Presentation Logic들이 무조건 있어야만 한다고 생각했었는데 그렇게만 생각할게 아니였다.

CleanArchitecture에서는 Presentation 부분과 Domain을 분리하기 때문에 여기서의 Model은 Domain에 속하는 녀석이고 ViewModel은 Presentation에 속하는 녀석이다. 이게 더 쓰임이 명확하게 구분되는 것 같다.

class ArticleViewModel {
    private let article: Article

    var imageUrl: String? {
        return article.urlToImage
    }

    var title: String? {
        return article.title
    }

    var description: String? {
        return article.description
    }

    init(article: Article) {
        self.article = article
    }
}

 

 

RootViewController

 

위에서 구독을 해야 이벤트가 전달된다고 했다. 옵저버가 구독을 시작하는 방법은 subscribe를 호출하는 것이다.

subscribe를 통해 옵저버와 옵저버블이 이어진다.

어떻게 구독하는가? 요렇게.

보통 많이쓰는건

let observer = observable.subscribe ( onNext: {할 일} )

observer.disposed(by: disposeBag)

 

 

또는 onNext로 클로저를 넣는 방식 말고, element 속성에 접근하면 next에서 뱉는 값에 접근이 가능하다. 하지만 거의 onNext를 사용하는 것 같다.

 

코드를 보자.

viewDidLoad에서 fetchArticles, subscribe를 호출한다.

 

우선 RootViewController에는 CollectionView에 뿌리기 위한 2개의 프로퍼티가 있다.

let articleViewModel = BehaviorRelay<[ArticleViewModel]>(value: [])
var articleViewModelObservable: Observable<[ArticleViewModel]> {
    return articleViewModel.asObservable()
}

 

함수 내용은 아래와 같다. fetch 해온 것을 BehaviorRelay가 구독한다.

그럼 Relay는 accept로 이걸 받는다. 

accept를 하면 next 이벤트가 발생한다.

음.. 그냥 쉽게 막 이해하자면 Relay라는 형식에서 사용하는.. accept하면 next한다 == 밥먹자마자 똥싼다 (????)

 

// MARK: Helpers
func fetchArticles() {
    // 서비스에서 받은거 구독!!
    // 구독하여 onNext에서 뷰 업데이트 해주면 될 것 같다.
    // 근데 여기서는 받아온걸 다시 relay에 전달한다.
    // 그럼 relay를 구독하는 놈이 받는건데
    // 여기서는 relay를 직접 구독하지 않고 relay를 옵저버블로 만들어서 이걸 바로 아래에서 구독하네
    // relay는 전달용도로만 사용된듯하다.
    viewModel.fetchArticles().subscribe(onNext: { articleViewModels in
        self.articleViewModel.accept(articleViewModels) // 전달(next와 동일한 효과)
    }).disposed(by: disposeBag)
}

func subscribe() {
    // 1. 리팩토링 - subscribe
//        self.articleViewModel.subscribe(onNext: { articles in
//            // collectionView reload
//            DispatchQueue.main.async {
//                self.collectionView.reloadData()
//            }
//        }).disposed(by: disposeBag)

    self.articleViewModelObservable.subscribe(onNext: { articles in
        // collectionView reload
        DispatchQueue.main.async {
            self.collectionView.reloadData()
        }
    }).disposed(by: disposeBag)
}

 

Relay란?

subject처럼 이벤트를 받아서 구독자에게 전달하는데(밥먹자마자 똥을 싸는데 .. ), 가장 큰 차이는 next만 전달한다는 것이다.

빈 생성자로 생성하고 accept 메소드를 통해 구독자에게 next가 전달된다.

value속성은 next 이벤트에 접근해서 여기에 저장된 값을 리턴한다.

 

subject란?

옵저버블 && 옵저버이다.

구독과 전달 모두 OK!

약간.. 원래 옵저버는 멍청이고 머리가 없다면 subject는 뇌와 눈이 있는 옵저버라고 이해했다.. 옵저버는 주는대로 받아서 사용하지만, subject는 보고 원하는걸 고를 수 있음.

 

 

리팩토링

 

나도 내가 맞는지 모르겠지만, 내 눈에 위 코드에서 바꿀만한 점은 

 

1. 옵저버블이 뷰모델에 있는게 옳지 않나 싶다. -> 저장해도 되고, 그냥 View, ViewModel 어디에도 Observable을 저장하지 않고 View의 viewDidLoad에서 한 번 최초로 구독하는 코드만 넣어놔도 괜찮다.

2. 불필요하게 꼬리에 꼬리를 무는 구독들이 있는 것 같다. 뷰모델에 옵저버블을 넣고 뷰에서 한 번만 구독하면 될 것 같은데..

3. 명명이 헷갈린다. (ViewModel이 다른 의미로 또 사용됨) -> 위에 써놨지만 이건 잘못된 생각이였다.

 

그래서 변경해보았따.. -> 지금보니 아주 일차원적인 변경임... 훌륭한 변경은 아니였던 것 같다.

-------------------------------------

 

ViewModel

 

1. articles를 프로퍼티로 가지게 했다. 그리고 service에서 받아온거 그대로 저장.

2. ArticleViewModel이라는 모델은 삭제하였다.

 

ViewController

 

1. VC에 있는 데이터들(Relay, Observable)들을 지웠다.

 

2. articles라는 배열을 가지게 했다(display 용도. 만약 가공이 필요하다면 DisplayArticle 이런식으로 디스플레이용 모델을 하나 만들어도 될 것 같다)

3. subscribe에서는 viewModel의 articles를 직접 구독하여 뷰의 articles에 저장하고, collectionView reload를 한다.

collecitonView에서는 articles를 바라본다.

 

 

Cell

 

cell은 변수이름 viewModel -> subject로만 바꿨다.

 

cell은 subject를 가지고, cellForRowAt에서 subject.onNext로 Article을 보내주는 형식이다.

 

 

결과 

잘 나온당.. 굿.