[RxSwift] 14. RxSwift + URLSession2, MVVM 리팩토링

miori·2022년 2월 27일
0

RxSwiftBasic

목록 보기
26/29

RxSwift를 21일간 공부하는 루틴
"Rx를 기깔나게 쓰는 신입개발자 도전" 시작 🚀 ; 마지막날 🎓


이전 글에서는 rx + URLSession 을 정리했었다.
그 결과, api 통신후 원하는 데이터값을 가져올수 있었다.

이번엔 가져온 데이터를 테이블뷰 셀에 뿌린 다음, 이 과정을 mvvm 패턴으로 리팩토링해보겠다.

결과물

결과물은 다음과 같다,
[영화 제목]과 [랭킹에 신규진입여부(old/new)] 를 표시가 된다.
그리고 랭킹에 신규진입여부의 값이 new일 경우, label의 색을 다르게 하여 강조한다.

1. 테이블뷰셀에 데이터 표시하기

- 코드

//MARK: data -> cell에 뿌리기
BoxOfficeNetwork().getBoxOffice()
    //single -> Observable
    .asObservable()
    .share()
    // 성공 값 value 꺼내기
    .compactMap { data -> BoxOfficeResponse? in
        guard case let .success(value) = data
        else {
            return nil
        }
        return value
    }
    // 꺼낸 response -> weeklyBoxOfficeList 변형
    .map { $0.boxOfficeResult.weeklyBoxOfficeList}
    // drive로 셀에 뿌리기
    .asDriver(onErrorJustReturn: [])
    .drive(tableView.rx.items(cellIdentifier: BoxOfficeTableViewCell.registerID, cellType: BoxOfficeTableViewCell.self)) { [weak self] row, element, cell in
        cell.setData(element)
    }
    .disposed(by: disposeBag)

우선 getBoxOffice() 는 지난 글에서 작성했던 것 처럼, session.rx.data를 활용해 결과 json을 디코딩해주는 함수이다.

이제 rx 연산자를 활용하여, 필요한 데이터 형식으로 변형을 시키고 cell에 bind를 해보겠다.

.compactMap

.compactMap 연산자 코드 블록에 대해 정리하겠다.
이 연산자를 통해 Result<BoxOfficeResponse, URLError> 를 성공시에, BoxOfficeResponse 로 변경 시킬것이다.

.compactMap 연산자를 활용한다면 1)실패일때와 2)성공이지만, 데이터가 없는 경우도 표현하여 에러처리를 할 수 있다.

.map

map 연산자를 통해 BoxOfficeResponse를 실제 셀에 표현해줄 객체인 weeklyBoxOfficeList 로 변경해줄 수 있다.

.drive

.drive 를 통해 셀에 데이터를 표시해 줄 수 있다.

setData() 함수 소개

표시할데이터 객체를 파라미터로 받아 셀의 label.text에 뿌려주는 함수이다.
커스텀한 tableViewCell 코드에서 작성하였다.

func setData(_ dataEntity : BoxOfficeList) {
    movieNameLabel.text = dataEntity.movieNm
    oldnewLabel.text = dataEntity.rankOldAndNew
    
    if dataEntity.rankOldAndNew == "NEW" {
        oldnewLabel.textColor = .orange
    }
}

뷰컨트롤러에서 위에 있는 테이블뷰에 데이터를 표시하는 observable코드를 적으면 뷰컨트롤러가 상당히 보기 불편해진다.

그래서 mvvm으로 리팩토링을 해보겠다.

2. MVVM 리팩토링

- Model

이미 앞에서, 받아올 데이터 객체들을 model로 만들어놓았어서, 그대로 사용할것이다.

그래도, url + RxSwift를 마지막으로 정리하는 글이니, 중요한 관련 코드를 다 올려보도록 하겠다.

포스트맨으로 확인한 json 결과값을 토대로 struct를 구현하였다.

구현한 struct 이다.

struct BoxOfficeResponse : Decodable {
    let boxOfficeResult : BoxOfficeDetail
}

