-
Notifications
You must be signed in to change notification settings - Fork 0
세션 화면에서 살펴보는 클린 아키텍처의 도입
제가 담당했던 화면은 아래와 같았습니다. 매칭이 성사되면 카운트 다운과 함께 사용자의 정보를 보여주고, 사용자들이 운동할 때 이동한 거리를 실시간으로 업데이트 시키고, 자신의 휴대폰 뿐 아니라 다른 사용자의 휴대폰에서도 업데이트 되어야 했어요.
그리고 오른쪽으로 스와이프 했을 때는 내가 지나온 거리를 보여주는 지도 화면도 있었어요.
실제로 작업한 화면(3배속)
비교적 UI/UX는 간단해보이지만, 하나의 화면 내에서 정말 많은 기능으로 이루어져있는 것을 볼 수 있었어요.
저는 이 화면을 설계하기에 앞서 어느 부분에 어느 기능이 들어갈지 머릿속으로 정리한 것을 Figma
를 이용하여 그려봤어요.
저는 여러 기능이 들어간 이 화면을 하나의 ViewController과 ViewModel로만 사용하게 된다면, **Massive ViewController(ViewModel)**이 될 것이라고 생각했어요. 여러 기능을 하나의 MVVM으로 다룬다면, 그만큼 코드가 커지고 분리하기 어려우며, 무엇보다 코드의 가독성이 떨어질 것이 자명했거든요.
그래서 기능을 분리하기 위해 다른 방법을 모색하기로 했고, 두 가지의 방법을 떠올리게 되었어요.
바로 UICollectionView
와 UIPageViewController
에요.
CollectionView는 스크롤 기능을 제공해주고 있고, Cell 단위로 뷰를 재사용할 수 있도록 만들어져 있어요.
하지만, 제가 구현하고자 하는 화면은 두 개 뿐이고, 각각의 화면은 뷰를 재사용하기 위한 형태가 아니었어요.
그리고 무엇보다, Cell에다가 ViewModel을 추가하는 작업을 해주고 싶지 않았습니다. 단순하게 UI만 갖고 있는 Cell이 되기를 바랐어요.
PageViewController는 여러 ViewController를 Paging단위로 보여줄 수 있는 기능을 제공하고 있어요.
가로, 세로 스와이프 제스쳐를 지원해줘요.
DataSource, Delegate를 이용해서 어떤 ViewController를 보여줄지, 제스쳐가 인식되고 난 뒤에 어떻게 처리해줄지를 관리할 수 있어요.
ViewController 여러개를 다룰 수 있다는 점에서, 기능을 분리할 수 있겠다는 생각이 들었고, UIPageViewController
를 채택했어요.
UIPageViewController
로 제스처를 통해 ViewController를 자유자재로 이동할 수 있다는 점을 확인했습니다.
그래서 Container View Controller를 활용하고자 부모 ViewController인 ContainerViewController
을 기준 삼아 자식 ViewController인 사용자 세션 화면(SessionViewController
)과 지도 화면(RouteMapViewController
)를 두었어요.
그리고 다시 그림으로 설계해봤을 때 생각외로 기능이 쉽게 세분화되었고, 비교적 구조가 간단해지는 것을 볼 수 있었어요.
하지만, 구조 자체는 간단해 보일지언정 자식 ViewController이 담당하는 역할은 여전히 고려해야할 사항이 많았어요.
SessionViewController
에서는 여전히 여러 사용자들의 정보를 전달 받거나 전달하면서 UI를 업데이트 해주어야 했고요. 서버와의 통신 방식도 정해야했고요.
RouteMapViewController
에서는 사용자의 위치 정보를 받아 지도에 그려주어야했어요.
이렇듯, 빈번한 뷰 업데이트가 필요한 다양한 이벤트와 지속적으로 변하는 데이터 상태를 관리하기 위해 Combine과 Input-Output 패턴을 도입했습니다. 덕분에 ViewModel의 상태변화를 ViewController에 쉽게 반영할 수 있었고, UI 이벤트를 ViewModel로 간편하게 전달할 수 있었어요.
실제로 ViewController와 ViewModel을 구성하는 과정에서 두드러졌습니다.
ViewController의 Input 정보를 ViewModel에게 전달하면, ViewModel은 이에 대응하는 Output을 내려보내주었고, 이렇게 생성된 Output은 UI 업데이트 메서드에 간편하게 연결할 수 있었어요.
어떤가요? 코드가 깔끔해보이지 않나요?
private func bind() {
let input: WorkoutRouteMapViewModelInput = .init(
filterShouldUpdatePositionPublisher: kalmanFilterShouldUpdatePositionSubject.eraseToAnyPublisher(),
filterShouldUpdateHeadingPublisher: kalmanFilterShouldUpdateHeadingSubject.eraseToAnyPublisher(),
locationListPublisher: mapSnapshotterImageDataSubject.eraseToAnyPublisher()
)
viewModel
.transform(input: input)
.sink { [weak self] state in
self?.render(state: state)
}
.store(in: &subscriptions)
}
private func render(state: WorkoutRouteMapState) {
switch state {
case .idle:
break
case let .snapshotRegion(region):
createMapSnapshot(with: region)
case let .censoredValue(value):
updatePolyLine(value)
}
}
Session View Controller 화면
SessionViewController
에서는 사용자들과 함께 실시간으로 운동하면서 얼만큼 운동했는지를 사용자들과 공유할 수 있는 기술이 들어가야 해요. 이를테면, 내가 운동하고 있을 때 100m를 이동했다면 내가 움직인 거리를 UI로 띄워주어야하고, 동시에 사용자들에게 내가 100m를 이동했음을 알려주어야 하죠.
그 뿐만이 아닙니다. 내가 얼만큼 운동했는지를 확인하기 위해 몇 초마다 데이터를 받아올 것인지, 받아온 거리와 칼로리 계산은 어떻게 할 것인지, 서버에게는 어떻게 데이터를 요청하고, 전달할 것인지를 ViewModel에서 해야합니다.
결국 이렇게 되면 ViewModel이 다시 복잡해질 수 밖에 없게 돼요.
Massive를 피하기 위해 Container ViewController
로 분리했지만, 다시 Massive ViewModel의 굴레에 빠지게 된 것입니다.
방대해지는 ViewModel의 역할을 분리하고자 저희는 **Clean Architecture**
를 도입했어요.
https://github.com/kudoleh/iOS-Clean-Architecture-MVVM
**Clean Architecture**
는 Data
, Domain
, Presentation
라는 세 가지의 계층을 갖으며, 각 계층의 역할과 책임을 명확하게 분리하는 데 있어요.
그래서 네트워크 처리와 비즈니스로직까지 담당하는 ViewModel을 UseCase와 Repository로 분리함으로써 ViewModel은 오직 Presentation 계층의 로직만을 처리하게 돼요. 즉, 사용자 인터페이스(User Interface)와 관련된 상태 관리와 이벤트 처리에 집중하게 되죠.
실제 데이터 처리와 비즈니스 로직은 Domain 또는 Data 계층으로 이동함으로써, ViewModel의 복잡성과 크기를 크게 줄일 수 있다고 판단했어요.