View
이번에 TCA를 공부하려고 자료를 열심히 보고 있었는데, 아카데미 멘토였던 young의 반가운 글이 보였다!
https://gist.github.com/unnnyong/439555659aa04bbbf78b2fcae9de7661
SwiftUI와 MVVM의 궁합이 썩 좋지만은 않다는 일본 글을 영이 번역을 해둔 글이였는데, 요 번역글이 꽤나 임팩트가 있었는지, 여러 블로그에서 위 아티클을 인용해서 요약해두거나, 저마다의 생각을 덧붙여둔 것을 볼 수 있었다.
이때까지 Presentation 계층의 아키텍처에 대해서 나도 특별한 생각없이 왼손에는 MVC, 오른손에는 MVVM을 쥔 채로 다른사람들의 MVI, MVP 패턴 등을 구경하기만 했었는데, 정작 이런 패턴들의 장단점, 득과 실에 대해서는 전혀 생각해본 적이 없었던 터라, 나도 여기에 대해서 한 번 정보를 모으고 생각을 정리해둘 필요가 있을 것이라 생각했다.
어쩌면 요런 내용들을 정리하면 단순히 지금 아키텍처를 결정하는데에 사용하는 것 뿐만 아니라, 앞으로의 기술, 프레임워크 선택이나 사고방식에 있어서 좀 더 합리적인 선택을 하도록 내가 변화하지 않을까 기대한다.
MVVM을 사용하는 이유
우선은 UIKit 환경에서의 여러 프로젝트들이 MVC를 떠나 MVVM을 다음 아키텍처로 채택했다고 알고있다.
MVVM 아키텍처 자체는 2010년 전후로 마이크로소프트 쪽의 개발 프로젝트에서 적용되기 시작하였고, 2016년 전후로 안드로이드 진영에서 프로젝트의 아키텍처로 채택하기 시작하였으며, 이 흐름에 따라 iOS 진영에서도 2010년대 후반부터 MVVM 아키텍처를 채택하기 시작하였다.
참고 : https://medium.com/mindorks/the-evolution-of-android-architectures-mvp-mvvm-mvi-f72f67093b81
[요기 아래는 나의 궁예 😎]
Apple의 UIKit 프레임워크 자체가 MVC를 기반으로 만들어졌기 때문에 많은 프로젝트에서는 MVC를 아키텍처로 채택해 개발을 진행했을 것이다. 또, 기존 UIKit에서 MVVM 패턴을 적용하기 위해서는 RxSwift와 같은 외부 라이브러리를 적용해야 데이터 바인딩의 처리가 가능했기에, Rx에 대한 Learning cost를 감당할 수 있으면서 리팩토링의 여력이 있는 팀이 프로젝트에 MVVM을 실험적으로 적용하기 시작했을 것으로 생각된다!
그러던 중 2019년에 SwiftUI가 소개되었다. SwiftUI에서는 @State, @Observable과 같은 어노테이션을 통해 데이터 바인딩 기능들이 편리하게 제공되어졌고, 2020년을 넘어가며 SwiftUI에 대한 학습이 어느정도 진행되자 여러 엔터프라이즈급의 iOS 프로젝트에 MVVM 아키텍처가 적용되기 시작하지 않았나 생각한다.
기존의 MVC 패턴을 사용해 UIKit 으로 개발을 할 때보다 MVVM을 적용하면 확실히 얻을 수 있는 이점이 많다.
- View와 ViewModel을 데이터 바인딩을 통해 연결하여, ViewModel 쪽 상태 변경에 따라 View가 자동으로 업데이트되며, 이에 따라 뷰의 업데이트에 필요한 코드의 양을 줄일 수 있다.
- ViewModel이 View 계층(UIKit)에 관련된 코드를 가지지 않는 독립적인 코드 덩어리가 되어, ViewModel에 대한 테스트 코드를 적용하기 간편해진다.
- 뷰와 뷰모델의 작업이 동시에 진행될 수 있다. 이를 통해 디자이너(프론트 개발자)와 로직 개발자가 동시 작업으로 시간 절약이 가능하다.
물론 어느 것이나 장점만을 가질 수는 없듯이, 여기에서 발생하는 단점도 있다.
- 데이터 바인딩이 필수적으로 요구된다. 바인딩을 위한 프레임워크에 대한 러닝 코스트!!
- 결국 ViewModel도 크기가 커질 수 있다.
그래서, 다른 아키텍처 말고 MVVM을 선택하는 이유는?
MVC, MVP 등의 패턴 대신에 MVVM 아키텍처 패턴을 선택하는 이유를 정리하면 다음과 같다.
- 계층별로 관심사 분리(책임 분리)가 가능하다.
- 각 계층의 모델이 너무 많은 책임을 지지 않아, 한 쪽의 코드가 기형적으로 비대해지지 않는다.
- 엔티티 별 코드의 복잡성이 낮아진다. (적은 책임 → 쉽게 코드의 역할을 이해 가능)
- 유닛 테스트의 구성이 쉽다
- 물론 MVP도 유닛 테스트를 적용할 수 있지만, 이 경우에는 가상의 뷰를 설정해줘야 한다. (Presentation이 View에 높은 의존성을 가지고 있기 때문)
- ViewModel이 View에 대한 의존성을 전혀 가지고 있지 않다는 점에서 MVVM이 테스팅에 더 유리하다
- 단방향으로 데이터가 전달되며, 이는 오류의 가능성을 크게 낮춰준다.
- user의 action → view → viewModel → data
- 상태 변경 → data → viewModel → view → user interface
- MVP에서도 동일한 형식으로 데이터의 흐름이 형성되어 있지만, MVVM에서는 데이터 바인딩을 통해 상태 변경에 따른 view 렌더링이 더 빠르게 진행될 수 있다.
그러나, 알다시피 요즘에는 TCA 같은 새로운 아키텍처 패턴도 등장하고 있다. 그래서 과연 새로 떠오르는 패턴들에 대비하여 MVVM이 어떤 장점을 가지고 있는지 궁금해서 “TCA 대신 MVVM을 선택하는 이유”, “Why MVVM is better than TCA” 같은 검색어로 검색을 해봤는데, 의외로 죄다 MVVM → TCA 로 갈아타야 하는 이유에 대한 글만 잔뜩 나왔다. ChatGPT에게 TCA보다 MVVM을 선택해야 하는 이유를 물어보니, 기존 개발자들이 MVVM에 익숙하기 때문에 생산성 측면에서 더 유리하다는 답을 줬다.
MVVM에서 더 발전된 형태의 아키텍처가 분명함에도, 앞으로 나아가기 어려운 이유가 러닝 코스트인걸 보니, “학습”이 여러가지 기술 적용 등에 있어서 보편적으로 적용되는 큰 장애물이라는 생각이든다.
SwiftUI에서는 MVVM이 별로이다!
앞서 언급한 것처럼, UIKit에서는 Data Binding을 위한 기능이 네이티브에 탑재되어 있지 않았기에 Observable 또는 Rx, Combine와 같은 도구를 통해 View(UIController)와 ViewModel의 state를 연결해주는 작업이 필요했다. 요런 코드가 state를 구독하는 View에서 작성되는 것이 아니라 state를 publish 하는 ViewModel 쪽에서 작성되어야 했기 때문에, UIKit 환경에서는 ViewModel이 데이터 바인딩을 위한 엔티티로 의미가 있었다.
그러나, SwiftUI에서는 View에 @State, @StateObject 등의 데이터 바인딩과 관련된 annotation을 사용할 수 있게 되면서 자연스럽게 View 내에서 상태를 관리하고, 상태의 변화에 따라 View를 자동으로 새로 그려주는 작업을 수행할 수 있게 되었다.
MVC 패턴에서 View 또는 Controller가 state를 가지고 있으면서 state 값 변경에 따라 화면을 새로 렌더링해주는 것과 무엇이 다르지..? 를 생각해봤을 때, state의 변화에 따라 화면을 “직접 렌더링하는 코드를 수행하냐” 아니면 “binding을 통해 자동으로 렌더링하도록 세팅이 되어있냐” 의 차이 정도로 이해를 했다.
즉, 이미 SwiftUI의 뷰에는 자체적으로 ViewModel의 역할을 수행할 수 있도록 데이터 바인딩을 수행하도록 해주는 Property Wrapper 들이 포함되어있다!
이러한 상황에서 Data Binding을 위한 ViewModel을 View 외부에 만들고 한 번 더 연결하는 일은 View-State 가 가능함에도 View-ViewModel-State 로 불필요하게 ViewModel 이라는 계층을 삽입하는 것이 되어버리고, 이 때문에 불필요한 코드가 추가되고 양방향 데이터 흐름이 만들어진다는 것 (View에서 ViewModel이 가진 state를 변경할 수 있다는 의미), 그리고 이로 인해 오류 발생 가능성이 높아진다는 것이 MVVM을 지양하자는 사람들의 의견이다.
여러 블로그들의 글을 읽기 전에 내가 생각하는 ViewModel의 역할은 2가지라고 생각했다.
- View가 참조할 state 데이터 관리 + 변경 알림
- 비즈니스 로직을 View로부터 분리 → UI와 로직을 분리하여 관리하기 편하도록 + 테스트하기 편하도록
좋아, 첫 번째 state 데이터에 대해서는 위 내용으로 불필요함을 이해하였다. 그러나, 두 번째 영역이 나는 또 하나의 중요한 ViewModel의 기능이라고 생각했다. iOS 개발을 진행할때나 플러터로 프로젝트를 진행할 때 둘 다 MVVM 구조로 코드를 작성했었는데, 로직과 View를 두 파일로 분리하여 작성하면서 꽤나 명쾌한 구조라는 생각이 들었었다. 또 (테스트코드를 실제로 작성하지는 못했지만) 테스트가 가능한 코드를 만들기 위해서는 “View” 보다는 “뷰에 보여지는 값”이 중요하며, ViewModel의 state 만 확인하면 더 간단하게 테스트를 작성할 수 있기 때문에 테스트를 작성하고 적용하기에 적합할 것이라 생각했다.
그런데 여기에서의 “View와 비즈니스 로직의 분리”의 역할을 담당하는 엔티티가 ViewModel 이라는 이름을 갖는게 어색하기 때문에, 오히려 TCA의 Store 또는 Model 이라는 이름을 사용하는게 더 적합하다는게 MVVM 지향파 사람들의 의견이다.
흠…
원래는 MVVM으로 분리가 되어있었다면, 여기에서 ViewModel의 역할 중 State 관리를 View에게 주고, 비즈니스 로직을 처리하는 Store를 따로 만들어 사용하자는 의견이라고 생각된다.
에?? View한테 책임이 늘어난 거 말고는 똑같은거 아닌가?? 라고 생각이 들지만, 여기에서의 Store는 Flux 아키텍처 또는 TCA 아키텍처의 Store 라고 생각해야 한다.
ㅤ
Flux 아키텍처와 TCA 아키텍처를 아직 제대로 공부하거나 사용해보지는 않았지만, 아래 영상과 여러 자료를 정독하면서 간단하게 이해를 해봤다.
참고 : https://www.youtube.com/watch?v=wQFBgKl1PYw
ㅤ
SwiftUI를 사용할 때 MVVM의 형태를 유지하면서 ViewModel을 Store로 대체하였을 때의 대략적인 아키텍처는 아래와 같은 형태일 것으로 생각된다. 물론 실제 TCA 아키텍처 등에서는 Store가 State를 관리하고 있지만, 우선은 위에서 언급한 내용처럼 View가 자신의 State를 관리하는 형태로 데이터의 흐름을 그려봤다.
여기에서 눈여겨 봐야할 점은 View 에서 Action이 직접 State를 변경할 수 있는 것이 아닌, 사용자의 액션이 발생했을 때 Store를 거쳐 State의 변경이 일어나고, 이것이 UI 에 반영된다는 점이다. 또 Reducer와 Model이 양방향으로 소통하는 것 같지만, 사실상 Store의 request에 대해 Model은 결과를 반환할 뿐이기 때문에 데이터는 Reducer → Model → Reducer 의 흐름을 가진다 (Model이 Store에게 요청을 보내지 않는다). 따라서 단방향 데이터 흐름이라는 방식도 위 구조에서는 유지가 가능하다!
ㅤ
즉 이러한 방식으로 구조를 변경하면 앞서 언급했던 기존의 MVVM 을 보완할 수 있다.
- ViewModel에 의해 SwiftUI의 데이터 바인딩을 중복으로 사용할 필요가 없어진다.
- ViewModel이 가지고 있던 비즈니스 로직을 Store가 책임지도록 한 뒤, 데이터의 흐름을 고정시켜 단방향 데이터 흐름을 만들어 예상치 못한 에러를 피할 수 있다.
ㅤ
한 문장으로 요약하자면 “MVVM을 하기 위한 SwiftUI 코드를 짜지 말고, 데이터의 흐름을 어떻게 만들고 상태 관리를 어떻게 할 것인지를 고민하자” 인 것 같다.
ㅤ
아니다, 그럼에도 SwiftUI에서 MVVM을 사용할 만 하다
요 의견에 대한 부분에 대해서는 아래 블로그의 내용을 참고하였다. 이미 대중의 여론은 MVVM을 지양하는 방향으로 돌아서버린 것 같다. 그래서 이 아티클이 아니였다면 MVVM을 버리는게 완전히 타당한 아이디어구나! 하고 넘어갔을텐데, 덕분에 MVVM을 그대로 유지할 이유가 있을까 를 다시 한 번 생각해보게 되었다.
https://doing-programming.tistory.com/entry/SwiftUI-에서-MVVM-을-멈춰야-하는가
ㅤ
위 아티클에서는 State와 Binding이 데이터 바인딩을 책임지기에는 부족하다고 언급한다. 아래에서 몇가지 예시를 통해 완전 데이터 바인딩으로 상태를 관리하기 어려움을 확인해보자.
State를 사용하면 되잖아
아래처럼 struct와 class로 구성된 모델이 2개 있다고 해보자.
두 모델을 모두 상태값으로 사용하기 위해 @State 프로퍼티 래퍼로 감싸줬다. 그리고 각자 change 함수를 호출하여 모델 내부의 count의 값을 변경해주었다.
위 코드를 실행한 결과는 다음과 같다.
- struct 모델을 사용한 쪽에서는 count의 변경을 감지하고 뷰를 새로 그려준다.
- class 모델을 사용한 쪽에서는 count의 변경을 감지하지 못하고 뷰를 그리지 못한다.ㅤ
클래스 모델을 사용할 경우에 값이 변경되는 것을 감지하지 못하는 이유는 reference 타입이라 그렇다고 알고있다. 변경된 값을 가리키는 메모리 주소 자체는 count 호출 이전이나 이후나 변경되지 않았으니, 뷰에서는 count 변경사항을 감지할 수 없다.
ㅤ
혹시나? 하는 마음에 Equatable 프로토콜을 활용해 count 값이 변경되면 각자 다른 클래스 모델 인스턴스라는 코드를 작성했봤다.
혹시나하면 늘 역시나. 이것도 통하지는 않았다 ㅋㅋㅋ 값 변경 자체를 감지하는 것이지, 뭔가 이벤트가 발생하면 이전 상태와 현재 상태를 비교하여 상태 변경을 감지하는 방식으로 로직이 돌아가지는 않나보다.
ㅤ
StateObject 쓰면 클래스도 상태로 사용할 수 있는데?
요런 식으로 class 에 ObservableObject 프로토콜을 달아주고, 변화에 대해 notify 해주고 싶은 상태 값 앞에 @Published 래퍼를 달아주면 class 모델도 상태를 표현하는 객체로 사용이 가능하다!
상태 값 자체를 @State 대신에 @StateObject로 감싸주면, Published 어노테이션에 의해 해당 상태 값이 변경될 때 자동으로 ObjectWillChange() 함수를 호출해주고, @StateObject 에서 이 변화에 대한 notify를 받아 상태의 변경이 있었음을 뷰에 전달해, 뷰를 새로 랜더링하도록 해준다.
ㅤ
그러나 요런 방식에도 문제가 하나 있다.
ㅤ
클래스로 선언된 모델 안에 클래스가 하나 더 있다면? 근데 이 클래스 값의 변경사항에 대해 notify가 필요한 상황이라면 어떻게 이를 작성해줘야하지?
ㅤ
앞서 사용했던 2가지 방식을 다시 여기에서도 적용해보려 했다.
1. @Published 적용하기
- 실제로 해보니, 작동하지 않았다.
- Published 자체가 값의 변경이 있어야 objectWillChange를 호출해줄텐데, 여전히 메모리 주소값은 동일하기 때문에 변경 사항을 알리지 못한다는 문제가 있었다.
2. ObservableObject + StateObject / ObservedObject 적용하기
솔직히 나는 이거 될 줄 알았다- StateObject 래퍼는 View 클래스를 상속하는 곳에만 붙을 수 있다는 런타임 경고가 떴다. 아무래도 View와 동일한 생명주기를 가지고 동작하도록 만들어주는 녀석이라 그런지 요 방식은 사용 자체가 불가능했다.
- 그래서 StateObject 대신에 ObservedObject 래퍼를 사용해봤다. 요 녀석은 View 생명주기랑 상관 없이 고유한 생명주기를 가지고 있으니 되지 않을까? 라는 생각에 사용을 해봤지만 여전히 변경사항을 뷰 쪽으로 알리지는 못했다.
ㅤ
요러한 이유로 SwiftUI의 기본 기능인 State, Observable 만을 활용해서는 완전히 데이터 바인딩으로 상태 변경을 뷰에게 알리는 것이 어렵다는 것이 위에서 언급한 아티클이 말하는 단점이였다. 따라서 Model에서 한 번에 View까지 값을 가져오지 않고, ViewModel이 중간에서 한 번 Model로부터 상태 값들을 받아서 State로 사용할 수 있는 형태로 가공을 해줘야 원활한 데이터 바인딩이 가능하다는 것이다.
ㅤ
인정합니다. struct로 모든 데이터 모델을 만들수는 없을 것 같은데, 공감이 되었다.
ㅤ
또, 이런 식으로 View 내에 있는 프로퍼티로 상태 값들을 관리하게 되면, 상태 값이라는게 앱 서비스 전체에서 관리하고 접근할 수 있는 값이 아닌 현재 띄워져있는 View 내에 종속된 프로퍼티가 된다. 이 말은, 뷰가 사라지면 상태 값도 사라져버린다는 것을 말한다. 재사용성이 떨어진다.
ㅤ
모델에서 가져온 entity 데이터가 그대로 뷰에게 들어간다는 것도 어느정도 문제가 될 것 같다. 플러터로 앱개발을 하면서 클린 아키텍처로 프로젝트를 구성할 때, Data - Domain - Presentation 에서 사용하는 데이터 모델 형태를 각기 다르게 나눠서 각자가 적합한 형태로 가공하여 사용하도록 해줬는데, 이런 부분들이 ViewModel에서 일어나게 코드를 작성해뒀다. 그런데 ViewModel이 없어지면 View의 코드 중간에 이런 Raw Model을 적합한 형태로 가공하거나 파싱하는 로직이 들어가버려야하는데, 이 너무 코드를 더럽히는 일이 아닌가?
ㅤ
ㅤ
요런 이유들로 SwiftUI에서 MVVM은 현재 여론대로 완전히 안티 패턴까지는 아니라고 아티클이 말하고 있다. 개인적으로 프로젝트를 진행하면서는 여기에서 언급된 부분들에 ViewModel의 이점을 처절하게 느꼈었기 때문에 MVVM이 나쁜게 아니라는 쪽에 마음이 더 가는 것 같다.
나는 그럼 SwiftUI에서 기존의 MVVM을 사용할 것인가?
이러한 의견들을 들었을 때, 나도 이제 기존의 MVVM을 계속 적용할지, 아니면 개선된 아키텍처를 사용할 지 선택을 해야할 것이다. 글 앞부분에서 언급했던 것처럼, 이를 선택하는 기준은 “성능”, “개발 지속성, 유지보수”, “확장성” 같은 부분 보다는 아무래도 “학습 비용”이 될 것 같다. 여기에 따르자면 나는 아마 지금 수준에서는 SwiftUI로 개발을 진행할 때 기존의 MVVM을 계속 사용할 것 같다.
ㅤ
사실 최근 진행중인 프로젝트에서 테스트코드를 아직 작성하지는 않았지만, “테스트 코드를 작성한다면 이런 부분들을 신경써야 할 것이다!” 라고 염두에 두고 개발을 진행해오고 있었다. 이런 면에서 현재 내가 이해하고 있는 기존의 MVVM이 꽤나 명쾌하게 View와 ViewModel이 책임을 잘 분리하고 있고, ViewModel에서는 input에 따른 output 을 반환해주고 있다고 생각한다. 또, 기존의 MVVM에서 양방향 데이터 바인딩이 가지는 단점을 아직 체감해보지 못했기에 (사실 이 부분이 제일 큰 이유일 것 같다) 과연 ViewModel을 없애는게 필요한지 잘 체감이 되지는 않는 것 같다.
ㅤ
그러나 MVC를 사용할 때에도 MVVM이 별로 필요하지 않을 것이라는 동일한 생각을 가지고 있었던 나이기에, 이번에 TCA를 공부할 때 이 “데이터 단방향 흐름”과 “코드 구조 단순화”가 주는 이점이 어떤 것인지 좀 더 신경쓰면서 구조를 파악하려고 하면 더 배우는게 많지 않을까 싶다. 오히려 한 달 뒤에는 TCA 예찬론자가 되어있을지도 모르겠다. 😆
ㅤ
물론 찾아보면서 아래 사진을 봤을 때에는 어라..? 싶기는 했다. 듣고보니 맞는 말 같았다. SwiftUI에서의 View의 영역은 Body 쪽이고, ViewModel의 영역이 State와 Action이라면 각자의 역할을 다 흡수하고 있는 형태일 수 있겠다는 생각이 들었다. 그치만… View랑 로직을 분리하지 않으면 코드가 막막 길어지고 복잡해지지 않나..? 그건 그냥 나의 실력 탓인가..?
이런 이유로 아래 짤이 돌았나 싶었다. ㅋㅋㅋ 다시 봐두 웃김
이전에 애플 아카데미에 있을 때 이 짤을 되게 재미있게 봤었는데, 그때는 MVVM에 대해 제대로 이해하지도 못한 레벨이라서 정확히 어떤 의미로서 이 짤이 돌아다녔는지는 몰랐던 것 같다 ㅋㅋㅋㅋ
ㅤ
이걸 혼자 공부하면서 이렇게 다시 마주하게 될 것이라고는 생각을 못했었는데, 역시 아는 만큼 보이는 것 같다. 영이 번역한 아티클도 한국의 iOS 개발자들에게 꽤나 큰 타격을 준 모양이다. 생각보다 많은 사람들이 열띄게 블로그로 자신의 의견을 주고받는 걸 보니, 나도 이런 내용의 논의거리들을 빠르게 캐치해서 주변 사람들과 논하는 날이 오면 좋겠다…
ㅤ
그럼 열심히 TCA 공부해보자!!
샤라웃
https://gist.github.com/unnnyong/439555659aa04bbbf78b2fcae9de7661
https://medium.com/mindorks/the-evolution-of-android-architectures-mvp-mvvm-mvi-f72f67093b81
https://velog.io/@kshyeon123/React-Flux-Architecture
https://velog.io/@fe_jungseok/MVVM-jqvanos9
https://velog.io/@flamozzi/SwiftUI-MVVM의-대한-고찰
https://green1229.tistory.com/267
https://doing-programming.tistory.com/entry/SwiftUI-에서-MVVM-을-멈춰야-하는가
'Develop > iOS 개발' 카테고리의 다른 글
[iOS] Apple MVC ≠ Standard MVC (0) | 2024.05.07 |
---|---|
iOS 취업공고 16개 자격요건, 우대사항 간추리기 (0) | 2024.04.25 |
[Swift] 스위프트는 Protocol-Oriented Programming 을 지향한다! (0) | 2024.03.24 |
[Swift] DateFormatter는 매번 생성하지말고 재활용하자 (0) | 2024.03.17 |
[Swift] Array를 ContiguousArray로 바꿔서 최적화 (0) | 2024.03.16 |