반응형

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/클린 아키텍처와도 자연스럽게 궁합이 맞습니다.
반응형
Posted by 까칠코더
,