내용 정리
내가 만든 과제와 과제 해설 영상을 비교해보며 어떤 점이 다른지, 어떤 점을 배울 수 있는지 확인해보자.
이번 과제를 진행하며 나도 나름대로 MVVM 패턴을 준수하기 위해 구조도를 작성하고, 디렉토리를 분리해서 작업을 진행했다.
| 구조도 | 디렉토리 분리 |
|---|---|
![]() |
그러나 여전히 MVVM이라는 것 자체가 추상적으로 느껴져서 코드적으로 대입하기 어려웠던 것 같다. 코드로 작업을 하면서도 이게 맞는걸까? 하는 의문을 여러번 가졌고, 그럴 때마다 인터넷 등을 서치해서 MVVM의 정의를 찾아보았지만, 그럼에도 어렵게 느껴지는 개념이었다.
과제 해설 영상에서 MVVM에 대해 얘기를 했던 것 중 반복적으로 나오고 기억에 남는 것은 '뷰 컨트롤러가 ID 값을 가지고 있게 하고싶지 않다'라는 내용이었다. 뷰 컨트롤러는 그저 뷰를 보여주기 위한 역할로, 직접적으로 값을 가지지 않고 뷰 모델에게 전달하는 역할만을 가진다는 것이다.
이것이 MVVM의 핵심이라면 나는 잘 지켰을까 코드를 되돌아 보았다.
음... 일단은 지켜진 것 같긴 하다. 값을 직접적으로 가지지 않고 전달하는 방식을 사용했으니까. 다만, 클로저를 사용한 부분이 있어서 이것도 잘 지켜진건지 애매한 것 같다. 되도록 클로저 같은 방식이 아닌 rx를 통해 데이터를 주고받고 싶었는데 어려운 부분이 있어서 클로저로 해결했는데, 과제에서 rx에 대한 많은 내용이 나와서 리팩토링을 하려면 할 수 있을 것 같다.
다만 과제해설과 달리 나는 뷰 컨트롤러가 아닌 뷰가 뷰 모델을 관측하고 있다. 지금 생각해 보면 잘못된 방식이 아니었을까 생각 된다. MVVM의 구조를 생각해보면 View에 뷰 컨트롤러와 뷰들이 있지만, 뷰를 관리하는 것이 뷰 컨트롤러이기 때문에 뷰가 직접 뷰 모델을 관측하는 것이 아니라 뷰 컨트롤러가 뷰 모델을 관측하고, 변화를 뷰에 전달해줬어야 하는게 아닐까 생각한다.
과제 해설을 참고하며 어떻게 rx를 통해 이를 구현할 수 있을지 연습해보며 다음부터는 뷰 컨트롤러가 뷰 모델을 관측하고 뷰는 변화만 되도록 구현하는 방식으로 코드를 작성해봐야겠다고 생각했다.
과제 해설 영상을 보며 가장 놀란 부분은 역시 RxSwift를 활용하는 부분들이다. 과제 해설에서는 정말 거의 모든 기능을 RxSwift를 사용해서 구현하는데, 이렇게까지 사용할 수도 있구나 하고 놀랐던 것 같다.
나도 웬만한 부분들은 최대한 RxSwift만으로 구현할 수 있도록 노력했지만, 아직 모르는 부분도 많고 RxSwift를 알기 전의 코드 작성 방식이 익숙한 탓에 많이 활용하지 못했는데, 과제 해설을 보며 큰 도움을 받은 것 같다.
가장 인상 깊었던 부분은 뷰 모델의 로직을 구현하는 부분이었다.
class DetailViewModel {
private let repository = PokemonRepository()
private let pokemonId: Int
init(pokemonId: Int) {
self.pokemonId = pokemonId
}
func transform(_ input: Input) -> Output {
let pokemonDetail = input.load
.withUnretained(self)
.flatMapLatest { owner, _ in
owner.repository.fetchPokemonDetail(pokemonId: owner.pokemonId)
}
.asObservable()
return .init(pokemonDetail: pokemonDetail)
}
}
extension DetailViewModel {
struct Input {
let load: Observable<Void>
}
struct Output {
let pokemonDetail: Observable<PokemonDetail>
}
}
private func bind() {
let load = self.rx.viewWillAppear
let input = DetailViewModel.Input(load: load)
let output = viewModel.transform(input)
let detail = output.pokemonDetail.share()
detail
.map {
"No.\($0.id) \(PokemonTranslator.getKoreanName(for: $0.name))"
}
.bind(to: detailView.nameLabel.rx.text)
.disposed(by: disposeBag)
위의 코드는 과제 해설에서 뷰 모델을 만들 때 작성한 코드이다. RxSwift의 핵심은 데이터 스트림에서 어떤 값을 받고 이벤트를 방출 시키는 것이기 때문에 extension을 사용해서 Input과 Output 객체를 만들고, 이를 사용해서 로직을 처리하는 부분이 인상적이었다.
transform(_:)라는 메소드에서 Observable<Void> 타입의 값을 파라미터로 받고 이를 Output으로 원하는 옵저버블로 변환시켜 바인딩하여 사용하는게 RxSwift는 이렇게 사용하는거구나 하고 느꼈던 것 같다.
코드의 사용에는 다양한 방법이 있다고 하니, 또 어떤 식으로 RxSwift를 사용하는지 찾아보면서 나만의 스타일을 찾고 적용해보면 좋을 것 같다고 생각했다.
또 과제 해설을 보며 share, bind, asObservable 등 다양한 RxSwift의 코드들을 알 수 있어서 좋다고 생각했다.
과제 해설을 보고 내 코드도 리팩토링 해보면 좋겠지만... 그러기엔 추가한 것들이 너무나 많아서 쉽지 않을 것 같다고 생각했다.
대신 과제 해설에서 제공하는 코드를 리팩토링 해보자고 생각했다.
리팩토링이라고 하기에도 뭐하지만, MainViewModel과 DetailViewModel의 로직이 거의 비슷한데 파일을 따로 만들고 로직도 다시 생성하는 것이 비효율적이라고 생각했다.
MVVM의 장점 중 하나가 코드의 재활용성인데, 같은 로직을 두 번 사용하면 재활용성이 떨어지니 이를 해결해보고자 하였다.
protocol ViewModelProtocol: AnyObject {
typealias Input = Observable<Void>
typealias Output = Observable<Decodable>
func transform(_ input: Input) -> Output
}
class DetailViewModel: ViewModelProtocol {
private let repository = PokemonRepository()
private let pokemonId: Int
init(pokemonId: Int) {
self.pokemonId = pokemonId
}
func transform(_ input: Input) -> Output {
return input
.withUnretained(self)
.flatMapLatest { owner, _ in
owner.repository.fetchPokemonDetail(pokemonId: owner.pokemonId)
}
.map { $0 as PokemonDetail }
.asObservable()
}
}
쨘!... 이렇게까지 완성하고 나니 MainViewModel과 DetailViewModel의 로직은 조금 차이가 있다는 것을 알았다. 그래도 최대한 로직을 공유할 수 있도록 해보려고 했지만, 어려웠다...
결국 리팩토링에는 실패했지만 시도가 중요한게 아닐까? 아직 갈 길이 멀었으니 더 열심히 공부하자.
특히, 과제에서 처음 배운 RxSwift의 메소드들에 대해 더 공부해보면서 RxSwift를 익히면 좋을 것 같다.
RxSwift에 대해 조금 알았다고 생각했는데, 과제 해설을 보고 아직 갈 길이 멀었다고 느꼈다.
이제 막 배우기 시작한 참이니 조급해 하지 말고 천천히 배워가며 내 기술로 만들어 보자고 생각했다.
그리고 MVVM 패턴도 더 공부해서 익숙해지면 좋을 것 같다.
PokeDex
├── AppDelegate.swift
├── Base.lproj
│ └── LaunchScreen.storyboard
├── Info.plist
├── Model
│ ├── APIEndpoint.swift
│ ├── AlertManager
│ │ └── AlertManager.swift
│ ├── DataModel
│ │ ├── PokemonDataModel.swift
│ │ └── PokemonDetailDataModel.swift
│ ├── Network
│ │ ├── NetworkError.swift
│ │ └── NetworkManager.swift
│ ├── StorageManager
│ │ ├── CoreDataManaged.swift
│ │ └── CoreDataManager.swift
│ └── Translator
│ ├── PokemonTranslator.swift
│ └── PokemonTypeTranslator.swift
├── MyPokemon.xcdatamodeld
│ └── MyPokemon.xcdatamodel
│ └── contents
├── SceneDelegate.swift
├── View
│ ├── DetailView
│ │ ├── DetailViewController.swift
│ │ └── PokemonDetailView.swift
│ ├── MainView
│ │ ├── MainViewController.swift
│ │ ├── PokeDexView.swift
│ │ ├── PokemonCell.swift
│ │ └── PokemonCollectionView.swift
│ ├── MyPokemonView
│ │ ├── MyPokemonCell.swift
│ │ ├── MyPokemonView.swift
│ │ └── MyPokemonViewController.swift
│ ├── SearchView
│ │ ├── SearchTableView.swift
│ │ ├── SearchTableViewCell.swift
│ │ ├── SearchView.swift
│ │ └── SearchViewController.swift
│ ├── TabBar
│ │ ├── MainTabBarCell.swift
│ │ ├── MainTabBarController.swift
│ │ ├── MainTabBarView.swift
│ │ └── TabBarIndicator.swift
│ └── ViewController.swift
└── ViewModel
├── DetailViewModel.swift
├── MainViewModel.swift
├── PokemonServiceProtocol.swift
├── SearchViewModel.swift
└── TabBarViewModel.swift
지금의 구조는 뭔가... 별로 깨끗해 보이지는 않으니까...
RxSwift와 MVVM 정복까지 파이팅...!!
combine은 또 언제 배우지...