struct BoxOfficeDetail : Decodable {
    let weeklyBoxOfficeList : [BoxOfficeList]
}

struct BoxOfficeList : Decodable {
    let movieNm : String
    let rankOldAndNew : String
}

- ViewModel

위에서 적은 코드를 분리해보았다.

셀에 데이터를 표시해줄때는 mainThread 에서 작동해야하기 때문에 detailListCellData를 Driver로 하였다.

그다음 받아온 api값은 PublishSubject로 구현하여, 구독 후 next 이벤트가 발생하면 값을 확인할수 있게 하였다.

struct BoxOfficeViewModel {
    let disposeBag = DisposeBag()
    // 셀에 뿌릴값
    let detailListCellData : Driver<[BoxOfficeList]>
    // 받아온값
    let apiData = PublishSubject<[BoxOfficeList]>()
    
    init(_ networkModel : BoxOfficeNetwork = BoxOfficeNetwork()) {
        
        //네트워크값 observable로
        let boxOfficeResult = networkModel.getBoxOffice()
            .asObservable()
            .share()
        
        // success일때 value 꺼내고 nil 제거
        let boxOfficeResultValue = boxOfficeResult
            .compactMap { data -> BoxOfficeResponse? in
                guard case let .success(value) = data
                else {
                    return nil
                }
                return value
            }
        
        // respose 형태 중 weeklyBoxOfficeList로 map
        boxOfficeResultValue
            .map { $0.boxOfficeResult.weeklyBoxOfficeList }
            .bind(to: apiData)
            .disposed(by: disposeBag)

        self.detailListCellData = apiData
            .asDriver(onErrorJustReturn: [])
    }
}
  • BoxOfficeNetwork는 네트워크 통신후, session.rx.data를 통해 json을 디코딩하는 함수가 있는 클래스이다.

  • 최종적으로 아래 코드를 통해 뷰에서 데이터와 테이블뷰셀을 bind 할 수 있게 하였다.

self.detailListCellData = apiData
            .asDriver(onErrorJustReturn: [])

- View

func bind(_ viewModel : BoxOfficeViewModel) {
    //MARK: MVVM 합체
    // viewModel에서 driver였던 detailListCellData 와 tableView.rx.items 합체
    viewModel.detailListCellData
        .drive(tableView.rx.items(cellIdentifier: BoxOfficeTableViewCell.registerID, cellType: BoxOfficeTableViewCell.self)) { [weak self] row, element, cell in
            cell.setData(element)
        }
        .disposed(by: disposeBag)
}

viewModel을 받아서 bind하는 함수이다.

- sceneDelegate

    let rootViewModel = BoxOfficeViewModel()

    func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
        // Use this method to optionally configure and attach the UIWindow `window` to the provided UIWindowScene `scene`.
        // If using a storyboard, the `window` property will automatically be initialized and attached to the scene.
        // This delegate does not imply the connecting scene or session are new (see `application:configurationForConnectingSceneSession` instead).
        guard let windowScene = (scene as? UIWindowScene) else { return }
        self.window = UIWindow(windowScene: windowScene)
        
        let rootViewController = BoxOfficeVC()
       rootViewController.bind(rootViewModel)
        window?.rootViewController = UINavigationController(rootViewController: rootViewController)
        window?.makeKeyAndVisible()
    }

sceneDelegate 에서 let rootViewModel = BoxOfficeViewModel()rootViewController.bind(rootViewModel) 를 추가하여 뷰컨트롤러의 bind함수를 호출하였다.


Rx + MVVM 리팩토링 느낀점

Rx를 사용하니 확실히 MVVM 적용이 편해지고, 전체적으로 코드가 직관적으로 변한 느낌이다.
특히 ViewModel을 View에 바인딩하는 과정에서 Rx를 사용하니 훨씬 MVVM 으로 리팩토링하는게 수월하게 느껴졌다.
왜냐하면 서로 뷰모델에서 내용이 업데이트 되면 이벤트가 전달되고 bind를 통해 뷰에 표시할수 있기 때문이다.

profile
iS를 공부하는 miori 입니다.

0개의 댓글