[iOS] Rx-MVVM의 올바른 사용법 - saebyuck_choom

saebyuck_choom·2022년 6월 19일
6

디자인패턴

목록 보기
2/3
post-thumbnail

"MVVM이 좋은 것도 알겠고, Observable과 Operator들이 어떤 기능을 하는지는 알겠는데, 그래서 ViewController와 ViewModel을 어떻게 작성해야 하는거지?"

MVC에서 이제 막 MVVM으로 넘어와 RxSwift를 사용해 보시는 많은 분들이 어려워하시는 부분일 거에요. 사실 이 부분은 MVVM의 단점이기도 한데요, 정형화된 패턴이 없기에 개발자들 간에도 모두 스타일이 다르다는 것이죠. 그만큼 정답이 없는 문제이기도 합니다.

저도 공부를 하는 과정에서 많은 혼란을 겪었고, 처음에 참고했던 코드가 나중에는 그리 좋지 않은 예시였다는 것을 알게 되는 등, 시행착오를 많이 겪었어요.

그 과정에서 깨달았던 포인트를 몇 가지 말씀드리겠습니다.

1. ViewModel, ViewController에 있어야 하는 코드는?

- ViewController

  • 프로퍼티로 viewModel을 소유
  • binding 함수: self.viewModel.transform(_:)메서드를 사용하여 뷰의 input 스트림을 매개변수로 넘기고, 반환값인 Output 스트림을 뷰에 적절히 binding
  • 뷰 레이아웃 관련 코드

- ViewModel

  • 프로퍼티로 비즈니스 로직을 담당하는 Model 객체(Usecase)를 소유
  • transform 함수: Input 스트림을 받아서 Model을 통해 비즈니스 로직을 처리한 후, Operator를 통해 적절히 가공하여 반환
  • (필요시) 각종 뷰의 State를 저장하는 Subject 프로퍼티

2. Input - Output 모델링, transform(_:)

MVVM에 정형화된 문법은 없지만, 최소한 Input - Output 모델링을 구현한 코드가 개인적으로 읽기 좋았고, 제가 작성할 때에도 머릿속으로 흐름을 이해하기 좋았어요.

class MovieDetailViewModel {

    private let movieSearchService: MovieSearchService // 비즈니스 로직을 담당하는 객체(API Call)

    init(movieSearchService: MovieSearchService) {
        self.movieSearchService = movieSearchService
    }

    struct Input { // View에서 발생할 Input 이벤트(Stream)들
        let trigger: Observable<Void> // viewWillAppear와 같은 trigger event
    }
    struct Output { // NView에 반영시킬 Output Stream들
        let resultTitleText: Observable<String> // UILabel 등에 바인딩할 데이터 Stream
    }

    func transform(input: Input) -> Output { // 뷰의 Input을 받아 Output으로 변형하는 메서드
        let resultTitleText = input.trigger
            .flatMap { _ in
                self.movieSearchService.fetchMovieDetail()
            }
            .map { $0.title }

        return Output(resultTitleText: resultTitleText)
    }
}

간단한 예시 코드를 작성해 보았어요. 지금은 Input과 Output이 하나씩이지만, 여러 이벤트가 들어와 가공하고, 비즈니스 로직을 사용하고, 스트림 간의 합성 및 분리를 거치는 등 복잡한 작업을 한다면, 이렇게 Input과 Output을 정리해 놓는 것이 쓰는 사람도 읽는 사람도 명확하게 의도를 알 수 있게 됩니다.

3. 하나의 Stream -> 구독은 한 번 만!!

이 내용을 설명하기 위해 만들어본 개념도인데요,
뷰에서 시작된 Stream(사람 손 모양의 User Input)subscribe되는 곳이 어디인지 보이시나요? 실선을 쭉쭉 따라가다 보면, 다시 뷰로 돌아와서야 subscribe되는 것을 확인할 수 있습니다!

처음 Rx-MVVM을 구현한 코드에서 가장 많이 보이는 실수가 "모든 곳에서 subscribe"하는 것이었어요. 뷰에서 뷰모델로 이벤트 전달할 때 subscribe, 뷰모델에서 비즈니스 로직을 사용할 때 또 subscribe...
그렇게 하나의 스트림이 중간에 뚝 뚝 끊어지는 모습을 많이 보았는데요. 이는 전혀 Rx답지 못한 방식이고, MVVM의 개념에도 맞지 않아요.

subscribe는 Stream의 종착점인 ViewController에서, 한 번만 이루어지는 것이 좋습니다!

모든 스트림은 최종적으로 뷰의 변경을 일으키게 되지요. 그 종착점까지 가는 과정에서는 subscribe하지 않고도, Operator를 통해 충분히 데이터를 가공하거나 Stream을 합성 및 분리, 비즈니스 로직을 사용한 사이드이펙트를 반영할 수도 있습니다.

추천하고 싶은 예시 Repo

github.com/sergdort/CleanArchitectureRxSwift

저는 위 코드를 보고 Rx-MVVM과 Clean Architecture를 이해하는데 큰 도움을 받았습니다! 보면 볼 수록 모범적으로 잘 작성된 것 같아서 교과서로 삼고 있는데, 여러분께도 도움이 많이 되었으면 좋겠어요!

마치며

아직 공부하는 입장이기에, 오류나 다른 의견이 있다면 댓글을 통해 알려주시면 정말 감사하겠습니다.
읽어주셔서 고맙습니다!

0개의 댓글