Massive View에서 MVVM으로의 전환

낯선 언어를 매일 학습하며 몰입과 성장을 기록하는 일지

  ·  2 min read

MVVM #

문제상황 #

  • 처음 해보는 Swift 구조 관리에 어려움 발생
  • 화면을 그리는 코드와 로직을 담당하는 코드가 동시에 혼재해 있어서 디자인 변경 시 로직까지 건드리는 상황 발생
  • 해결책으로 MVVM 도입 결정

ViewModel #

  • LotttoViewModel 생성
  • Input, Processing, Output의 파이프라인을 가짐

Input #

func addTicketFromQR(url: String) {
    print("📷 스캔 감지: \(url)")

    let result = parseQRUrl(url)
    let newTickets = result.tickets
    let scannedRound = result.round

    if newTickets.isEmpty {
        print("⚠️ 유효하지 않은 로또 QR입니다.")
        return
    }

    scannedTickets.insert(contentsOf: newTickets.reversed(), at: 0)

    if let round = scannedRound {
        print("🔄 \(round)회차 감지! 당첨 정보 로딩 시작")
        Task {
            await fetchWinningData(round: round)
        }
    }
}
  • View에서 발생한 QR코드 스캔을 받아, 내부적으로 비즨스 로직을 수행

Processing #

func checkRank(for ticket: LottoTicket) -> LottoRank {
    guard !winningNumbers.isEmpty else { return .miss }

    let count = matchCount(for: ticket)
    let hasBonus = ticket.numbers.contains(bonusNumber)

    if count == 5 {
        return hasBonus ? .second : .third
    }

    let ranks: [LottoRank] = [.miss, .miss, .miss, .fifth, .fourth, .miss, .first]

    return count < ranks.count ? ranks[count] : .miss
}
  • View로부터 받아온 입력을 통해 등수를 계산
  • 기존에는 View에서 모든 로직들을 직접 수행하였으나 View는 입출력만 하고 비즈니스 로직은 하지 않도록 변경
  • 즉 View를 Dumb하게 만듬

Output #

@Published var scannedTickets: [LottoTicket] = []
@Published var winningNumbers: [Int] = []
@Published var bonusNumber: Int = 0
@Published var showResults = false

@Published var rankPrizes: [LottoRank: Int] = [:]

@Published var isLoading = false
@Published var errorMessage: String?

@Published var fetchedWinningNumbers: [Int] = []
  • @Published를 통해 해당 ViewModel을 보고 있는 모든 View에게 변경사항을 알리도록 함

Data Flow #

ContentView #

  • 앱의 진입점이므로 ViewModel을 생성하고 소유해야 함
  • @StateObject 사용하여 ViewModel 선언
struct ContentView: View {
    @StateObject private var viewModel = LottoViewModel()
    ...
}

TicketListView #

  • 부모가 만든 ViewModel을 전달받아 사용만 함
  • @ObservedOBject를 사용하여 ViewModel 선언
  • TicketListView에도 @StateObject를 사용했다면 뷰가 재생성될 때마다 뷰모델이 초기화되는 일이 발생
struct TicketListView: View {
    @ObservedObject var viewModel: LottoViewModel
    ...
}

Model #

  • 데이터 그 자체를 의미하며, UI와 무관한 역할
  • LottoRank, LottoResult, LottoTicket
  • LottoRank의 경우 Model 내부에서 색상, 상금 등을 Dictionary를 통해 설계하여 응집도를 높임
    • OCP를 준수하려 노력

결과 #

  • UI 디자인 변경 시 로직 코드와 분리되어 있어 안전한 변경이 가능
  • ConfettiView와 같은 새로운 기능 추가 시 기존 코드를 수정하지 않고 쉽게 확장 가능
  • MVVM을 적용하여 책임의 분리를 통해 안정성을 높일 수 있었음