반응형
SwiftUI에서 많이 하는 실수 - ViewModel에 비즈니스 로직과 UI 상태를 모두 섞어버리는 실수
MVVM에서 ViewModel은 흔히 “View를 위한 상태 + 약간의 화면 로직”만을 가지는 계층으로 이해됩니다.
여기에 도메인 규칙/비즈니스 로직까지 모두 때려 넣으면,
테스트하기 어렵고, 재사용이 불가능한 거대한 God ViewModel이 탄생합니다.
1. 문제 원인
- “어차피 View랑 연결되는 건 ViewModel뿐이니까 다 여기다 쓰자”는 사고
- UseCase / Service / Domain 계층에 대한 분리 개념 부족
- 단기 개발 속도를 위해 모든 의존성을 ViewModel에 직접 주입
2. 나타나는 증상
- ViewModel 파일이 수백~수천 줄로 커짐
- 다른 화면에서 같은 비즈니스 로직을 쓰고 싶은데, ViewModel를 재사용할 수 없어 복붙
- 테스트 시 UI와 비즈니스 로직이 뒤엉켜, 순수 로직 테스트가 사실상 불가능
3. 잘못된 코드 예시
final class CardsViewModel: ObservableObject {
@Published var cards: [Card] = []
@Published var isLoading: Bool = false
// ❌ 네트워크 클라이언트, 파서, Validator 등이 여기 다 들어옴
private let apiClient = APIClient()
private let validator = CardValidator()
func load() async {
isLoading = true
defer { isLoading = false }
do {
let dto = try await apiClient.fetchCards()
// ❌ 도메인 변환/검증 로직도 전부 ViewModel에
let valid = dto.filter { validator.isValid($0) }
self.cards = valid.map(Card.init(dto:))
} catch {
// ...
}
}
}
4. 올바른 코드 예시 (UseCase / Domain 분리)
4-1. UseCase
struct LoadCardsUseCase {
let repository: CardRepository
let validator: CardValidator
func execute() async throws -> [Card] {
let dto = try await repository.fetch()
let valid = dto.filter { validator.isValid($0) }
return valid.map(Card.init(dto:))
}
}
4-2. ViewModel
@MainActor
final class CardsViewModel: ObservableObject {
@Published var cards: [Card] = []
@Published var isLoading: Bool = false
private let loadCardsUseCase: LoadCardsUseCase
init(loadCardsUseCase: LoadCardsUseCase) {
self.loadCardsUseCase = loadCardsUseCase
}
func load() async {
isLoading = true
defer { isLoading = false }
do {
cards = try await loadCardsUseCase.execute()
} catch {
// 에러 상태를 UI에 맞게 변환
}
}
}
ViewModel은 “언제 로드할지, 로딩/에러 상태를 어떻게 UI에 보여줄지”만 책임지고,
실제 비즈니스 규칙은 UseCase/Domain이 담당합니다.
5. 정리 및 팁
- ViewModel은 기본적으로 “UI 친화적인 상태 캡슐화”에 집중하고,
비즈니스 규칙과 인프라 의존은 UseCase/Service/Repository에 위임하는 것이 이상적입니다. - 이 분리만으로도
- 유닛 테스트가 매우 쉬워지고
- View 교체(SwiftUI ↔ UIKit) 시 재사용성이 높아지며
- TCA/클린 아키텍처와도 자연스럽게 궁합이 맞습니다.
반응형
'Dev Study > SwiftUI' 카테고리의 다른 글
| SwiftUI에서 많이 하는 실수 - List 셀 내부에서 상태 변경/로직을 수행해 무한 렌더링이 발생하는 실수 (0) | 2025.12.05 |
|---|---|
| SwiftUI에서 많이 하는 실수 - ScrollView 안에서 LazyVStack을 사용하지 않아 성능이 나빠지는 실수 (0) | 2025.12.05 |
| SwiftUI에서 많이 하는 실수 - offset을 레이아웃 도구로 사용해서 반응형이 깨지는 실수 (0) | 2025.12.05 |
| SwiftUI에서 많이 하는 실수 - GeometryReader를 오용해 레이아웃이 엉망이 되는 실수 (0) | 2025.12.05 |
| SwiftUI에서 많이 하는 실수 - ObservableObject에서 @Published를 남발하는 실수 (0) | 2025.12.05 |
| SwiftUI에서 많이 하는 실수 - UI 업데이트를 메인 스레드에서 하지 않아 생기는 문제 (0) | 2025.12.05 |
| SwiftUI에서 많이 하는 실수 - .task와 .onAppear를 혼동해 비동기 로직이 중복 실행되는 실수 (0) | 2025.12.05 |
| SwiftUI에서 많이 하는 실수 - onAppear가 여러 번 호출되는 특성을 무시하고 API를 중복 호출하는 실수 (0) | 2025.12.05 |

