[Swift] MVVM 패턴과 RxSwift 를 활용해 영화앱을 구현해보자

팔랑이·2024년 8월 5일
0

iOS/Swift

목록 보기
56/71
post-thumbnail

이번에 MVVM 패턴을 처음으로 도입해보면서, RxSwift를 활용한 비동기 데이터 처리와 UI 바인딩을 경험했다.
MVVM 패턴은 뷰(View)와 뷰모델(ViewModel) 사이의 역할을 명확히 분리해서, 코드의 가독성과 유지보수성을 높이는 데 큰 도움이 된다.
MVVM 구조의 핵심 요소와 RxSwift의 사용법을 중심으로 배운 내용을 정리해본다.


1. MVVM 패턴의 구조 이해

MVVM 패턴에서 뷰(View)는 사용자와 상호작용하며, 그 결과를 뷰모델(ViewModel)에 전달한다.
반면, 뷰모델은 비즈니스 로직을 처리하고, 그 결과를 다시 뷰에 전달한다.
이때, 데이터 바인딩을 통해 뷰모델의 데이터가 변경되면 자동으로 뷰가 업데이트된다.

  • ViewModel: 비즈니스 로직을 담당하며, 네트워크 요청 등의 비동기 작업을 처리한다. 여기서는 영화 데이터를 불러오는 로직이 포함된다.
  • View: ViewModel과 데이터 바인딩을 통해 UI를 업데이트하며, 사용자 입력을 ViewModel에 전달한다.

2. RxSwift와의 연계

RxSwift를 통해 비동기 작업을 쉽게 처리하고, 데이터를 반응형으로 관리할 수 있다.
여기서 주요하게 사용된 RxSwift의 개념은 BehaviorSubjectSingle이다.

BehaviorSubject

ViewModel에서 각 영화 카테고리(인기 영화, 높은 평점 영화, 개봉 예정 영화)를 관리하는데에 BehaviorSubject가 사용되었다.

BehaviorSubject는 초기값을 가지며, 구독자가 추가되면 즉시 마지막으로 방출된 값을 받을 수 있다.
이 말은, 새로운 구독자가 생길 때마다 최신 데이터를 즉시 받을 수 있다는 의미이다.
예를 들어, 뷰컨트롤러가 ViewModel의 popularMovieSubject를 구독할 때, 이미 데이터를 방출한 상태라면 즉시 최신 영화 목록을 받을 수 있다.
즉 초기값이 필요하고, 항상 최신 상태를 보장하기 위해 BehaviorSubject를 사용.

참고로 PublishSubject는 구독 이후에 방출된 값만 받을 수 있다!

let popularMovieSubject = BehaviorSubject(value: [Movie]())

Single

Single은 단일 값을 방출하거나 에러를 방출하는 Observable이다.
주로 네트워크 요청에서 사용되며, 성공 시 단일 데이터를 반환한다
예를 들어, 특정 영화의 예고편 키를 가져올 때 Single을 사용한다.

func fetchTrailerKey(movie: Movie) -> Single<String> {
        guard let apiKey = apiKey, let movieId = movie.id else { return Single.error(NetworkError.dataFetchFail)}
        let urlString = "https://api.themoviedb.org/3/movie/\(movieId)/videos?api_key=\(apiKey)"
        guard let url = URL(string: urlString) else {
            return Single.error(NetworkError.invalidUrl)
        }
        
        return NetworkManager.shared.fetch(url: url) // 여기까지만 보면 리턴타입이 Single<VideoResponse>
        // Single의 리턴타입을 변경해주고 싶을 때, flatMap을 활용해 리턴타입을 String으로 변경
            .flatMap { (VideoResponse: VideoResponse) -> Single<String> in
                if let trailer = VideoResponse.results.first(where: {$0.type == "Trailer" && $0.site == "YouTube"}) {
                    guard let key = trailer.key else { return Single.error(NetworkError.dataFetchFail) }
                    return Single.just(key) // key를 활용해 예고편 영상을 틀 수 있다
                } else {
                    return Single.error(NetworkError.dataFetchFail)
                }
            }
    }
}

