ViewModel Output의 필요성
이전 포스팅인 Enum을 이용한 ViewModel Action 정의을 보시면 한 가지 의문이 생길 수 있습니다.
바로 메서드의 반환 값이 다른 경우입니다.
예를 들어 아래와 같은 action 메서드가 있을 때
func action(_ actions: ViewModelActions) {
switch actions {
case .save(let data):
save(data)
case .delete(let data):
delete(data)
case .deleteAll:
deleteAll()
}
}
save, delete, deleteAll 메서드가 각각 다른 타입의 반환값을 가진다면 어떻게 해야할까요?
이때 필요한게 ViewModelOutputs 입니다.
ViewModelAction들이 반환하는 타입도 Enum으로 정의하여 사용하는 방식입니다.
(저는 보통 ~~~ViewModelActionOutputs 라고 네이밍 하는데요.
이번 포스팅에서는 간략하게 ViewModelOutputs 라고 작성하도록 하겠습니다.)
ViewModelActions 반환값 설정
먼저 지난 포스팅에서 사용한 save, delete, deleteAll 메서드에 반환값을 넣읍시다.
private func save(_ data: Int) -> Bool {
print("Save \(data)")
array.append(data)
return true
}
private func delete(_ data: Int) -> Bool {
print("Delete \(data)")
guard let index = array.firstIndex(of: data) else {
return false
}
array.remove(at: index)
return true
}
private func deleteAll() -> Int {
print("Delete All Data")
let count = array.count
array = []
return count
}
save, delete는 액션의 성공 여부를,
deleteAll에는 삭제한 데이터 개수를 반환하도록 했습니다.
세 개의 반환 타입이 모두 다르기 때문에 action 메서드에서 직접 타입을 반환할 순 없습니다.
그렇다고 반환 타입별로 action 메서드를 분리한다면 action 메서드를 사용하는 의미가 흐려지죠.
이 문제를 ViewModelOutputs로 해결할 수 있습니다.
ViewModelOutputs 정의
ViewModelOutputs를 정의해 봅시다.
enum ViewModelOutputs {
case save(Bool)
case delete(Bool)
case deleteAll(Int)
}
case 이름은 액션 이름으로 설정하고, 연관값으로 반환할 타입을 넣었습니다.
저는 어떤 메서드의 output인지 명확하게 하고 싶어서 위와 같은 네이밍을 했습니다만,
이건 사용하는 측에서 적절한 정책을 정하면 됩니다.
예를 들어, case 이름을 반환 타입의 특징으로 설정할 수도 있습니다.
enum ViewModelOutputs {
case isSuccess(Bool)
case numberOfDeletedItems(Int)
}
isSuccess는 save와 delete 메서드에서 사용할 수 있겠고,
numberOfDeletedItems는 deleteAll 메서드에서 사용할 수 있습니다.
이런 방식은 반환값에 공통점이 있다면 하나의 케이스로 묶을 수 있다는 장점이 있습니다.
대신 반환 값이 달라진다거나, 애매한 기준으로 묶는다면 명확성이 떨어진다는 단점이 있습니다.
이번 포스팅에서는 후자로 해보겠습니다 ㅎㅎ
마지막으로 ViewModelOutputs의 연관값을 편하게 쓸 수 있도록 메서드를 하나 추가합니다.
enum ViewModelOutputs {
case isSuccess(Bool)
case numberOfDeletedItems(Int)
func value() -> Any {
switch self {
case .isSuccess(let isSuccess):
return isSuccess
case .numberOfDeletedItems(let number):
return number
}
}
}
이렇게 ViewModelOutputs를 사용하는 곳에서는
switch문 없이도 연관값을 사용할 수 있게 되었습니다.
(참고로 여기서 한 단계 더 나아가면
연관값을 Result 타입으로 설정하여 Error도 함께 전달하는 방식으로 응용할 수도 있습니다.)
ViewModelOutputs 사용
이제 action 메서드에 ViewModelOutputs를 적용합시다.
func action(_ actions: ViewModelActions) -> ViewModelOutputs {
switch actions {
case .save(let data):
return .isSuccess(save(data))
case .delete(let data):
return .isSuccess(delete(data))
case .deleteAll:
return .numberOfDeletedItems(deleteAll())
}
}
action 메서드의 반환 타입으로 ViewModelOutput를 설정해주고,
Enum 값을 생성해서 반환해줍니다.
action 메서드 호출부는 어떻게 처리하면 될지 봅시다.
let viewModel = ViewModel()
let isSaveSuccess = viewModel.action(.save(1)).value()
print(isSaveSuccess) //true
let isDeleteSuccess = viewModel.action(.delete(2)).value()
print(isDeleteSuccess) //false
let numberOfDeletedItems = viewModel.action(.deleteAll).value()
print(numberOfDeletedItems) //1
반환 타입이 Enum이지만 value 메서드 덕분에 switch 없이도 연관값을 사용할 수 있습니다.
물론 switch문이 간편한 상황이라면 switch문을 이용해서도 처리할 수도 있습니다.
마무리
이번 포스팅까지 Enum을 이용한 ViewModel 코드 최적화에 대해 다뤄봤습니다.
타입 안정성은 Swift의 장점 중 하나입니다.
Enum을 이용하면 이 장점을 극대화할 수 있는데요.
이 특징을 이용해 ViewModel의 안정성을 높인 것입니다.
MVVM 패턴을 적용하시다가 ViewModel의 가독성이 낮은 거 같다, 코드가 난잡한 거 같다라는 문제가 느껴지실 경우
이 방법을 한 번 시도해보시는 것도 좋아보입니다.
감사합니다!
아직은 초보 개발자입니다.
더 효율적인 코드 훈수 환영합니다!
공감과 댓글 부탁드립니다.