Search
🗂️

SwiftUI 와 MVVM

MVVM 과 SwiftUI

MVVM 은 Model View ViewModel 의 약자로, 소프트웨어 아키텍쳐 패턴이다. 코드를 조금 더 깔끔하게 정리하여 유지보수 및 확장을 용이하게 만드는 일종의 청사진이라고 생각하면된다.
MVVM 은 SwfitUI 와 특히 결합되어 있으며, MVC 보다 훨씬 좋은 선택이 되ㅣㄹ것이다.MVVM 은
이번 포스팅에서는 MVVM 이 무엇인지, 더 나아가서 MVVM 의 SwiftUI 에서의 역할이 무엇인지를 알아보겠습니다.

MVVM 이란 무엇인가?

앞서 말했다시피 MVVM 은 소프트웨어 아키텍쳐 패턴, 또는 디자인 패턴이다. MVVM 을 이루는 가장 큰 요소로는 Model, View, ViewModel 이 있고 한가지 더 중요한 요소를 말하자면 binding 이라는 키워드이다.
소프트웨어 개발자로서 코드를 더 체계화하여 읽고 이해하기 쉬우며 버그 픽스 및 새로운 피쳐들의 추가 또는 제거를 쉽게 하는 것은 사명이다. 따라서 디자인 패턴을 배워두면 이 사명을 이룰 수 있을 뿐만아니라, 이렇게 유명한 패턴의 사용은 다른 개발자들과의 소통하기 쉽게 만들어준다.

MVC vs MVVM

MVVM 이야기를 할때면 MVC 이야기를 빼둘 수 가 없는게 MVC 는 MVVM 의 사촌지간으로 취급되기 때문이다.
Model : 데이타에 관련된 모든 역할을 담당한다.
View : 데이터를 display 하는 역할, 사용자 상호작용에대한 handling 하는 역할을 담당한다. 흔히 우리가 실제로 보는 UI 라고 생각하면된다.
ViewModel : 뷰 모델은 모델 데이터의 상태를 나타낸다. view 와 model 사이의 소통을 담당한다. 이러한 소통ㅇ은 binding 이나 다른 이벤트 또는 액션에 의해서 이루어진다.
Bindings: 명확히는 하나의 역할로 구분되지 않으나 MVVM 의 중심적인 개념이다. 데이터 조각들과 view 들 사이의 connection 이라고 생각하면 편하다.
역할
MVVM
MVC
UI 를 스크린에 보여주는 역할, user 상호작용 역할
동일
X
Model 과 View 를 서로 묶어줌
X
View, Model 이 아닌 모든것
View 와 Model 사이의 소통

Model

현대 소프트웨어 개발에서 데이터 모델은 보통 투명하거나 매우 잘 정의되어 잇다.
struct User { var id = UUID() var name: String var email: String }
Swift
복사
Codable 과 같은 요소들 덕분에 JSON API 또는 호환가능한 데이터 소스로 Swift struct 를 직접적으로 대응시킬 수 있다. Realm 에서는 데이터베이스로부터 반환된는 객체가 native Swift 타입이다. domain model 들을 Swift type 에 매칭시킬 수 있다.
model 에 관한 가장 큰 문제는 도메인 model 이 계속해서 업데이트 된다는 것이다. 이런 상황에서는 데이터를 어떻게 구조화 해야할지 확실하지 않기 때문에 , object realational Mapping, SQL 쿼리를 이용한 정규화 기술이 중요하다.

View

view 는 뭘까? view 는 역할이 명확하다. 사용제에게 UI 를 보여주는 것, 사용자 상호작용을 handling 하는 것.
역사적으로 iOS 개발은 MVC 에 집중되어 있었고 MVC 에서 ViewController 의 역할은 Model 과 View 의 소통 창구 였지만, 이러한 소통 방법이 Controller 하나였기 때문에, View Contoller 에 너무 많은 코드가 쌓이게 되었고 결과적으로 Spagetti code 가 생기게 되었다.
view 와 관련된 가장 큰 이슈는 이식성과 디커플링이다. 얼마나 View 를 logic 과 분리 시켜서 View 를 재사용 가능하게 할 것인가가 가장 중요하다.

MVC 이후의 MVVM

