[iOS] MVVM 패턴에 대한 고찰 #1 (SwiftUI편)

이정훈·2023년 12월 14일
1

iOS

목록 보기
3/3

새로운 프로젝트를 시작하면서 어떤 아키텍처를 사용할까 하다가 별 생각 없이 MVVM을 사용하곤 했는데..

사실 큰 이유가 있다라기 보다 그냥 주변에서 관례적으로 그렇게 많이 사용하니까 필자 또한 그렇게 사용 해왔던거 같다..

MVVM이 다른 아키텍처 장점은 무엇인지, 어떻게 하면 Swift 답게 MVVM을 사용할 수 있는지 iOS 관점에서 정리 해보려고 한다.

🧐 MVC VS MVVM

기존 UIKit 프레임워크는 기본적으로 MCV 기반으로 설계 되어있다. MVC 기반이긴 하나 MVVM으로도 설계 가능하다

UIKit MVC 기반으로 개발을 하다보면 알겠지만 Controller 역할을 하는 ViewController의 어깨가 무겁다.

MVC에서 View와 사용자의 피드백을 처리하는 작업은 ViewController의 delegate 패턴으로 처리 하고 ViewControllerModel의 notification을 처리하는 작업 등 대부분의 작업을 Controller가 많은 책임을 가지고 있고 그 결과 Controller가 비대해지고 코드량이 많아진다.

또한 모든 ViewViewController 단위로 구성되며 그 결과 ViewController가 밀접하게 연관 되면서 의존도가 높아지고 서로 분리 하기가 힘들어 지게 되는데 이렇게 되면 코드 재사용성이 떨어지고 유지보수가 힘들어 진다.

이런 MVC의 단점을 해결하고자 등장한 것이 MVVM이다.

먼저 MVVM의 의미와 MVC에서 Controller와의 차이는 다음과 같다.

Model - ViewModel - View

Model

modelViewModel이 요청한 데이터를 반환하는 역할을 가진다. 다시 말해 데이터를 관리하는 역할로, 네트워크 API 요청과 같은 비즈니스 로직과 json과 같은 추상적인 형태의 데이터를 캡슐화하는 구조체 및 클래스를 포함한다.

ViewModel

MVCcontroller와 가장 큰 차이점은 View에 표시할 데이터를 ViewModel이 가지고 있고 ViewViewModel의 데이터를 관찰, 다시 말해 Binding 되어 있다는 점이다. 그 결과 MVC에서 controllerView에게 데이터를 전달하는 작업이 사라지게 되면서 View와 의존성을 분리할 수 있다.

View

사용자에게 직접 보여지는 부분으로 ViewModel바인딩 되어 데이터를 관찰하면서 데이터가 변경되면 변화를 감지하고 자동으로 View를 재렌더링 하게된다.

단방향 데이터 플로우

MVC와 또 다른 특징은 MVVM은 데이터의 흐름을 한 방향으로만 흐르게 하는 단방향 데이터 플로우(unidirectional data flow) 패턴을 가진다는 것이다.

다시 말해,

ViewViewModel을 알지만(참조를 가지고 있지만) ViewModelView를 모르며, ViewModelModel을 알지만 ModelViewModel을 알지 못한다.

단방향 데이터 플로우 패턴의 결과, View에서 오류가 발생 하더라도 ViewModel까지 영향을 미치지 않으며, ViewModel에 오류가 발생 하더라도 Model에게까지 영향을 미치지 않는다.

즉 특정 layer에서 발생한 오류는 다른 layer에 영향을 미치지 않으므로 각 layer는 각자 맡은 임무만 잘 수행하면 된다는 뜻이고 다시 말해 Unit Test에 용이하다고 할 수 있다.

데이터 바인딩

ViewModelView를 가지고 있지 않은데 어떻게 변경된 데이터를 View에 표현할 수 있을까?

이것을 가능하게 하기 위해 ViewViewModel의 데이터를 관찰하고 있으면 되고 이것을 데이터 바인딩이라고 한다.

데이터 바인딩의 결과 ViewModel은 데이터의 변경이 있을때 마다 View에게 일일이 데이터를 전달해 줄 필요가 없어졌고 그저 데이터가 변경 했다는 알림만 보내주면 변경된 데이터와 관련된 View가 알아서 변화를 감지하고 재렌더링하게 되며 이것이 MVVM의 핵심이다.

데이터 바인딩을 구현하기 위한 방법으로 RxSwift 라이브러리를 사용하거나 Swift Combine framework를 사용하면 된다.


