[포켓몬 도감 앱 만들기] 리팩토링 2

황석범·2025년 1월 3일
0

내일배움캠프_iOS_5기

목록 보기
58/76

현재 구조

  • DetailViewModel과 MainViewControllerModel가 네트워크 요청을 직접 처리하고 있음,
  • 뷰모델이 네트워크 요청, 데이터 가공, 뷰와의 바인딩을 모두 담당
// DetailViewModel
final class DetailViewModel {
    
    private let disposeBag = DisposeBag()
    let pokemonDetailSubject = BehaviorSubject<PokemonDetail?>(value: nil)
    let pokemonImageRelay = PublishRelay<UIImage?>()
    
    func fetchPokemonDetail(for urlString: String) {
        guard let url = APIEndpoint.pokemonDetailURL(for: urlString) else {
            pokemonDetailSubject.onError(NetworkError.invalidUrl)
            return
        }
        
        NetworkManager.shared.fetch(url: url)
            .subscribe(onSuccess: { [weak self] (pokemonDetail: PokemonDetail) in
                self?.pokemonDetailSubject.onNext(pokemonDetail)
                self?.fetchPokemonImage(for: pokemonDetail.id)
            }, onFailure: { [weak self] error in
                self?.pokemonDetailSubject.onError(error)
            })
            .disposed(by: disposeBag)
    }
   
    func fetchPokemonImage(for id: Int) {
        NetworkManager.shared.fetchPokemonImage(for: id)
            .subscribe(onSuccess: { [weak self] image in
                self?.pokemonImageRelay.accept(image) 
            }, onFailure: { [weak self] error in
                self?.pokemonImageRelay.accept(nil)
            })
            .disposed(by: disposeBag)
    }
}

개선 방향

NetworkManger와 ViewModel사이에 UseCase을 계층 추가

  • 비즈니스 로직(데이터 가공 및 네트워크 요청)을 담당하는 UseCase 객체를 도입합니다.
  • UseCase는 데이터 요청, 가공, 캐싱 등을 담당하고, 뷰모델은 이를 호출하여 데이터를 받아옵니다.
// PokemonUseCase
final class PokemonUseCase {
    
    private let networkManager: NetworkManager
    private let disposeBag = DisposeBag()
    
    init(networkManager: NetworkManager = .shared) {
        self.networkManager = networkManager
    }
    
    func fetchPokemonList(limit: Int, offset: Int) -> Single<PokemonResponse> {
        guard let url = APIEndpoint.pokemonListURL(limit: limit, offset: offset) else {
            return .error(NetworkError.invalidUrl)
        }
        return networkManager.fetch(url: url)
    }
    
    func fetchPokemonDetail(for id: Int) -> Single<PokemonDetail> {
        guard let url = APIEndpoint.pokemonDetailURL(for: String(id)) else {
            return .error(NetworkError.invalidUrl)
        }
        return networkManager.fetch(url: url)
    }
    
    func fetchPokemonImage(for id: Int) -> Single<UIImage> {
        return networkManager.fetchPokemonImage(for: id)
    }
}
// DetailViewModel
final class DetailViewModel {
    
    private let disposeBag = DisposeBag()
    private let useCase: PokemonUseCase
    let pokemonDetailSubject = BehaviorSubject<PokemonDetail?>(value: nil)
    let pokemonImageRelay = PublishRelay<UIImage?>()
    
    init(useCase: PokemonUseCase) {
        self.useCase = useCase
    }
    
    func fetchPokemonDetail(for id: Int) {
        useCase.fetchPokemonDetail(for: id)
            .subscribe(onSuccess: { [weak self] detail in
                self?.pokemonDetailSubject.onNext(detail)
                self?.fetchPokemonImage(for: id)
            }, onFailure: { [weak self] error in
                self?.pokemonDetailSubject.onError(error)
            }).disposed(by: disposeBag)
    }
    
    func fetchPokemonImage(for id: Int) {
        useCase.fetchPokemonImage(for: id)
            .subscribe(onSuccess: { [weak self] image in
                self?.pokemonImageRelay.accept(image)
            }, onFailure: { [weak self] _ in
                self?.pokemonImageRelay.accept(nil)
            }).disposed(by: disposeBag)
    }
}

1. 의존성 분리

  • 뷰모델에서 네트워크 요청을 직접 처리하지 않고, UseCase 계층을 통해 데이터를 요청합니다.
  • 이를 통해 테스트 가능성과 확장성을 높입니다.

2. 공통 로직 추출

  • MainViewControllerModel과 DetailViewModel의 중복된 네트워크 요청 로직을 UseCase에 통합합니다.

기대 효과

1. 책임 분리

  • UseCase가 비즈니스 로직을 담당하므로 뷰모델이 더 가벼워지고 역할이 명확해집니다.

2. 재사용성

  • UseCase는 다른 뷰모델에서도 재사용 가능하며, 중복 코드가 줄어듭니다.
  • 뷰 모델과 뷰도 다른 UseCase를 사용하여 재사용 가능합니다.

3. 테스트 용이성

  • UseCase와 뷰모델을 독립적으로 테스트할 수 있습니다.

4. 확장성

  • 새로운 기능 추가 시 UseCase에 로직을 추가하면 뷰모델 수정이 최소화됩니다.
profile
iOS 공부중...

0개의 댓글

관련 채용 정보