최근 프로젝트를 진행하면서, Combine 을 사용하여 이벤트 처리를 하기 시작했습니다.
Combine 을 계속 사용하여, MVVM 패턴을 쓰면서 이런 생각이 들었습니다.
"점점 더 이벤트 처리가 많아지면, 어떻게 관리하지??"
그래서 바로 구글에 How to Control Combine in Swift 라고 검색을 한 결과, INPUT/OUTPUT 패턴 이라는 것을 알게 되었습니다.
읽어보니, 간단하면서 이벤트들을 관리하기 편해 보였습니다.
INPUT/OUTPUT 패턴은 View 로부터 받는 입력과 ViewModel 에서 내보내는 동작만 설정하면, 전체 이벤트들을 관리할 수 있는 패턴입니다.
그러면, INPUT/OUTPUT 패턴을 사용한 간단하게 데이터를 추가 / 삭제 해보겠습니다.
protocol ViewModelType {
associatedtype Input // View -> ViewModel
associatedtype Output // ViewModel -> View
func transform(input: AnyPublisher<Input, Never>) -> AnyPublisher<Output, Never>
}
Enum Input : View 로부터 들어오는 입력들
// 뷰로부터 들어오는 입력
enum Input {
case createData(Data)
case deleteData(Data)
}
Enum Output : View 로 나가는 출력들
// 뷰모델로부터 뷰로 나가는 출력
enum Output {
case loadData([Data]) // 전체 데이터 전달
case createFail(error: Error) // 추가 실패 이벤트
case deleteFail(error: Error) // 삭제 실패 이벤트
}
Output 변수 : PassthroughSubject 를 사용하여, View 로 이벤트를 전달하는 Subject
private let output: PassthroughSubject<Output, Never> = .init()
Subscriptions 변수 : 구독한 이벤트를 모아놓는 변수
private var subcriptions = Set<AnyCancellable>()
Transform 메서드 : INPUT 이벤트를 입력받아 -> OUTPUT 이벤트를 내보내는 메서드
@Inject var dataService: DataServiceProtocol // 의존성 주입 받는 서비스
func transform(input: AnyPublisher<Input, Never>) -> AnyPublisher<Output, Never> {
input.sink { [weak self] event in
guard let self = self else { return }
switch event {
case .createData(let data):
createData(data: data) // creation data
case .deleteData(let data):
deleteData(data: data) // delete data
}
}
.store(in: &subcriptions)
return output.eraseToAnyPublisher()
}
// 의존성 주입 받은 서비스에세 발생한 추가 이벤트를 전달 받는 메서드
private func createData(data: Data) {
dataService.createData(data)
.sink { [weak self] completion in
// 데이터 추가 실패시
if case .failure(let error) = completion {
self?.output.send(.createFail(error: error))
}
} receiveValue: { [weak self] datas in
self?.output.send(.loadData(datas))
}
.store(in: &subcriptions)
}
// 의존성 주입 받은 서비스에세 발생한 삭제 이벤트를 전달 받는 메서드
private func deleteData(data: Data) {
dataService.deleteData(data)
.sink { [weak self] completion in
// 데이터 삭제 실패시
if case .failure(let error) = completion {
self?.output.send(.deleteFail(error: error))
}
} receiveValue: { [weak self] datas in
self?.output.send(.loadData(datas))
}
.store(in: &subcriptions)
}
protocol DataServiceProtocol {
var datas: [Data] { get }
func createData(_ data: Data) -> AnyPublisher<[Data], Error> // 데이터 추가 메서드
func deleteData(_ data: Data) -> AnyPublisher<[Data], Error> // 데이터 삭제 메서드
}
final class DataService: DataServiceProtocol {
var datas: [Data]
init(datas: [Data] = []) {
self.datas = datas
}
/// 데이터가 추가된 Data 배열 값 발행 || 여기서는 실패할 가능성이 X이기 때문에 promise(.fail) 안 씀
func createData(_ data: Data) -> AnyPublisher<[Data], Error> {
return Future<[Data], Error> { [weak self] promise in
guard let self = self else { return }
datas.append(data)
promise(.success(datas)) // 데이터 추가 성공
}
.eraseToAnyPublisher()
}
/// 특정 데이터 삭제된 후 Data 배열 값 발행 || 여기서는 실패할 가능성이 X이기 때문에 promise(.fail) 안 씀
func deleteData(_ data: Data) -> AnyPublisher<[Data], Error> {
return Future<[Data], Error> { [weak self] promise in
guard let self = self else { return }
if let index = self.datas.firstIndex(where: { $0.id == data.id }) {
datas.remove(at: index)
promise(.success(datas))
}
}
.eraseToAnyPublisher()
}
}
ViewModel 로 데이터를 전달함.
private let viewModel = ViewModel()
View 에서는 ViewModel 로 이벤트를 전달할 Subject 를 생성해야함.
private let input: PassthroughSubject<ViewModel.Input, Never> = .init()
View 에서는 ViewModel 로 이벤트 구독을 관리할 Subscriber 를 생성해야함.
private var subscriptions = Set<AnyCancellable>()
View 에서 ViewModel 의 transform 메서드를 구독(= ViewModel 에서 발생하는 이벤트를 전달받기 위함.)
private func bind() {
let output = viewModel.transform(input: input.eraseToAnyPublisher())
output.sink { [weak self] event in
switch event {
case .loadData:
self?.tableView.reloadData()
case .createFail(error: let error):
print("#### \(error)")
case .deleteFail(error: let error):
print("#### \(error)")
}
}
.store(in: &subscriptions)
}
View 에서 추가 이벤트 발행
button.addAction(UIAction(handler: { _ in
let data = Data(title: "Hello World")
self.input.send(.createData(data)) // Data를 ViewModel에 이벤트 발행
}), for: .touchUpInside)
View 에서 삭제 이벤트 발행
extension View: UITableViewDelegate {
func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCell.EditingStyle, forRowAt indexPath: IndexPath) {
let item = viewModel.dataService.datas[indexPath.row]
if editingStyle == .delete {
input.send(.deleteData(item)) // Data를 ViewModel에 이벤트 발행
}
}
}
위와 같은 구조로 코드를 짜본 결과, 데이터를 추가/삭제 하는 이벤트들을 관리하기가 훨씬 편해졌다.
Input, Output 동작 외의 행동들을 방지할 수 있기 때문인 거 같다.
그러나, 관리하기 편하다는 장점들도 있는 한편, 단점도 있다.
간단하거나, 작은 규모의 프로젝트에서는 오버프로그래밍 인 것 같다...
관리해야할 이벤트가 많지 않은 경우에는 더 많은 코드를 사용해 작성하니 낭비라고 생각이 든다.
아무튼, 그러한 단점에도 불구하고, 관리하기가 편하다는 점 때문에 향후 프로젝트를 하거나, 리팩토링 할 때 사용을 고민해 봐야겠다.