반응형

SwiftUI Study – View 내부에 비즈니스 로직을 넣지 않고 도메인/UseCase로 분리하는 실전 설계 패턴

 

1. 왜 중요한가 (문제 배경)

SwiftUI는 선언적 UI 구조이기 때문에, 다음과 같은 실수가 매우 흔하게 발생한다.

  • View 내부에서 도메인 규칙을 직접 계산
  • 가격 계산, 날짜 비교, 권한 체크 등 비즈니스 로직이 UI 코드에 뒤섞임
  • UI가 조금만 바뀌어도 핵심 로직까지 수정해야 하는 구조
  • 테스트 불가능한 View 중심 아키텍처로 고착
  • ViewModel은 단순 상태 저장소로 전락하고 로직이 View로 이동

이 문제는 SwiftUI 초보뿐 아니라 숙련된 개발자에게도 자주 나타난다.

핵심은 다음과 같다.

“View는 로직을 갖는 계층이 아니라, 이미 계산된 결과를 표현하는 계층이다.”

UI가 로직을 알기 시작하면 구조가 무너지고 유지보수 난도가 기하급수적으로 증가한다.

 

2. 잘못된 패턴 예시

❌ 예시 1: View에서 가격 계산 직접 수행

struct WrongPriceView: View {
    let price: Int
    let discountRate: Double

    var body: some View {
        let finalPrice = Int(Double(price) * (1 - discountRate))   // ❌ View에서 핵심 비즈니스 계산
        Text("최종 가격: \(finalPrice)원")
    }
}

문제점
- 가격 계산 규칙이 뷰 내부에 노출됨
- 할인 정책이 바뀌면 View 파일까지 수정해야 함
- 동일 로직을 여러 View에서 반복 작성하게 됨

❌ 예시 2: View에서 날짜 로직 처리

struct WrongDeadlineView: View {
    let deadline: Date

    var body: some View {
        let now = Date()
        let remaining = deadline.timeIntervalSince(now)   // ❌ 로직과 UI 섞임

        Text(remaining < 0 ? "마감" : "남은 시간: \(Int(remaining))초")
    }
}

문제점
- 테스트 불가능
- 로직이 여러 View에 흩어져 재사용 불가
- UI는 단순 표현만 담당해야 한다는 원칙에 위배

❌ 예시 3: View가 비즈니스 흐름을 제어

Button("구매하기") {
    if user.isLoggedIn && cart.items.count > 0 {   // ❌ 도메인 규칙을 View가 알고 있음
        order()
    }
}

문제점
- 인증/장바구니 규칙이 View에 노출
- 기능이 복잡해질수록 View는 거대한 if 블록이 됨
- 테스트 불가능한 UI 중심 구조가 만들어짐

 

3. 올바른 패턴 예시

✅ 예시 1: 도메인 로직을 UseCase에서 처리

UseCase:

struct PriceCalculator {
    func finalPrice(price: Int, discount: Double) -> Int {
        Int(Double(price) * (1 - discount))
    }
}

ViewModel:

final class PriceViewModel: ObservableObject {
    private let calculator = PriceCalculator()

    @Published var finalPrice: Int = 0

    func update(price: Int, discount: Double) {
        finalPrice = calculator.finalPrice(price: price, discount: discount)
    }
}

View:

struct CorrectPriceView: View {
    @StateObject private var vm = PriceViewModel()

    var body: some View {
        Text("최종 가격: \(vm.finalPrice)원")
            .task {
                vm.update(price: 10000, discount: 0.2)
            }
    }
}

장점
- 정책이 바뀌어도 View는 수정 불필요
- 재사용성 증가
- 테스트 가능성 향상

✅ 예시 2: 날짜 계산도 도메인 계층에서 수행

UseCase:

struct DeadlineChecker {
    func remainingSeconds(to deadline: Date) -> Int {
        max(Int(deadline.timeIntervalSince(Date())), 0)
    }

    func isExpired(_ deadline: Date) -> Bool {
        deadline < Date()
    }
}

View에서는 단순 표현만:

struct CorrectDeadlineView: View {
    let text: String   // 계산된 결과만 받음

    var body: some View {
        Text(text)
    }
}

장점
- 테스트가 매우 쉬움
- UI는 문자열 표시만 담당
- 도메인 규칙을 UI가 알 필요 없음

✅ 예시 3: 버튼 이벤트는 View → ViewModel → UseCase 흐름

View:

Button("구매하기") {
    vm.buy()
}

ViewModel:

func buy() {
    if purchaseUseCase.canBuy(user: user, cart: cart) {
        purchaseUseCase.buy()
    }
}

UseCase:

struct PurchaseUseCase {
    func canBuy(user: User, cart: Cart) -> Bool {
        user.isLoggedIn && !cart.items.isEmpty
    }

    func buy() {
        print("Purchase confirmed")
    }
}

장점
- 비즈니스 규칙이 UI에 노출되지 않음
- UI는 단순 이벤트 트리거일 뿐
- 규칙 변경 시 View는 건드릴 필요 없음

 

4. 실전 적용 팁

✔ 팁 1 – 비즈니스 규칙이 View 안에 생기면 즉시 UseCase로 이동

View 파일은 절대 비즈니스 규칙을 포함하면 안 된다.

✔ 팁 2 – ViewModel은 UI 상태 변환 + UseCase 호출만 담당

비즈니스 규칙을 ViewModel에 직접 넣지 말고, UseCase로 분리하라.

✔ 팁 3 – UseCase는 테스트 가능하도록 순수 함수 기반으로 유지

네트워크·디스크 접근은 Repository나 Service로 위임.

✔ 팁 4 – 동일한 계산이 View 여러 곳에서 반복되면 100% 구조 문제

중복 계산이 보이면 즉시 도메인 계층으로 분리해야 한다.

✔ 팁 5 – UI는 “표시”, ViewModel은 “상태”, UseCase는 “규칙”

이 세 역할을 혼동하지 않는 것이 SwiftUI 아키텍처 핵심이다.

 

5. 정리

  • View 내부에 비즈니스 규칙이 들어가면 유지보수와 테스트가 불가능해진다.
  • 모든 도메인 로직은 UseCase·Service·Domain 계층으로 분리해야 한다.
  • View는 순수하게 UI 렌더링만 담당해야 하며,
    ViewModel은 상태 변환과 UseCase 호출만 담당해야 한다.
  • 이 패턴을 적용하면 SwiftUI 프로젝트의 안정성·확장성·테스트 가능성이 크게 향상된다.
반응형
Posted by 까칠코더
,