본문 바로가기

Programming/iOS_Swift

RxSwift Input/Output 구조 사용하기

RxSwift와 MVVM을 프로젝트에 적용시키기 위해 다양한 글들을 읽어봤는데 가장 보편적으로 사용되는 패턴이 Input/Output 패턴인 것 같았다.

해당 패턴은 두 가지 방법으로 구현이 가능했는데

첫 번째는 ViewModel 내에서 Input, Output 객체를 생성하고 View에서 바인딩하는 방법

두 번째는 View에서 Input 객체를 생성하고 ViewModel의 transform의 함수를 호출해 Output 객체를 반환받아 사용하는 방법이 있었다.

개인적으로 View에서는 ViewModel의 데이터에 직접 접근해 수정이 가능하도록 만드는 것이 MVVM과는 살짝 거리가 있다고 생각해서 두 번째 방식을 선택했고 코드는 아래와 같다.

// ViewModel.swift
protocol ViewModel {
    associatedtype Input
    associatedtype Output
    
    var disposeBag: DisposeBag { get set }
    
    func transform(input: Input) -> Output
}

ViewModel이라는 이름의 Protocol을 생성한다.

Input과 Output의 형태를 ViewModel 내에서 정의할 수 있도록 associatedType으로 지정하고 transform 함수를 구현하도록 선언한다.

// MainChatViewModel.swift
class MainChatViewModel: ViewModel {
    struct Input {
        let inputMessage: Observable<String>
        let tapSend: Observable<Void>
        let tapChatTextField: Observable<Void>
    }
    
    struct Output {
        let replyArray: Observable<Array<ReplyModel>>
        let outputMessage: Observable<String>
        let isShowLoginAlert: Observable<Void>
    }
    
    var disposeBag = DisposeBag()
}

실제 ViewModel은 ViewModel 프로토콜을 채택하고 Input, Output, disposeBag을 정의해준다.

이 패턴의 가장 큰 지향점은 모든 사용자 이벤트를 ViewModel로 넘겨 비즈니스 로직을 ViewModel에서만 처리하도록 하는 것이기 때문에 버튼의 탭 이벤트, 텍스트필드의 입력 이벤트 등을 전부 Input에 정의하고 View로 넘겨줄 데이터들을 Output에 정의해주면 된다.

// MainChatViewModel.swift
private let replyArray = BehaviorRelay(value: Array<ReplyModel>())
private let inputMessage = BehaviorRelay(value: "")
private let isShowLoginAlert = PublishRelay<Void>()

func transform(input: Input) -> Output {
        input.inputMessage.bind(to: inputMessage).disposed(by: disposeBag)

        loadData()
        
        input.tapSend.subscribe(
            onNext: sendChat
        ).disposed(by: disposeBag)
        
        input.tapChatTextField.bind(to: isShowLoginAlert).disposed(by: disposeBag)
        
        return Output(replyArray: replyArray.asObservable(), outputMessage: inputMessage.asObservable(), isShowLoginAlert: isShowLoginAlert.asObservable())
    }

그리고 transform에서는 input으로 받은 이벤트나 데이터 등을 처리해 Output으로 반환하도록 구현한다.

뷰모델 내에서 저장하거나 사용해야하는 데이터들은 뷰모델의 멤버 변수로 정의해 저장하거나 구독했다.

이 부분에서 굳이…? 싶었지만 안드로이드의 AAC에서 MutableLiveData와 LiveData를 각자 생성하는 것과 비슷하다고 생각하니 이해할 수 있었다.

// MainChatView.swift
private let viewModel = MainChatViewModel()

private func bindViewModel() {
        let input = MainChatViewModel.Input(
            inputMessage: textFieldReply.rx.text.orEmpty.asObservable(),
            tapSend: buttonSend.rx.tap.asObservable(),
            tapChatTextField: textFieldReply.rx.controlEvent(.touchDown).asObservable()
        )
        let output = viewModel.transform(input: input)
        
        output.replyArray.bind(to: tableViewReply.rx.items(cellIdentifier: "ChatBubbleCell", cellType: ChatBubbleCell.self)) { (index: Int, element: ReplyModel, cell: ChatBubbleCell) in
            cell.setContent(content: element.content)
        }.disposed(by: disposeBag)
        
        output.outputMessage.bind(to: textFieldReply.rx.text).disposed(by: disposeBag)
        
        output.isShowLoginAlert.subscribe(
            onNext: mainViewModel.showAlert
        ).disposed(by: disposeBag)
    }

그리고 View의 bindViewModel 함수에서 Input 객체를 생성하고 transform 함수를 통해 Output 객체를 리턴받아 사용하면 된다.

처음 이 구조를 접하고 구현하고 시뮬레이터를 돌려볼 때 까지만 해도 굳이 이렇게까지 구현할 필요가 있을까? 했는데

View에서는 완전히 Snapkit으로 뷰를 구현하는 코드만 남고, 모든 처리가 ViewModel에 들어가니 코드 가독성이나 유지보수 측면에서 상당한 이득을 얻을 수 있었다.


참고자료

https://mrgamza.tistory.com/509

https://blog.gangnamunni.com/post/HealingPaperTV-ViewModel-Test/