final class DetailViewController: UIViewController {
private let viewModel = DetailViewModel()
private let disposeBag = DisposeBag()
var pokemonID: Int? {
didSet {
if let id = pokemonID {
viewModel.pokemonID.onNext(id)
}
}
}
private let pokemonStackView: UIStackView = {...}()
private let pokemonImageView: UIImageView = {...}()
private let nameStackView: UIStackView = {...}()
private let infoStackView: UIStackView = {...}()
private let idLabel: UILabel = {...}()
private let nameLabel: UILabel = {...}()
private let typeLabel: UILabel = {...}()
private let heightLabel: UILabel = {{...}()
private let weightLabel: UILabel = {...}()
override func viewDidLoad() {
super.viewDidLoad()
configureUI()
bind()
}
private func configureUI() {}
private func bind () {...}
지금은 과제 제출 전날 일요일 오후 11시 48분이라서 넘어가고싶었다너무졸리지만수정할ㅇ건수정해야지
문득 디테일뷰컨에서 포켓몬아이디를 프로퍼티로 두고 뷰모델로 데이터를 전달하는 방식이 어디서부턴가 잘못됐다고 느꼈다.. 아... 이것은 MVVM이 아니다..!
MVVM 패턴에서는 뷰컨이 뷰모델의 데이터 구독해서 UI를 업데이트 하는 역할을 맡고 있는데, 지금의 내 드러운 코드는 pokemonID를 뷰컨에서 관리하고, 뷰모델로 직접 데이터를 전달하고 있다.
이것이 아래의 문제를 야기할 수 있는데,
일단 MVVM 패턴을 수행해내야하는데 이게 제ㅣㄹ 큰 문제다. 뷰컨트롤러가 데이터를 직접 처리하고 뷰모델로 전달하는 것은 뷰컨이 비즈니스로직에 관여한다고 볼 수 있는데, MVVM패턴은 뷰컨이 온전히 받아들일 때 그 역할을 다 하는 것 이다.
데이터의 플로우를 담당하는 것은 뷰모델의 소행이다.
이건 아직 잘 와닿지 않는 문제인데 pokemonID가 변경될 떄마다 didSet에서 onNext로 데이터 전달하는데, 이 과정에서 유효성 검사가 부족하거나 뷰모델에서 데이터를 중복으로 처리할 가능성이있다고 한다..
아 그리고 뷰모델에 ID 직접 전달하지 않고, 뷰컨 통해서 설정하는 방식은 테스트를 어렵게 한다고 한다. 뷰모델을 테스트하려면 데이터 세팅을 별도로 해주어야한다는데 아주 거지같은 코드를 짰구나..
MVVM에서 뷰모델이 데이터를 관리하고, 뷰컨이 구독.하는 형태가 맞ㄷㅏ
그래서 뷰컨트롤러는 데이터를 설정하지 않고, 뷰모델을 초기화하거나 데이털르 주입받는 형태로 처리해야한다.
긍정적으로 생각해보니까 실수를 하게 되니까 패턴의 이해도가 조금 올라갔다.
근데 솔직히 이게 맞는지는 모르겠고 일단 제 자리를 찾아주는게 먼저라고 생각해서..수정을 해보자면
final class DetailViewModel {
private let networkManager = NetworkManager.shared
private let disposeBag = DisposeBag()
// 뷰컨으로
let pokemonName = PublishSubject<String>()
let pokemonType = PublishSubject<String>()
let pokemonHeight = PublishSubject<String>()
let pokemonWeight = PublishSubject<String>()
let pokemonImage = PublishSubject<UIImage>()
let error = PublishSubject<String>()
// 초기화 시점에 id를 전달????
init(pokemonID: Int) {
fetchPokemonDetail(id: pokemonID)
}
private func fetchPokemonDetail(id: Int) {
networkManager.fetch(endpoint: PokemonAPI.fetchPokemonDetail(id: id), type: PokemonDetail.self)
.subscribe(onSuccess: { [weak self] detail in
self?.pokemonName.onNext(detail.name)
self?.pokemonType.onNext(detail.types.joined(separator: ", "))
self?.pokemonHeight.onNext("\(detail.height)m")
self?.pokemonWeight.onNext("\(detail.weight)kg")
self?.fetchPokemonImage(for: id)
}, onFailure: { [weak self] error in
self?.error.onNext("포켓몬 데이터를 가져오는 데 실패했습니다: \(error.localizedDescription)")
})
.disposed(by: disposeBag)
}
private func fetchPokemonImage(for id: Int) {
networkManager.fetchImage(for: id)
.subscribe(onSuccess: { [weak self] image in
self?.pokemonImage.onNext(image)
}, onFailure: { error in
print("Failed to fetch image: \(error.localizedDescription)")
})
.disposed(by: disposeBag)
}
}
뷰컨에서는 뷰모델을 초기화??시점??에?? 읽는건가 아무튼 데이터를 구독받아서 UI를 업데이트 한다.
final class DetailViewController: UIViewController {
private let viewModel: DetailViewModel
private let disposeBag = DisposeBag()
private let pokemonStackView: UIStackView = { ... }()
private let pokemonImageView: UIImageView = { ... }()
private let nameLabel: UILabel = { ... }()
private let typeLabel: UILabel = { ... }()
private let heightLabel: UILabel = { ... }()
private let weightLabel: UILabel = { ... }()
// ViewModel 읽기
init(pokemonID: Int) {
self.viewModel = DetailViewModel(pokemonID: pokemonID)
super.init(nibName: nil, bundle: nil)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func viewDidLoad() {
super.viewDidLoad()
configureUI()
bindViewModel()
}
private func configureUI() {
view.backgroundColor = .mainRed
view.addSubview(pokemonStackView)
...
}
}
private func bindViewModel() {
viewModel.pokemonName
.observe(on: MainScheduler.instance)
.bind(to: nameLabel.rx.text)
.disposed(by: disposeBag)
viewModel.pokemonType
.map { "타입: \($0)" }
.observe(on: MainScheduler.instance)
.bind(to: typeLabel.rx.text)
.disposed(by: disposeBag)
viewModel.pokemonHeight
.map { "키: \($0)" }
.observe(on: MainScheduler.instance)
.bind(to: heightLabel.rx.text)
.disposed(by: disposeBag)
viewModel.pokemonWeight
.map { "몸무게: \($0)" }
.observe(on: MainScheduler.instance)
.bind(to: weightLabel.rx.text)
.disposed(by: disposeBag)
viewModel.pokemonImage
.observe(on: MainScheduler.instance)
.bind(to: pokemonImageView.rx.image)
.disposed(by: disposeBag)
}
}
일단 역할 분리는 한 것 같은데
그러니까 지금 뷰모델이 id를 직접 처리?하고.. 뷰컨이 UI 구독, 업데이트하는 것 같은데......음
그리고 그게 맞다면 불필요한 데이터 설정 코드가 제거 된거고....데이터 처리 과정을 독립적으로 검증할 수 있고..? 문제점이 해결 된 걸 지도..?
12시 34분이네