
Fitapat iOS팀에서는 클린 아키텍처를 도입해 사용하고 있고, 계층 간 관심사 분리를 통해 많은 이점을 체감하고 있습니다. 그중에서도 Presentation 계층의 ViewModel을 Fitapat iOS팀 내에서 해석한 방식으로 사용하면서 느꼈던 부분들을 소개해보려고 합니다.
더 자세한 소개와 코드는 (Github: Fitapt-iOS)에서 확인할 수 있어요.

저희 프로젝트의 아키텍처는 위와 같습니다. RxSwift를 활용하여 MVVM을 구축하였고, 부분적으로 UseCase를 사용했습니다. 또한 Repository 패턴으로 데이터 출처를 몰라도 쉽게 데이터를 읽고 쓸 수 있게 구현했어요.

해당 포스트에선 Presentation 영역을 설계하며 고민한 내용을 소개해드리고자 합니다.

저희 팀은 초기엔 MVC 아키텍처를 따랐으나, MVVM 으로 리팩토링했습니다. 리팩토링을 결정한 가장 큰 이유는 Massive ViewController 문제를 해결하기 위함이었죠. 하지만 ViewController가 비대해진다는 이유만으론 MVVM으로 리팩토링하기엔 무리가 있어보입니다. 그 이유는 MVVM은 MVC에 비해 더 많은 파일, 더 많은 코드를 작성해야 할 리스크가 다분하기 때문이에요. 코드 양이 증가함에도 다수의 개발자들이 MVVM 아키텍처를 따르는 이유는 무엇일까요?
저희 팀은 MVC 와 MVVM 을 결정하는 과정에서 “수평이동과 수직이동 중 어느 것이 유지보수에 용이한 프로젝트인가?” 에 대해 고민할 필요가 있다고 느꼈습니다.
설명을 돕기 위해 뷰 하나를 가져와볼게요. 실제 저희 온보딩에 있는 휴대폰 번호 인증뷰입니다.
휴대폰 번호 인증뷰의 기획은 아래와 같아요.