눈치채셧겠지만, model 과 view 사이의 통신은 주로 모델데이터의 검색 및 조작(View 에서의 사용을 위한) 그리고 view 로 부터 model 로의 변경사항 저장입니다. MVVM 에서 View 는 중심적인 역할을 하게 됩니다.
MVVM 의 통신이 매우 단일하기 때문에 MVVM 은 binding 이라는 요소가 추가되어 잇습니다.
Binding 은 model 의 프로퍼티를 view 와 연결합니다
예를 들어 Bool 프로퍼티를 on-off 토글 스위치에 묶는다(bind)고 합시다, toggle 의 상태는 사용자가 이를 변경할 때마다 프로퍼티에게 다시 읽혀지며 프로퍼티로부터 토글의 상태가 읽어져서 스크린으로 보여집니다.
또다른 MVC 와 MVVM 의 공통점은: model 조작,
데이터베이스에 User table 이 존재한다고 가정합시다, 사용자의 주소가 깔끔하게 4,5 행으로 정리되어있다. 앱내에서 너는 오직 이 데이터를 규격화된 주소로만 show 하려고 한다.
뷰컨트롤러에서는 adress 를 규격화 하도록 강요받는다, model 로부터 데이터를 읽어올 때마다 말이다.
MVVM 의 강조점은 not controller 이다. model 간의 변형 코드를 다른 부분에 작성해 둘 수 있다.
아래와 같이 말이다
struct User { var id = UUID() var name: String = "" var email: String = "" var address_1: String = "" var address_2: String = "" var address_postcode: String = "" var address_city: String = "" var address_country: String = "" var address: String { [address_1, address_2, address_postcode, address_city, address_country] .filter { !$0.isEmpty } .joined(separator: "\n") } }
Swift
복사
address 라는 computed property 는 각각의 address 프로퍼티에 기반해서 만들어진다. 따라서 위의 관련 프로퍼티들은 private 으로 설정해 외부에서는 오직 address 프로퍼티만 읽혀지도록 제한할 수 잇다. 이러한 formatting code 는 extension 으로 작성하는 것이 관례이다.
항상 고민이 되는것은 바로 이 로직이 model , ViewModel , View 중 어디에 포함되어야 하느냐 이다. 이러한 formatting code 가 view 에 구체화된다면 view 에 포함시킨다. 네트워킹 코드를 viewModel 에 포함시킬 수 있다. 하지만 추출해서 어떠한 컨트롤러나 manager 요소로 따로 빼놓는것이 현명하다. 유사하게 쿼리들을 model 에 넣어둘 수 있다. 하지만 이러한것은 API 안에 구현해 놓는 것이 현명하다 (model 과 분리 시켜서)

ViewModel

뷰모델의 역할은 크게 두가지이다.
1.
model에서 얻어온 프로퍼티에 대한 binding 을 view 에 제공
2.
model에서 얻어온 데이터를 view 로 제공
ViewModel 이 View 보다 위에 있는 것으로 착각하기 쉬우나 ,
1.
ViewModel 은 View 안에 속한다.
2.
ViewModel 은 하나이상의 Model 을 참조한다.
View 는 model 은 매우 약하게 연결되어야 한다. 하지만 이는 항상 가능한 것은 아니다. View 는 ViewModel 을 소유한다. 그리고 ViewModel 은 model 을 소유한다. view 는 절대 model 을 소유하지 않는다.
그러나 model 은 view 에 자주 삽입된다, 이는 view 에 model 객체를 전달하는 가장 쉬운방법이다. 이렇게 삽입되었다고 해서 그 둘이 강하게 연결된것을 의미하지는 않는다 다른 접근 방식(protocol, generics)을 활용해 view 를 모듈화 할 수 있다.
모든 요소를 MVVM 에 끼워 맞추려고 하지말자, 다른 디자인패턴도 많다 Helper Controller, Manager, Facade, Factory 등등. 목표는 읽기 쉽고, 유지보수 및 확장이다. 한가지 디자인 패턴을 고수하는것은 현명하지않다.

MVVM 과 SwiftUI

SwiftUI ≠ MVVM
이 둘은 서로 굉장히 일치하는 부분이 많지만 이둘이 같다는 것은 완벽한 오해이다.
SwiftUI 는 View layer 의 프레임워크로 ViewModel 의 생성이 필수가 아니다.
SwiftUI 는 @State, @Binding and @ObservedObject 와 같은 프로퍼티 래퍼를 사용하여
데이터가 변경될 때 마다 자동으로 view 를 업데이트 한다.
이미 binding 이 존재하는 것 같이 보인다.
Combine : 다대다 데이터 흐름 제어의 프레임워크 SwiftUI 와 잘맞는다.
구문이 완전 간결하고, 데이터 조작이 이미 구현되어 있다. 대부분의 combine 코드가 비동기적으로 실행되지만, 어떻게 설정하느냐에 따라서 동기적인 코드와 유사하게 읽힌다.

MVVM 실전