🔥 SwiftUI로 MVVM 구현

SwiftUIMVVM을 구현하기 전에..

SwiftUI 프레임워크가 등장한지 거의 4~5년이 다 되어 가지만 SwiftUI에서 MVVM 패턴을 쓰지 말아야 하는가에 대한 논쟁은 아직까지 결론이 나지 않은것 같다. 그래서 실제 MVI와 같은 패턴을 적용하는 개발자도 심심치 않게 볼 수 있었다.

필자 또한 SwiftUI에서 MVVM 패턴이 완벽한 해법이라고 생각하지 않는다. 이유는 SwiftUI에서 지향하는 선언형 프로그래밍 방식에서 View 자체가 View + ViewModel로서의 역할을 수행할 수 있기 때문.

그렇다고 ViewModel을 제거하고 ViewModel만의 조합으로 개발한다..? 그럼 View 자체가 너무 비대 해지고 프레젠테이션 로직과 비즈니스 로직이 한 파일에 섞여 있을 것이기 때문에 MVC에서 넘어온 이유가 없다고 생각한다.

Apple에서도 명확한 가이드 라인을 제시하는 것도 아니기 때문에 SwiftUI + Combine 프레임워크로 쉽게 MVVM Binding을 구현할 수 있다는 장점과 필자가 SwiftUIMVVM을 개발할때 사용하는 방법을 공유 하고자 한다.

먼저 예제 프로젝트의 디렉토리 형태는 다음과 같다.

└── SwiftUI_MVVM_Practice
   ├── Model
   │   └── Person.swift
   ├── SwiftUI_MVVM_PracticeApp.swift
   ├── View
   │   └── ContentView.swift
   └── ViewModel
       ├── ContentViewModel.swift
       └── ViewModel.swift

Model 정의

struct Person {
    var name: String
    var age: Int
}

ViewModel 정의

ViewModel은 다형성을 갖고 유연한 유지보수를 가능 하도록 protocol로 정의 하고 View에서 ViewModel이 가지고 있는 데이터를 관찰 가능 하도록 ObservableObject protocol을 채택하도록 한다.

protocol ViewModel: ObservableObject {
    var person: Person { get }
    
    func addAge()
    
    func subAge()
}

이제 ViewModel protocol을 준수하는 ContentViewModel class를 정의한다.

final class ContentViewModel: ViewModel {
    @Published var person: Person = Person(name: "홍길동", age: 99)
    
    func addAge() {
        person.age += 1
    }
    
    func subAge() {
        person.age -= 1
    }
}

ContentViewModel에서는 관찰 중인 데이터에 변화가 발생하면 View에게 알리기 위해 @Published property wrapper를 사용한다.

그럼 View@Published 변수의 변화를 감지하고 View를 재렌더링 하게 된다.

View 정의

import SwiftUI

struct ContentView<VM>: View where VM: ViewModel {
    @ObservedObject var contentViewModel: VM
    
    var body: some View {
        VStack(spacing: 20) {
            Text("이름: \(contentViewModel.person.name)")
            
            Text("나이: \(contentViewModel.person.age)")
            
            HStack(spacing: 20) {
                Button(action: {
                    contentViewModel.addAge()
                }, label: {
                    Text("더하기")
                })
                
                Button(action: {
                    contentViewModel.subAge()
                }, label: {
                    Text("빼기")
                })
            }
        }
    }
}

@ObservedObject property wrapper로 선언 되어 있는 contentViewModel 변수의 인스턴스는 ViewModel protocol을 준수하는 모든 타입의 인스턴스가 올 수 있기 때문에 다형성을 만족하고 유지보수재사용성을 높일 수 있었다.

의존성 주입

현재 View에서 contentViewModel의 인스턴스를 직접 생성하지 않고 있다.

인스턴스를 View 내부에서 직접 생성하는 것이 아닌 View를 생성할때 생성자를 통해 ContentViewModel class의 인스턴스를 주입 함으로서 ViewViewModel의존성을 낮추고 Unit Test를 가능하게 하는 코드를 작성 할 수 있었다.

import SwiftUI

@main
struct SwiftUI_MVVM_PracticeApp: App {
    var body: some Scene {
        WindowGroup {
            ContentView<ContentViewModel>(contentViewModel: ContentViewModel())
        }
    }
}
profile
새롭게 알게된 것을 기록하는 공간

0개의 댓글