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

황석범·2025년 1월 19일
0

내일배움캠프_iOS_5기

목록 보기
65/76

수정하고 싶은 부분

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 urlString: String) -> Single<PokemonDetail> {
        guard let url = APIEndpoint.pokemonDetailURL(for: urlString) else {
            return .error(NetworkError.invalidUrl)
        }
        return networkManager.fetch(url: url)
    }
    
    func fetchPokemonImage(for id: Int) -> Single<UIImage> {
        return networkManager.fetchPokemonImage(for: id)
    }
}

MainViewModel

final class MainViewModel {
    
    private let disposeBag = DisposeBag()
    private let useCase: PokemonUseCase
    
    let pokemonListSubject = BehaviorSubject<[Pokemon]>(value: [])
    let pokemonImagesSubject = BehaviorSubject<[Int: UIImage?]>(value: [:])
    let pokemonSelected = PublishSubject<Pokemon>()
    
    private var currentPage = 0
    private let limit = 20
    private var isLoading = false
    
    init(useCase: PokemonUseCase) {
        self.useCase = useCase
    }

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
    }

MainViewModel과 DetailViewModel이 같은 UseCase를 주입 받고 있다. MainViewModel과 DetailViewModel이 같은 PokemonUseCase를 공유하면서, 각 뷰 모델이 자신에게 필요하지 않은 메서드에 접근 가능한 문제를 해결하고 싶다.


개선 방향

1. UseCase 인터페이스 분리

  • PokemonUseCase의 기능을 두 가지 프로토콜로 분리
    • PokemonListUseCaseProtocol: 포켓몬 목록 관련 메서드 정의
    • PokemonDetailUseCaseProtocol: 포켓몬 상세 정보 관련 메서드 정의

1) 책임 분리

  • 프로토콜은 구현체(PokemonUseCase)와 분리된 역할을 가짐

UseCaseProtocols.swift

import UIKit
import RxSwift

protocol PokemonListUseCaseProtocol {
    func fetchPokemonList(limit: Int, offset: Int) -> Single<PokemonResponse>
    func fetchPokemonImage(for id: Int) -> Single<UIImage>
}

protocol PokemonDetailUseCaseProtocol {
    func fetchPokemonDetail(for urlString: String) -> Single<PokemonDetail>
    func fetchPokemonImage(for id: Int) -> Single<UIImage>
}

2. PokemonUseCase 수정

  • PokemonUseCase가 ViewModel 기능 별로 분리한 프로토콜을 준수하도록 구현

PokeMonUseCase.swift

final class PokemonUseCase: PokemonListUseCaseProtocol, PokemonDetailUseCaseProtocol {

...


}

3. ViewModel에 필요한 프로토콜만 주입

1) MainViewModel.swift

final class MainViewModel {
    
    private let disposeBag = DisposeBag()
    private let useCase: PokemonListUseCaseProtocol
    
    let pokemonListSubject = BehaviorSubject<[Pokemon]>(value: [])
    let pokemonImagesSubject = BehaviorSubject<[Int: UIImage?]>(value: [:])
    let pokemonSelected = PublishSubject<Pokemon>()
    
    private var currentPage = 0
    private let limit = 20
    private var isLoading = false
    
    init(useCase: PokemonListUseCaseProtocol) {
        self.useCase = useCase
    }

2) DetailViewModel.swift

final class DetailViewModel {
    
    private let disposeBag = DisposeBag()
    private let useCase: PokemonDetailUseCaseProtocol
    let pokemonDetailSubject = BehaviorSubject<PokemonDetail?>(value: nil)
    let pokemonImageRelay = PublishRelay<UIImage?>()
    
    init(useCase: PokemonDetailUseCaseProtocol) {
        self.useCase = useCase
    }

개선 효과

  1. ViewModel 간 결합도 감소
  • ViewModel은 자신이 필요한 기능만 접근
  1. 확장성
  • 프로토콜과 구현체를 독립적으로 확장 가능
  1. 테스트 용이성
  • Mock UseCase를 생성해 ViewModel 테스트를 쉽게 수행 가능
profile
iOS 공부중...

0개의 댓글

관련 채용 정보