첫번째로 model 과 view 를 먼저 작성한다.
어떠한 종류의 데이터를 사용하게 될지 명확하지 않다면, View 를 먼저 구성
만약 어떠한 데이터를 사용하게 될 지 이미 알고있다면 model 을 먼저 구성. 이렇게 하면 View 를 구성하기가 훨씬 쉬워진다. >> 실제 모델 객체를 사용할 수 있기 때문
struct Book: Identifiable, Codable { var id: Int var title: String var author: String }
Swift
복사
Identifiable : ID 프로퍼티를 이용해 객체 식별
Codable : 외부 API 의 URL 을 통해 받아온 JSON 데이터를 자동으로 파싱해줌
이러한 Book 데이터를 나타내는 view 를 작성한 바는 아래와 같다
struct BookRow: View { var book: Book var body: some View { VStack(alignment: .leading) { Text(book.title) Text(book.author) } } }
Swift
복사
이렇게 보는 바와 같이 BookRow 와 Book 는 강하게 커플링 되어있다. 이를 어떻게 디커플링 할것인가??

Decoupling the View and Model

protocol Detailable { var title: String { get } var subtitle: String { get } }
Swift
복사
디커플링은 프로토콜로 가능하다
바로 view 에서 보여주는 item 을 Book 이라는 model 로 한정짓는 것이 아닌
view 에서 사용하고자 하는 데이터의 조건을 만족하는 (프로토콜을 준수하는) model 이라면 전부 이 View 를 재사용할 수 있도록 구성하는 것이다.
extension Book: Detailable { var subtitle: String { return author } }
Swift
복사
먼저 사용하고자 했던 Model 인 Book 을 Detailable 프로토콜을 준수하게 만든다.
struct DetailRow: View { var item: Detailable var body: some View { VStack(alignment: .leading) { Text(item.title) Text(item.subtitle) } } }
Swift
복사
그러곤 해당 subView 에 전달되어야하는 데이터를 이 프로토콜을 준수하는 데이터로 확장한다.
이로써 BookRow 는 DetailRow 로 확장되었고
Detailable 프로토콜을 준수하는 모든 model 에 한해서 View 를 재사용할 수 있게 되었다

ViewModel 의 생성

ViewModel 은 View 와 Model 사이의 소통을 가능하게 하낟.
ViewModel 은 View 에 속한다. 하지만 반드시 View 에 속할 필요는 없다. 같은 ViewModel 을 다양한 View 에 사용해도 된다. 또는 하나의 중심 시작 포인트에서 사용하여 model data 를 view 를 타고 흐르게 만들 수 있다.
아래 코드를 보자
class BooksViewModel: ObservableObject { let url:URL! = URL(string: "/books.json") @Published var books = [Book]() init() { fetchBooks() } func fetchBooks() { URLSession.shared.dataTaskPublisher(for: url) .map { $0.data } .decode(type: [Book].self, decoder: JSONDecoder()) .replaceError(with: []) .eraseToAnyPublisher() .receive(on: DispatchQueue.main) .assign(to: &$books) } }
Swift
복사
주목할점은 아래와 같다.
1.
class 로 선언됨
2.
ObservableObject 준수
3.
외부 API 에서 Model 로의 mapping
4.
@Published : 이 프로퍼티 래퍼로 선언된 데이터가 변경될 시 이와 연결된 View 들을 자동으로 업데이트

View 와 ViewModel 의 결합

struct BookList: View { @ObservedObject var viewModel: BooksViewModel var body: some View { NavigationView { List(viewModel.books) { book in let index = NavigationLink(destination: BookEdit(book: $viewModel.books[index])) { BookRow(book: book) } } .onAppear { viewModel.fetchBooks() } } } }
Swift
복사
1.
@ObservedObject 를 활용해서 BooksViewModel 타입 프로퍼티 선언
이로써 @Published 로 선언된 변수와 View 가 반응형으로 묶임 (Binding)
2.
네비게이션 링크로의 subView 에게 Book 객체에 대한 binding 을 제공. BookEdit View 에서 Book model 에 대한 조작가능해진다!!
3.
view 가 나타남과 동시에 (onAppear) JSON 데이터에 대한 HTTP request 그리고 그것을 Book 객체로 변형한다.
4.
왜 BooksViewModel 의 객체생성 즉, 초기화를 하지 않았을까???
이는 오직 BookList view 가 ViewModel 에 대한 참조만을 갖게 하기 위함이다.
이것은 tightly- coupled 코드를 피하기 위해서이고 이것은 dependenct injection @10/26/2021, 9:00:00 AM 을 위한 문을 열어두는 것이다.