회사에서 SwiftUI 마이그레이션과 동시에 VC, VM단 구조 리팩토링도 같이 진행을 했다.
이전에 다뤘던 내용중 긁지 못했던 부분이 있다고 했는데, 바로 이 부분이었다.
프로젝트 전체적으로 에러 처리, 로딩 처리, VC와 VM의 역할 분배가 좀 잘못 되어있다고 생각했었다.
그때 당시의 생각으론 단순하게 로딩뷰 또한 뷰의 느낌이고, 얼럿창 또한 뷰의 느낌이라고 생각해서 VC에서 이를 관리하기로 했었다.
그리고 VC, VM의 역할 분배의 경우 명확한 기준을 세우지 못하고 개발을 진행했던 것 같다. (그 때 당시에는 나름 그럴듯하게 세웠다고 생각했다)
그러나 시간이 지나 코드가 쌓이게 되자 단점이 보이게 되었다.
VC는 “화면을 보여주고, 사용자 입력을 받아서 VM에 전달하는 것” 의 역할만을 수행해야 하는데, 우리 코드에선 그렇지 못했던 것 같다.
로딩뷰를 띄울 시점, 데이터를 받아 업데이트 하는 시점 등을 VC에서 알고 있었다.
그러다보니 VC의 코드양이 비대해졌고, 심한 경우 1700줄에 가까운 VC도 생기게 되었다.
그래서 이 구조를 개선하기 위해 VC는 말 그대로 버튼을 누르면 api를 호출하고 그저 값을 보여주기만 하는 역할만을 수행하도록 코드를 변경했다.
우선 기존 코드는 아래와 같다(X주석은 기존에 마음에 들지 않았던 부분)
기존 코드
// DepositVC.swift
private func getDepositTransactionDetail() {
Task { [weak self] in
guard let self = self else { return }
do {
CommonUtil.showLoadingView() // X
try await self.viewModel.getDepositTransactionDetail()
self.barcodeView.updateView(with: viewModel.depositHistoryDetail) // X
self.createStoreDepositGuideLabel() // X
CommonUtil.hideLoadingView() // X
} catch {} // X
}
}
//DepositVM.swift
private(set) var depositHistoryDetail: DepositDetailInfo.Entity = .init() // X
func getDepositTransactionDetail() async throws {
do {
depositHistoryDetail = try await usecase.getDepositTransactionDetail(depositId: self.depositId)
} catch { throw error } // X
}
개인적으로 위 코드의 문제점은 크게 다음과 같다고 생각했다.
1. 로딩뷰 히든 처리 시점을 VC에서 관리
2. 불필요한 do-catch문
3. UI 업데이트 시점을 VC에서 관리
그래서 해당 내용을 리팩토링 해 아래와 같이 개선했다.(SwiftUI마이그레이션도 포함)
변경 코드
//DepositVC.swift
var body: some View {
VStack {
ScrollView {
LazyVStack(spacing: 0) {
StoreBarcodeViewSU(depositDetail: viewModel.depositHistoryDetail, // 값이 변경되면 자동 적용
remainingTimeString: viewModel.timeString,
barcodeImage: barcodeImage)
.padding(.top, moderateScale(number: 20))
}
}
.scrollIndicators(.hidden)
}
.globalAlert() // 에러 처리
.loadingOverlay(isLoading: $viewModel.isLoading) // 로딩 처리
}
//DepositVM.swift
@Published var isLoading: Bool = false
@Published var depositHistoryDetail: DepositDetailInfo.Entity = .init()
func getDepositTransactionDetail() async {
isLoading = true
defer { isLoading = false }
do {
depositHistoryDetail = try await usecase.getDepositTransactionDetail(depositId: self.depositId)
} catch {
showAlert(for: error)
}
}
1. 로딩뷰 히든 처리 시점을 VC에서 관리 -> 개선
VM에서는 isLoading이라는 퍼블리셔를 두었다.
api를 호출하기 시작할때 isLoading을 true로 만든다.
defer를 활용해 api 실패, 성공 여부와 상관없이 호출이 끝나면 isLoading을 false로 변경한다.
VC에서는 그저 isLoading을 구독하고, 그 값이 true냐 false에 따라 로딩뷰를 보여주거나 숨김 처리한다.
위 코드에서 loadingOverlay가 해당 역할을 한다.
2. 비어있는 catch문 -> 개선
이건 나름 큰 공사였다. 기존에 에러 캐치를 vc나 vm이 아닌 usecase에서 하고 에러를 캐치하고 처리를 했다. 그래서 사실상 VC와 VM에 있는 catch문은 장식용..?인 경우가 많았다. 우리가 처음 Swift Concurrency를 도입할 당시 기초 지식이 부족해서 저지른 실수라고 생각한다.
이런 구조의 단점은 usecase단 즉, api에러를 제외한 Presentation Layer에서 처리해야 되는 에러처리는 따로 관리를 했어야 했고, 이 때문에 에러 처리를 어떤 곳은 usecase가 방출한 퍼블리셔를 통해서, 어떤 곳은 VC의 catch문에서 하는 등 구조가 중구난방이었던 것 같다.
그래서 에러 캐치를 usecase나 VC가 아닌 VM이 관리하는 것으로 구조를 변경했다.
그리고 각 화면에 퍼져있는 api에러 처리를 Factory를 활용해 한 곳에서 관리 하는 방향으로 구조를 변경했다.
(VC의 비어있는 catch문의 자세한 내용은 다음 글에서 다뤄보겠다)
VM에서 showAlert를 통해 얼럿창을 띄우거나, 기타 에러처리를 하고,
VC에서는 단순히 globalAlert() 함수를 통해 받은 에러들을 화면에 보여주기만 하면 된다.
class BaseViewModelSU: AnyObject {
var cancelBag = Set<AnyCancellable>()
func showAlert(for error: Error) {
let alertData = AlertFactory.makeAlert(for: error)
AlertManager.shared.showAlert(alertData)
}
deinit {
LogDebug("⚡ deinit ---> \(self)")
}
}
3. UI 업데이트 시점을 VC에서 관리 -> 개선
변경 전 VM을 보면 퍼블리셔가 아닌 저장 프로퍼티로 depositHistoryDetail이 관리되고 있었다.
그러다 보니 VC에서 api호출이 성공적으로 되었을때 따로 updateView(), createStoreDepositGuideLabel() 등을 호출해서 UI를 업데이트 했어야 됐는데, VC는 단지 VM에서 방출되는 상태를 화면에 반영만 하는 역할로 알고 있다.
따라서 이러한 부분들을 @Published var 를 활용해서 api호출이 성공하고 값이 변경되면 자동으로 UI를 업데이트 할 수 있게 변경하여 VC에서 따로 UI 업데이트 시점을 관리하지 않는 방향으로 수정했다. (StoreBarcodeViewSU)
그리고 추가적으로 usecase를 잘 활용하지 못하는게 프로젝트의 문제라고 생각하는데, 추후에는 VM, usecase 역할을 한번 더 나눠서 특정 계층에 코드가 집중되는 걸 방지하고 개선해 봐야겠다.
이번년도 초에 면접에서 과제에서 왜 로딩 관리를 VM에서 안하고, VC에서 했냐는 질문을 받은 적이 있었는데 그게 어떤 의미에서 질문을 한 것인지 지금에서야 이해했던 것 같다. 다음번에 대답을 잘하는걸로..
'iOS 개발' 카테고리의 다른 글
| [iOS개발] Swift Concurrency async, throw 관련 테스트 정리 (4) | 2026.01.08 |
|---|---|
| [iOS 개발] iOS 앱 용량 줄이기 (4) | 2025.02.15 |
| [iOS 개발] Interceptor란? (2) | 2023.11.23 |
| [iOS 개발] 브랜치 전략 세우기 (0) | 2023.11.17 |
| [iOS 개발] OAuth 2.0이란? (3) | 2023.11.15 |