그럼 해당 뷰의 코드가 MVC와 MVVM일 때 어떤 차이가 있을지 살펴보죠! 여러분들이 저희 프로젝트에 최근에 합류한 개발자라고 가정하고 코드를 살펴봐주세요. 어떤 아키텍처가 로직 파악이 쉽고, 수정에 용이한 지를 중점적으로 생각하면서요!
(아래 등장하는 예시 코드들은 이해를 돕기 위한 코드로 컴파일이 되지 않을 수 있습니다.)
// PhoneViewController.swift
class PhoneViewController : UIViewController {
let phoneTextField = UITextField()
let sendButton = UIButton()
let codeTextField = UITextField()
override func viewDidLoad() {
super.viewDidLoad()
hierarchy()
layout()
style()
}
func hierarchy() { ... }
func layout() { ... }
func style() { ... }
func sendButtonDidTap() async {
guard checkPhoneNumberIsValid(with: phoneTextField.text)
else { return }
updateSendButtonEnabled(true)
let code = makeVerificationCode()
await requestVerification(to: phoneTextField.text, with: code)
updateSendButtonEnabled(false)
updateCodeTextFieldHidden(false)
}
func checkPhoneNumberIsValid(with phoneNumber: String?) -> Bool {
// 휴대폰 번호 정규식을 통과하는지 검사
}
func makeVerificationCode() -> String {
// 6자리 난수 생성
}
func requestVerification(to phoneNumber: String?, with code: String) async {
// SMS 인증 번호 발송 API 호출
}
func updateCodeTextFieldHidden(_ idHidden: Bool) {
codeTextField.isHidden = idHidden
}
func updateSendButtonEnabled(_ isEnabled: Bool) {
sendButton.isEnabled = isEnabled
}
}
해당 코드를 읽은 개발자들은 아래와 같은생각들을 할 수 있을 것 같아요.
(주관적인 의견으로 다소 과장이 섞여있을 수 있습니다.)
🙂: “이 버튼이 눌리면 어떤 함수가 실행되지?”
😕: ”난수 생성은 어디서 하지? 아하 전송버튼이 눌렸을 때 하는 구나“
🤨: ”action함수 내에서 다른 함수를 호출하네,, 해당 함수는 어떤 로직을 처리하지?”
😡: ”전송버튼의 활성화 여부는 updateSendButtonEnabled에서만 일어나나? 다른 함수도 뷰의 변경을 야기 시키나..?”
🤬 : ”뷰 코드와 로직 코드가 섞여있어서 로직파악이 너무 어려워!!!!”
MVVM은 ViewController와 ViewModel 두 파일로 나뉘게 됩니다.
// PhoneViewController.swift
class PhoneViewController : UIViewController {
// 생략
func bindViewModel() {
let input = PhoneViewModel.Input(
sendButtonDidTap: sendButton.rx.tap.asObservable(),
signUpButtonDidTap: signUpButton.rx.tap.asObservable(),
phoneTextFieldText: phoneTextField.rx.text.orEmpty.asObservable(),
codeTextFieldText: codeTextField.rx.text.orEmpty.asObservable())
let output = viewModel.transform(from: input, disposeBag: disposeBag)
output.updateSendButtonIsEnabled
.subscribe(onNext: { owner, _ in
// 전송 버튼 활성화
})
.disposed(by: disposeBag)
output.verifySuccess
.subscribe(onNext: { owner, _ in
// 다음 화면으로
})
.disposed(by: disposeBag)
}
(주관적인 의견으로 다소 과장이 섞여있을 수 있습니다.)
🙂: “아하!!!! 해당 뷰에서 이런 액션들을 뷰모델에게 넘기는 구나!”
😃: ”아하!!!!!!! 뷰의 Output이 변경될 시 이런식으로 뷰를 변경 시키는 구나!“
// PhoneViewModel.swift
class PhoneViewModel {
struct Input {
let phoneTextFieldTextDidChange: Observable<String>
let sendButtonDidTap: Observable<Void>
let codeTextFieldTextDidChange: Observable<String>
let signUpButtonDidTap: Observable<Void>
}
struct Output {
let phoneTextFieldText = BehaviorRelay<String>(value: "010-")
let updateSendButtonIsEnabled = PublishRelay<Bool>()
let updateCodeTextFieldHidden = PublishRelay<Bool>()
let verifySuccess = PublishRelay<Void>()
}
func transform(_ input: Input) -> Output {
input.phoneTextFieldText
.subscribe {
// 하이픈 자동 추가
// send버튼 활성화 여부 판별
}
.disposed(by: disposeBag
input.sendButtonDidTap
.subscribe {
// 6자리 난수 생성
// SMS 인증 번호 발송 API 호출
}
.disposed(by: disposeBag)
}
}
(주관적인 의견으로 다소 과장이 섞여있을 수 있습니다.)
🙂: “아하 해당 뷰는 이런 Input과 Output을 가지고 있구나!!!” 🍀
😊: ”Input이 들어올시 이런 로직을 거쳐 Output으로 변환되는구나!!!!!!“ 🌸
네! MVC와 MVVM의 코드를 살펴봤는데요, 어떤 코드가 더 가독성이 좋으시던가요?
수직이동과 수평이동 중 무엇이 더 좋을까?
저는 수평이동이라고 생각해요. 수평이동이 좋다는 결정을 내린 저의 사고의 흐름은 아래와 같아요.
지속가능한 소프트웨어 ➡️ 유지보수 하기 좋은 프로젝트
유지보수 하기 좋은 프로젝트 ➡️ 수정을 단기간 내에 반영할 수 있는 것
수정을 단기간 내에 반영할 수 있는 것 ➡️ 로직 파악이 쉬운 코드, 객체 결합도가 낮은 코드, SOLID 원칙이 잘 지켜진 코드(특히 역할 분리)
파악하기 쉬운 코드 ➡️ 통일성이 잘 지켜진 코드
그렇다면 통일성을 잘 지키기 위해선 어떻게 해야할까요?
개발자가 통일성을 지키는 방식
개발자가 통일성을 지키는 방식은 크게 두 가지가 있다고 생각해요. 아키텍처와 컨벤션이죠. 그 중 제가 생각하는 우선순위는 “아키텍처 > 컨벤션” 이라고 생각해요
”수평이동은 아키텍처(역할 분리)에 의해 결정된다.”
거시적인 관점에서 팀원과 아키텍처를 얼라인 시키면 개발자는 해당 아키텍처 내에서 코드를 짜기에, 보다 쉽게 통일성을 지킬 수 있습니다. 이는 코드를 어떻게 짜야할지에 관한 사소한 의사소통 비용을 줄일 수 있습니다.
”수직이동은 컨벤션에 의해 결정된다.”
Swift에선 mark 주석 혹은 extension 등으로 구분할 수 있지만, 사람에 따라 스타일이 다르며, 인수인계 할 때 마다 매번 새로운 컨벤션을 습득해야한다는 비용이 듭니다.
따라서 저희 팀은 수직이동보단 수평이동으로 프로젝트의 통일성을 지키고 싶기에 역할을 더 잘게 분리한 MVVM을 채택했습니다!
더불어, MVVM을 채택할 시 아래와 같은 추가적인 advantage 도 있었습니다.
수평이동으로 프로젝트를 통제할 수 있다는 것과 위 장점이 저희 Fitapat팀이 MVVM을 적용한 이유입니다.
저희 팀은 MVVM을 Input, Output 구조로 설계하였으며, RxSwift를 적용했습니다.
RxSwift
비동기를 포함한 데이터 흐름을 선언적으로 작성할 수 있는 반응형 프레임워크
저희 프로젝트의 요청 응답에 따른 계층 간 소통은 아래와 같았어요.
하나의 데이터를 가지고 오기위해 약 5개 정도의 계층이 협력해야 했죠. Swift에서 요청에 대한 응답을 처리하는 방식은 Closure, Delegate패턴 그리고 Reactive(Rx, Combine)한 방식이 있습니다.
클로저와 Delegate 패턴은 코드의 양이 너무 늘어나며 가독성이 좋지 않다고 판단하여 RxSwift를 채택했습니다. Combine과 RxSwift 중 Rx를 채택한 이유는 당시 참고할만한 레퍼런스가 RxSwift가 더 많아서 채택했어요.
데이터흐름이 단방향으로 흐르는 Flow를 문법적으로 보장하기 위해서 Input, Output 으로 나누었습니다. View의 액션은 Input을 통해서만 ViewModel에 전송할 수 있으며, ViewModel의 상태는 Output을 통해서만 View 에게 전달되도록 말이죠!
먼저, 저희 팀의 ViewModel Input 형태를 볼까요? 앞서 소개드린 휴대폰 인증뷰라고 가정해봅시다. 저희 팀은 Input을 아래와 같이 Observable로 통일했습니다.

Input은 기본적으로 관측가능 해야합니다. 뷰에서 발생한 액션이 관측될 시 선언한 데이터 흐름이 반응형으로 동작해야 하기 때문이죠. RxSwift에선 관측가능한 Trait는 여러가지가 있습니다.
RxSwift에서 Observable을 채택하고 있는 Trait들
두가지 이유가 있습니다.
RxSwift는 개발자의 편의성을 위해 다양한 operator(map, flatMap, withLatestFrom, … )를 제공해줍니다. 이러한 Operator는 Observable을 반환하죠. 드물긴 하지만 View에서 ViewModel로 Input을 넘겨줄 때 이런 operator를 사용할 때가 있습니다. CollectionView의 itemSelected의 Element를 IndexPath가 아닌 row만 보내고 싶을 때가 있겠네요. 이런 변화에도 통일성을 지키기위해 Observable로 통일했습니다.
그 이유는 모든 Observable은 ObservableConvertibleType을 채택하기에, asObservable()로 타입 변환을 할 수 있기 때문이에요. 최근에 Combine을 공부중인데 .eraseToAnyPublisher()를 통해 AnyPublisher로 통일되는 이유와 유사한 것 같네요. RxSwift 라이브러리 내부 코드를 보며 더 깊게 살펴보죠!
// RxSwift 내부 코드
struct ControlEvent : ObservableType { /* 생략 */ }
struct ControlProperty : ObservableType { /* 생략 */ }
class PublishRelay: ObservableType { /* 생략 */ }
총 세 가지의 Trait를 가져와봤습니다. 각 속성들은 아래와 같은 경우에 사용되곤 합니다.
모두 ObservableType을 채택하고 있네요. 그렇다면 ObservableType을 살펴볼까요?
// RxSwift 내부 코드
protocol ObservableType: ObservableConvertibleType { /* 생략 */ }
public protocol ObservableConvertibleType {
associatedtype Element
func asObservable() -> Observable<Element>
}
ObservableType은 ObservableConvertibleType을 채택하고 있으며, ObservableConvertibleType은 asObservable()이란 메소드를 가지고 있었습니다!
이 함수가 해당 Trait들을 Observable로 변환해주는 함수이죠!
Button의 tapEvent를 관측하는 ControlEvent도,
UITextField의 text 값을 관측하는 ControlProperty도,
ViewController에 선언해둔 PublishRelay도,
모든 Trait은 ObservableConvertibleType을 채택하고 있기에, Observable로 convert할 수 있었네요 :)
이번엔, ViewModel의 Output은 형태를 볼까요? 저희팀은 아래와 같이 PublishRelay 혹은 BehaviorRelay 로 통일하였습니다.

Output은 ViewModel에서 Input이 뷰로직을 수행한 후 어떤 결과를 내뱉는지 관찰(observer)해야하며, View에선 관측 당하기(observable) 때문이죠. Output 프로퍼티중 SendButton의 활성화 여부를 결정하는 updateSendButtonIsEnabled: PublishRelay<Bool>을 예시로 들어볼게요.
// PhoneViewModel.swift
class PhoneViewModel {
// ViewModel 에선 관찰자 역할
func transform(_ input: Input) -> Output {
input.phoneTextFieldTextDidChange
.map(Regex.verifyPhoneNumberFormat)
.bind(to: output.updateSendButtonIsEnabled)
.disposed(by: disposeBag
}
}
Input으로 phoneTextFieldTextDidChange 입력이 들어오면 func verifyPhoneNumberFormat(_ string: String) → Bool를 거친 후 해당 값을 View에게 알려줍니다. 이때 Output으로 상태를 방출시켜줘야하기에 관찰자 역할을 합니다.
// PhoneViewController.swift
class PhoneViewController: UIViewController {
let viewModel: PhoneViewModel
// ViewController에선 관측 당하는 역할
func bind() {
let input = PhoneViewModel.Input( /* 생략 */)
let output = viewModel.transform(input)
output.updateSendButtonIsEnabled
.subscribe { isEnabled in
sendButton.isEnabled = isEnabled
}
.disposed(by: disposeBag)
}
}
다음으로 ViewController를 봅시다. ViewController의 bind() 함수는 Output을 관찰하고 있습니다. Output의 상태를 UIComponents에 반영하는 역할을 하고 있네요. 그렇다면 RxSwift에선 관찰자이면서 관측 가능한 속성은 무엇이 있을까요?
// RxSwift 내부 코드
class PublishSubject: Observable, ObserverType { /* 생략 */}
대표적으로 Subject가 있습니다. 위 코드를 보면 알 수 있죠. 하지만 저희 프로젝트에서는 Subject를 쓰지않고 Relay를 사용했습니다.
Output은 View가 관측하고 있기에 error나 completed를 방출하면 안됩니다. 에러나 Completed가 발생하여 스트림이 끊기게 되면 추후 ViewModel의 Output 값이 바뀌어도 View에 반영이 되지 않기 때문이죠. Relay는 Subject로부터 상속받지만, error와 completed를 방출하지 않는다는 특징이 있습니다. 절대 끊기지 않는 스트림이죠. 따라서 에러를 방출하지 않는 Relay를 사용했습니다. 초기값이 있을 땐 BehaviorRelay를, 없을 땐 PublishRelay로 말이죠!
앞서 소개드린 예시들은 Input과 Output을 통해 액션과 상태가 명확하게 구분되어 쉽게 구현할 수 있었습니다. 다만, 프로젝트를 진행하다보면 더 복잡한 뷰도 마주하게 됩니다. 복잡한 뷰에서 Input과 Output은 아래와 같은 양상을 띕니다.

(이미지 출처: 단방향 데이터 플로우(Unidirectial Data Flow, UDF) iOS 앱 아키텍처로 복잡한 상태 관리하기)
이러한 문제를 해결하기 위해 탄생한 단방향 아키텍처가 ReactorKit, TCA입니다. Input, Output만으로는 제어하기 어려운 단방향 로직을 ReactorKit에선 Reactor가, TCA에선 Reducer가 해소시켜주죠. 저희 팀도 ReactorKit 도입을 논의해보긴 했으나, 새로운 아키텍처를 습득하기 위한 러닝 커브가 높다고 판단하였습니다. 대신 RxOperator를 적극적으로 활용하여 해당 문제를 해결하기로 했습니다.
앞서 언급한 휴대폰 번호 인증뷰의 뷰로직을 더 자세하게 살펴보며, 저희 팀이 복잡한 View에서 ViewModel을 어떻게 구축했는지 설명드리겠습니다.
유저가 휴대폰 번호를 입력하는 상황을 예시로 들어볼게요. 휴대폰 번호 입력은 사실 아래와 같이 두가지 동작이 필요했어요.

이처럼 하나의 Input이 여러개의 동작을 야기할 때 아래처럼 구현할 수 있을 것 같아요.
[방법 1] 두 동작을 하나의 Handler 안에서 처리
// PhoneViewModel.swift
class PhoneViewModel {
func transform(from input: Input) -> Output {
let output = Output()
input.phoneTextFieldTextDidChange
.subscribe(with: self, onNext: { owner, phoneNumber in
// 1. 하이픈을 자동으로 추가하는 로직
let phoneNumberWithHypen = phoneNumber.withHypen
output.phoneTextFieldText.accept(phoneNumberWithHypen)
// 2. 휴대폰 번호 형식일 때에만 전송 버튼을 활성화하는 로직
let isPhoneNumberFormat = Regex.verifyPhoneNumberFormat(phoneNumber)
output.updateSendButtonIsEnabled.accept(isPhoneNumberFormat)
})
.disposed(by: disposeBag)
return output
}
}
[방법 2] 각 동작마다 Handler 처리
// PhoneViewModel.swift
class PhoneViewModel {
func transform(from input: Input) -> Output {
let output = Output()
// 1. 하이픈을 자동으로 추가하는 로직
input.phoneTextFieldTextDidChange
.map { $0.withHypen }
.bind(to: output.phoneTextFieldText)
.disposed(by: disposeBag)
// 2. 휴대폰 번호 형식일 때에만 전송 버튼을 활성화하는 로직
input.phoneTextFieldTextDidChange
.map(Regex.verifyPhoneNumberFormat)
.bind(to: output.updateSendButtonIsEnabled)
.disposed(by: disposeBag)
return output
}
}
저는 두가지 방법 중 후자를 선호합니다. 이유는 Handler 역시 함수이기에 하나의 책임만 지는 것이 좋다고 판단하기 때문이에요. 더불어 map 과 bind를 통해 더욱 축약된 코드를 작성할 수 있어서 좋더라구요. 물론 하나의 Input이 두 개의 스트림으로 이어진다는 것을 모른다면 당황할 수 있겠네요 😅
유저가 전송 버튼을 눌렀을 때 유저에게 인증번호를 문자로 전송하는 상황을 예시로 들어볼게요.

유저가 전송버튼을 누른다면 난수를 생성하고, 유저에게 난수를 보내는 문자 전송 API를 호출해야 합니다. 이때 문제가 발생합니다.

sendButtonDidTap의 입력은 Void지만 Request를 보낼 땐 유저의 phoneNumber: String이 필요합니다. 해당 문제는 3가지 방식으로 해결할 수 있을 것 같아요.
[방법 1] View에서 sendButton의 Tap Event를 phoneTextField.text로 매핑
// PhoneViewController.swift
let input = PhoneViewModel.Input(
phoneTextFieldTextDidChange: phoneTextField.rx.text.orEmpty.asObservable(),
sendButtonDidTap: sendButton.rx.tap.map{ _ in return phoneTextField.rx.text.orEmpty }.asObservable()
)
[방법 1]은 ViewModel에서는 사용하기 편할 수 있으나, 뷰의 액션을 뷰에서 처리(mapping)한다는 특징이 있습니다. MVVM 아키텍처에서 View의 역할은 액션을 ViewModel에게 넘기기만 해야하며 별도의 로직은 처리하지 않는 역할입니다. 자아가 없는 View가 MVVM을 가장 준수한 View라고 생각해요. 이러한 관점에서 보면 해당 뷰는 자아를 지니고 있기에 [방법 1]은 기각입니다!
[방법 2] ViewModel에 저장 프로퍼티 사용하기
// PhoneViewModel.swift
class PhoneViewModel {
var phoneNumber: String = ""
func transform(from input: Input) -> Output {
let output = Output()
input.phoneTextFieldTextDidChange
.map{ $0.withHypen }
.do(onNext: { self.phoneNumber = phoneNumber}) // 추가!
.bind(to: output.phoneTextFieldText)
.disposed(by: disposeBag)
input.sendButtonDidTap
.map { self.phoneNumber }
.subscribe(with: self, onNext: { owner, phoneNumber in
// 난수 생성
// SMS 인증 번호 발송 API 호출
})
.disposed(by: disposeBag)
return output
}
}
우리가 필요한 phoneNumber의 값이 들어올 때 do(onNext: { }) 연산자를 통해 ViewModel의 저장프로퍼티에 값을 저장합니다. 그 후 sendButton이 눌렸을 때 map 을 통해 ViewModel의 phoneNumber에서 값을 가져와 사용하는 방식입니다.
해당 방식은 아키텍처 상 문제는 없어보입니다. 다만 ViewModel에 PhoneNumber 관련한 데이터가 두개(Input, 저장프로퍼티)가 있다는 점에서 Single Source of Truth (SSOT) 원칙에 어긋납니다. 이는 데이터의 일관성 유지 및 동기화 문제가 발생할 수 있기에 좋은 코드는 아닌 것 같네요.따라서 [방법 2]도 기각!!!!!!!!!!!!!!!!!
[방법 3] RxOperator: withLatestFrom() 사용하기
// PhoneViewModel.swift
class PhoneViewModel {
func transform(from input: Input) -> Output {
let output = Output()
input.sendButtonDidTap
.withLatestFrom(input.phoneTextFieldTextDidChange)
.subscribe(with: self, onNext: { owner, phoneNumber in
// 난수 생성
// SMS 인증 번호 발송 API 호출
})
.disposed(by: disposeBag)
return output
}
}
RxSwift에선 withLatestFrom()이란 연산자가 있습니다! 아래와 같이 Event시점은 a를 따라가되 값은 b의 최신값을 불러오는 것을 볼 수 있습니다.

우리가 정확히 원하던 방식이네요. MVVM 아키텍처도 준수하고, SSOT도 위배하지 않으며 코드의 양도 줄어 가장 이상적인 것 같습니다 :)
[방법 3] 당선~!!!!!
[3-1: 하나의 Input이 여러 개의 동작을 야기할 때] 와 유사하지만 다른 상황입니다. [3-1]은 하나의 Input이 두가지 Output으로 이어졌지만 두 Output은 서로 독립적이었습니다. [3-3] 에서는 독립적이 아닌 순차적이라는 것에 집중해야 합니다.
유저가 회원가입 버튼을 눌렀을 때를 예시로 들어볼게요. 회원 가입 버튼이 눌렸을 땐 아래와 같은 동작이 순차적으로 진행되어야 합니다.
RxSwift는 해당 로직을 하나의 스트림으로 만들 수 있습니다. 반응형 프레임워크의 존재의 이유이기도 하죠. 바로 코드로 살펴봅시다!
// PhoneUseCase.swift
protocol PhoneUseCase {
func verify(with code: String) -> Bool
func signUp() -> Observable<Void>
}
ViewModel을 보여드리기 이전에 저희 팀은 UseCase를 사용하고 있습니다. UseCase는 다음 포스트에서 더 자세하게 다룰 예정이니 참고만 해주세요. 휴대폰 번호 인증 뷰의 UseCase는 위와 같이 검증과 회원가입 이 있습니다. 다음으로 ViewModel에서 어떤 식으로 4가지의 동작을 순차적으로 진행되도록 했는지 보시죠.
// PhoneViewModel.swift
class PhoneViewModel {
let useCase: PhoneUseCase
func transform(from input: Input) -> Output {
let output = Output()
input.signUpButtonDidTap
.withLatestFrom(input.codeTextFieldText)
.map(useCase.verify)
.filter { $0 }
.flatMap(useCase.signUp)
.bind(to: output.signUpSuccess)
.disposed(by: disposeBag)
return output
}
}
어때요 굉장히 간단하죠!? 반응형 프레임워크의 꽃이 아닐까 싶습니다. 위 4가지의 순차적 동작을 각각 어떤 RxOperator를 사용해서 구현했는지 확인해보죠!
withLatestFrom : signUpButton의 Element를 codeTextField 의 text로 바꾸기 위함map : 동기 함수 verify(with code: String) -> Bool의 반환값인 Bool로 element를 변환하기 위함flatMap : 비동기 함수 signUp()-> Observable<Void>의 반환값으로 변환하기 위함 + 중첩 Observable을 방지하기 위해 flatbind : output으로 binding 해주기 위함.이처럼 상황에 맞게 RxOperator를 사용한다면 보다 간결하게 ViewModel을 구축할 수 있습니다 :)
해당 코드에서 flatMap이나 map 사용 시에 .flatMap(useCase.signUp)처럼 flatMap 인자에 클로저가 아닌 함수 원형을 넣은 것을 볼 수 있어요. flatMap은 인자로 @escaping (Element) → Observable을 받기 때문에 클로저를 넣어도 되지만 함수 자체를 넣어줄 수 있기 때문이에요.
ViewModel을 다른 방식으로 활용하고 있는 분들의 이야기 너무 궁금합니다! 또, 위 내용에서 잘못되었거나, 개선할 수 있는 부분도 언제든 공유 주시면 너무너무 반가울 것 같습니다. 🤗
Fitapat iOS팀에서 Presentation 계층을 설계 하며 고민한 흔적과 이유를 소개해드렸는데요, 다른 팀에 새로운 아이디어가 되길 바랍니다! 다음 포스트에선 Domain 영역을 설계한 과정을 소개해드릴게요!