이번에 MVVM 패턴을 처음으로 도입해보면서, RxSwift를 활용한 비동기 데이터 처리와 UI 바인딩을 경험했다.
MVVM 패턴은 뷰(View)와 뷰모델(ViewModel) 사이의 역할을 명확히 분리해서, 코드의 가독성과 유지보수성을 높이는 데 큰 도움이 된다.
MVVM 구조의 핵심 요소와 RxSwift의 사용법을 중심으로 배운 내용을 정리해본다.
MVVM 패턴에서 뷰(View)는 사용자와 상호작용하며, 그 결과를 뷰모델(ViewModel)에 전달한다.
반면, 뷰모델은 비즈니스 로직을 처리하고, 그 결과를 다시 뷰에 전달한다.
이때, 데이터 바인딩을 통해 뷰모델의 데이터가 변경되면 자동으로 뷰가 업데이트된다.
RxSwift를 통해 비동기 작업을 쉽게 처리하고, 데이터를 반응형으로 관리할 수 있다.
여기서 주요하게 사용된 RxSwift의 개념은 BehaviorSubject
와 Single
이다.
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의 역할
flatMap
은Observable
이나Single
의 방출된 값을 다른Observable
이나Single
로 변환할 때 사용된다. 여기서는Single<VideoResponse>
를Single<String>
으로 변환하기 위해flatMap
을 사용한다.
- 타입 변환: NetworkManager.shared.fetch(url: url) 호출로부터 받은 VideoResponse 타입을 문자열(String) 타입으로 변환해야 할 필요가 있다. flatMap을 사용하면 VideoResponse를 분석해서, 원하는 조건(YouTube 예고편)을 만족하는 동영상의 키를 추출하고, 이를 새로운 Single으로 변환 가능.
- 논리 추가: flatMap 안에서 조건을 체크할 수 있다. 예고편이 존재하는지, 그리고 YouTube 동영상인지 등을 확인하고, 조건에 맞는 경우에만 키를 방출한다. 그렇지 않으면 에러를 방출.
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]
를 사용해 메모리 누수를 방지한다.onError
를 통해 에러를 방출한다.ViewController에서 ViewModel의 BehaviorSubject
를 구독하고, 그 데이터가 변경될 때마다 UI를 업데이트한다.
이 부분에서 RxSwift의 observe
와 subscribe
를 활용하여 메인 스레드에서 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)
onNext
에서 전달된 데이터를 기반으로 컬렉션 뷰를 리로드한다.onError
에서 에러를 처리하여 디버깅에 도움을 준다.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 패턴이 익숙하지 않아서 유용성을 완전히 체감하지는 못하고 있다. 하지만 많은 개발자들이 이 구조와 패턴의 장점을 이야기하는 만큼, 나도 점차 익숙해져서 그 유용성을 더욱 깊이 느껴보고 싶다. 시간이 지나면서 자연스럽게 내 프로젝트에 더 잘 녹아들길 기대한다.