개발공방

[애니또] MVVM + Combine - 리팩토링 (3) 본문

Swift/UIKit

[애니또] MVVM + Combine - 리팩토링 (3)

chemi_ 2023. 6. 15. 18:05

노션에 먼저 작성한 후 블로그로 옮기기 때문에 노션이 보기에 더 편합니다.
https://www.notion.so/MVVM-Combine-323ab64639374213b1b1734fcea257e7?pvs=4 

 


[애니또] ViewController와 View의 분리 - 리팩토링 (1)

 

[애니또] ViewController와 View의 분리 - 리팩토링 (1)

현재 애니또 iOS팀이 직면한 문제 현재까지 개발하면서 많은 문제와 버그들을 만났다. 첫 문제들을 해결하는데는 어려움이 없었지만, 앱의 규모가 조금씩 커지면서 문제들이 서로 엮이기 시작했

dev-workplace.tistory.com

 

앞전에 작성한 글을 보면, UIViewController에서 View를 분리한 과정을 볼 수 있다. Delegate 패턴을 통해 데이터와 액션을 주고 받았다.

 

DetailWaitView를 MVC패턴에서 MVVM으로 변경하는 과정 중 먼저 한 작업은 네트워크 통신을 통해 방 정보를 불러와서 View에 바인딩하는 작업이었다.

기존의 방식과 크게 달라진 부분은 없지만, 핵심은 Publisher를 사용한 Stream으로 만들었다는 것이다.

이해를 돕기위해 기존 MVC 코드부터 살펴보자.

// 기존 코드

final class DetailWaitViewController: BaseViewController {

    private var roomInformation: Room?

    override func viewDidLoad() {
        super.viewDidLoad()
        self.fetchRoomData()
        // 생략
    }

    // MARK: func

    private func fetchRoomData() {
        self.requestWaitRoomInfo() { [weak self] result in
            switch result {
            case .success(let room):
                DispatchQueue.main.async {
                    self?.detailWaitView.updateDetailWaitView(room: room)
                }
            case .failure:
                self?.makeAlert(title: TextLiteral.errorAlertTitle,
                                message: TextLiteral.detailWaitViewControllerLoadDataMessage)
            }
        }
    }

    // MARK: network

    private func requestWaitRoomInfo(completionHandler: @escaping ((Result<Room, NetworkError>) -> Void)) {
        Task {
            do {
                let data = try await self.detailWaitService.getWaitingRoomInfo(roomId: self.roomIndex.description)
                if let roomInfo = data {
                    self.roomInformation = roomInfo
                    completionHandler(.success(roomInfo))
                }
            } catch NetworkError.serverError {
                completionHandler(.failure(.serverError))
            } catch NetworkError.clientError(let message) {
                completionHandler(.failure(.clientError(message: message)))
            }
        }
    }
}

간단하게 핵심은 총 3개다.

  1. ViewDidLoad단계에서 fetchRoomData 함수 실행
  2. Completion Handler를 통해 불러온 방 정보를 view에 업데이트
  3. roomInformation 변수에 데이터 할당

위 코드를 보면 ViewDidLoad 단계에서 fetchRoomData() 함수를 실행하고, 불러온 room 데이터를 self?.detailWaitView.updateDetailWaitView(room: room) 해당 뷰에 업데이트 시킨다.

그리고 방 정보를 self.roomInformation = roomInfo 변수에 저장한다. 해당 방 정보를 변수에 저장하는 이유는 초대코드를 복사하는 버튼을 눌렀을 때, 초대코드를 네트워크 통신을 통해 받아오는 것이 아니라 이미 받아왔던 데이터에서 초대 코드를 사용해 복사한다.

guard let invitationCode = self.roomInformation?.invitation?.code else { return }

이 코드 때문에 네트워크 통신으로 받아온 방 정보를 변수에 저장하고 있다.

 

 

위의 flow가 기존의 방 정보를 불러와서 View에 연결하는 코드였다.

그렇다면 이젠 MVVM으로 어떻게 변경했을까?

우선 DetailWaitViewModel을 만들었다.

 

// DetailWaitViewModel.swift

final class DetailWaitViewModel {

    var roomInformation = CurrentValueSubject<Room?, Never>(nil)

    func fetchRoomInformation() {
        requestWaitRoomInfo { [weak self] result in
            switch result {
            case .success(let roominformation):
                self?.roomInformation.send(roominformation)
            case .failure(let error):
                print(error)
            }
        }
    }

    // MARK: network

    private func requestWaitRoomInfo(completionHandler: @escaping ((Result<Room, NetworkError>) -> Void)) {
        Task {
            do {
                let data = try await self.detailWaitService.getWaitingRoomInfo(roomId: self.roomIndex.description)
                if let roomInformation = data {
                    completionHandler(.success(roomInformation))
                }
            } catch NetworkError.serverError {
                completionHandler(.failure(.serverError))
            } catch NetworkError.clientError(let message) {
                completionHandler(.failure(.clientError(message: message)))
            }
        }
    }
}

우선 방 정보에 대한 Stream을 만들었다. var roomInformation = CurrentValueSubject<Room?, Never>(nil)

case .success(let roominformation):
    self?.roomInformation.send(roominformation)

네트워크 통신이 정상적으로 이루어지면, send 를 통해 값을 Stream에 전달함으로서 이벤트 발생을 알린다.

 

그리고 ViewController에서는 뷰에 바인딩을 해준다.

// DetailWaitViewController.swift

final class DetailWaitViewController: BaseViewController {

    // MARK: - property
    
    private var cancellable = Set<AnyCancellable>()
    private let detailWaitViewModel: DetailWaitViewModel

    // MARK: - init
    
    init(roomIndex: Int) {
        self.detailWaitViewModel = DetailWaitViewModel(roomIndex: roomIndex, detailWaitService: DetailWaitAPI(apiService: APIService()))
        super.init()
    }
    
    override func viewDidLoad() {
        super.viewDidLoad()
        self.setBind()
        // 생략
    }

    // MARK: func

    private func setBind() {
        self.detailWaitViewModel.roomInformation
            .receive(on: DispatchQueue.main)
            .sink(receiveValue: { [weak self] room in
                if let room {
                    self?.detailWaitView.updateDetailWaitView(room: room)
                }
            })
            .store(in: &self.cancellable)
    }
}

이렇게만 하면 양방향 바인딩이 완료된다.

ViewModel에서는 이벤트가 발생했음을 Stream에 전달하는 역할에서 끝이고, ViewController는 해당 Stream에 이벤트가 발생했을 때 실행되는 함수를 연결함으로서 역할이 끝이다.

 

기존 코드에서는 네트워크 함수 호출 → 데이터 불러오기 시도 → UI 업데이트 이 세 역할이 하나의 flow로서 분리가 어려웠는데,
MVVM에서는 1. 네트워크 함수 호출 → 이벤트 발생 , 2. 이벤트가 발생했을 때 → UI업데이트 로써 각각의 역할이 분리되었다.

 

 

이번 게시글엔 대기중인 방의 정보를 불러와서 바인딩 하는 간단한 과정을 살펴보았다.
다음으론 각각의 액션(Input)과 결과(Output)을 어떻게 관리하고 처리했는지 살펴보겠다.

Comments