flatMap의 역할
flatMapObservable이나 Single의 방출된 값을 다른 Observable이나 Single로 변환할 때 사용된다. 여기서는 Single<VideoResponse>Single<String>으로 변환하기 위해 flatMap을 사용한다.

  • 타입 변환: NetworkManager.shared.fetch(url: url) 호출로부터 받은 VideoResponse 타입을 문자열(String) 타입으로 변환해야 할 필요가 있다. flatMap을 사용하면 VideoResponse를 분석해서, 원하는 조건(YouTube 예고편)을 만족하는 동영상의 키를 추출하고, 이를 새로운 Single으로 변환 가능.
  • 논리 추가: flatMap 안에서 조건을 체크할 수 있다. 예고편이 존재하는지, 그리고 YouTube 동영상인지 등을 확인하고, 조건에 맞는 경우에만 키를 방출한다. 그렇지 않으면 에러를 방출.

3. ViewModel의 비즈니스 로직 처리

ViewModel에서는 영화 데이터를 서버에서 가져와서 BehaviorSubject에 전달한다. 이를 통해 View에서 데이터를 구독하여 UI를 업데이트할 수 있다. 각 API 요청은 RxSwift의 Single로 처리되며, 성공 시 데이터를 BehaviorSubject로 방출하고, 실패 시 에러를 전달한다.

func fetchPopularMovie() {
    guard let apiKey = apiKey, let url = URL(string: "https://api.themoviedb.org/3/movie/popular?api_key=\(apiKey)") else {
        popularMovieSubject.onError(NetworkError.invalidUrl)
        return
    }

    NetworkManager.shared.fetch(url: url)
        .subscribe(onSuccess: { [weak self] (movieResponse: MovieResponse) in
            self?.popularMovieSubject.onNext(movieResponse.results)
        }, onFailure: { [weak self] error in
            self?.popularMovieSubject.onError(error)
        }).disposed(by: disposeBag)
}
  • 강한 참조 방지: [weak self]를 사용해 메모리 누수를 방지한다.
  • Error Handling: 네트워크 요청 실패 시 onError를 통해 에러를 방출한다.

4. View와 ViewModel의 데이터 바인딩

ViewController에서 ViewModel의 BehaviorSubject를 구독하고, 그 데이터가 변경될 때마다 UI를 업데이트한다.
이 부분에서 RxSwift의 observesubscribe를 활용하여 메인 스레드에서 UI 작업을 안전하게 처리한다.

viewModel.popularMovieSubject
    .observe(on: MainScheduler.instance)
    .subscribe(onNext: { [weak self] movies in
        self?.popularMovies = movies
        self?.collectionView.reloadData()
    }, onError: { error in
        print("에러 발생: \(error)")
    }).disposed(by: disposeBag)
  • UI 업데이트: onNext에서 전달된 데이터를 기반으로 컬렉션 뷰를 리로드한다.
  • 에러 처리: onError에서 에러를 처리하여 디버깅에 도움을 준다.

5. 데이터에 따른 UI 반응 처리

ViewController에서 특정 영화를 선택하면, 해당 영화의 예고편 키를 가져오고, 유튜브 플레이어 화면으로 전환하는 로직을 구현했다. 이를 통해 사용자가 선택한 영화에 따라 동적으로 다른 화면을 띄워주는 기능을 제공할 수 있다.

func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
    switch Section(rawValue: indexPath.section) {
    case .popularMovies:
        viewModel.fetchTrailerKey(movie: popularMovies[indexPath.row])
            .observe(on: MainScheduler.instance)
            .subscribe(onSuccess: { [weak self] key in
                self?.navigationController?.pushViewController(YoutubeViewController(key: key), animated: true)
            }, onFailure: { error in
                print("에러 발생: 33\(error)")
            }).disposed(by: disposeBag)
    // 다른 섹션도 유사하게 처리
    default:
        return
    }
}
  • 동적 화면 전환: 영화 선택 시 예고편 영상을 보여주는 화면으로 전환한다.
  • 에러 처리: 예고편 키를 가져오는 중 에러 발생 시 로그를 출력하여 문제를 파악한다.

결론

MVVM 패턴과 RxSwift를 결합해서 구현하는 것을 따라해 봤지만, 사실은 아직 RxSwift와 MVVM 패턴이 익숙하지 않아서 유용성을 완전히 체감하지는 못하고 있다. 하지만 많은 개발자들이 이 구조와 패턴의 장점을 이야기하는 만큼, 나도 점차 익숙해져서 그 유용성을 더욱 깊이 느껴보고 싶다. 시간이 지나면서 자연스럽게 내 프로젝트에 더 잘 녹아들길 기대한다.

profile
정체되지 않는 성장

0개의 댓글