최근 RxSwift와 ReactorKit을 함께 사용하면서 고민에 빠진 것이 있다.
RxSwift만 사용했을 때도 생각했던 것이지만, MVVM 패턴을 사용하게 되면 필연적으로 ViewModel을 만들거나 Reactor를 만들어서 사용하게 된다. 만드는 것 자체는 크게 어렵지 않지만, 만약 특정 뷰 컨트롤러에서 사용하는 이벤트가 단 하나 뿐이라면 어떻게 해야할까?
// 예시 이벤트
button.rx.tap
.bind(testEvent)
.disposed(by: disposeBag)
A 뷰 컨트롤러에서 발생하는 이벤트가 위와 같은 단순한 버튼 하나 뿐이라면, 이 하나의 이벤트를 위해 ViewModel이나 Reactor를 만드는 것이 오히려 복잡성만 늘리거나 손해를 보는 것이 아닐까 하는 고민이 들었다.
단순히 귀찮은 것도 사실이다. 어차피 이벤트가 단 하나라면 그냥 위의 예시처럼 코드를 작성해도 괜찮지 않나, 하는 생각이 들었던 것이다.
실제로 몇 번은 ViewModel이나 Reactor를 만들지 않고도 진행했었는데,
팀원들이 뭐라고 한 적은 없었고, 코드 자체도 문제 없이 작동했었다.
그렇기 때문에 그냥 사용해오기도 했었지만, 문득 고민이 되는 것이다.
더 좋은 코드를 만들기 위해서는 어떻게 해야하는걸까?
현실적으로 생각하면 이벤트가 하나뿐이라도 ViewModel이나 Reactor는 반드시 만드는 것이 좋다가 결론이다.
특히, 협업을 하고 있고 ViewModel이나 Reactor를 사용하기로 컨벤션을 정했다면 더더욱 만들어야 한다.
ReactorKit을 사용하는 상황을 예로 들자면, ReactorKit의 가장 큰 장점은 단방향 데이터 흐름을 강제한다는 점이다. 모든 비즈니스 로직은 Reactor 내에서 Action을 받아 State를 변경하는 방식으로 이루어진다.
이벤트가 단 하나뿐인 경우에만 이 컨벤션을 깨트리면, 나중에 코드를 볼 때 어디에 비즈니스 로직이 있는지 파악하기 어려워지기 때문에 코드 가독성 향상과 일관성을 유지하기 위해서는 반드시 Reactor를 구현해 주어야 한다.
또, 지금은 이벤트가 하나일지라도, 앱이 성장하면서 새로운 기능이 추가될 가능성이 있기 때문에 Reactor는 구현해주는 것이 좋다.
예를 들어, 로그인 이벤트만 있는 화면에 회원가입 또는 비밀번호 찾기 이벤트가 추가될 수 있다. 이 때, 처음부터 Reactor를 구현해 두었다면 단순히 Action에 새로운 이벤트를 추가하기만 하면 되므로, 확장성이 매우 좋아진다.
final class TestReactor: Reactor {
enum Action {
case buttonTapped
case newEvent // 이벤트가 추가되면 case를 추가
}
enum Mutation {
// ...
}
struct State {
// ...
}
}
협업, 개인 프로젝트에서 코드 일관성은 매우 중요하다. 만약 모든 화면이 Reactor라는 동일한 패턴을 따르게 되면, 새로운 개발자가 코드를 접했을 때 빠르게 적응할 수 있고, 버그 수정이나 기능 추가도 훨씬 수월해진다.
즉, 유지보수를 위해서도 이벤트의 수와 관계없이 Reactor는 꼭 구현을 해주는 것이 좋다.
이벤트가 하나뿐인 상황에서도 Reactor를 만드는 것이 귀찮게 느껴질 수 있지만, 더 좋은 코드를 위해 습관을 들이는 과정으로 생각하면 좋다.
Reactor는 단순히 이벤트가 여러 개인 경우에만 필요한 것이 아니다.
뷰와 비즈니스 로직을 분리하는 역할이 더욱 중요하다. Action이 하나뿐이라도, 그 Action이 트리거하는 복잡한 비즈니스 로직(네트워킹, 데이터 처리 등)이 있다면 Reactor의 역할은 충분히 의미가 있는 것이다.
만약 이벤트가 하나뿐인 화면이 자주 발생한다면, 이러한 경우를 위한 코드 템플릿을 만들어 두는 것도 좋은 방법이다.
예를 들어, 1개의 버튼의 탭 이벤트를 받는 Reactor가 자주 발생한다면 이 템플릿을 Xcode에 저장해두어 Boilerplate 코드를 줄일 수 있다.
만약 이벤트가 하나뿐이고, 그 로직마저도 매우 간단하다면 Reactor의 mutate()와 reduce() 메서드에 그 로직을 명확하게 작성하자
enum Action {
case didTapButton
}
struct State {
var isLoading: Bool = false
}
func mutate(action: Action) -> Observable<Mutation> {
switch action {
case .didTapButton:
// 단일 이벤트에 대한 로직을 명확하게 작성
return Observable.concat([
.just(.setLoading(true)),
// ... API 호출 등의 비즈니스 로직
.just(.setLoading(false))
])
}
}
결론적으로, ReactorKit을 사용하기로 결정했다면, 이벤트의 개수에 상관없이 모든 화면에 Reactor를 적용하는 것이 가장 좋은 선택이다.
이는 초기에는 번거롭고 귀찮을 수 있지만, 장기적으로는 코드의 품질과 유지보수성을 극대화할 수 있기 때문에 꼭 구현을 해주는 것이 좋다.
이는 ViewModel을 구현하는 것도 마찬가지이다.