본문 바로가기
Architecture_Pattern

현재 정착한 mvvm 아키택처 패턴 (for UIKit)

by LeviiOS 2024. 3. 11.
반응형

MVVM (Model-View-ViewModel)은 UI 개발에 쓰이는 아키텍처 패턴이다. 이 패턴은 개발자가 비즈니스 로직과 사용자 인터페이스를 효율적으로 나눌 수 있게 만들어준다. 주로 세 부분으로 구성되어 있다.

  • Model (모델): 앱의 데이터와 비즈니스 로직을 맡고 있다. 데이터 저장소, 모델, 서비스 등이 여기 포함되며, 데이터 처리 로직을 다룬다.
  • View (뷰): 사용자가 보는 UI 부분이다. 뷰는 사용자의 앱 상호작용을 정의하고, MVVM에서는 뷰가 ViewModel을 통해 데이터를 보여주도록 설계되어 있다.
  • ViewModel (뷰모델): 뷰와 모델 사이의 중간자 역할을 한다. 뷰모델은 뷰에 필요한 데이터와 명령을 제공하고, 뷰는 데이터 바인딩을 통해 모델의 상태 변경을 반영하거나 사용자 입력을 모델로 전달할 수 있게 한다. 이 과정을 통해 뷰와 모델 사이의 의존성이 줄고, 코드 재사용과 유지 보수가 쉬워진다.

하지만, MVVM을 실제로 써보면, 상태 관리나 이벤트 흐름을 다루는 게 복잡해질 수 있다는 걸 알게 되었고, 여러 정보들을 찾아가면서 구현한 결과, 이벤트 흐름을 Input과 Output으로 명확하게 나누는 방식이 도움이 되었다. viewModel.isHiddenView 같은 상태값을 다루다 보면 유지 보수 측면에서 문제가 생길 수도 있었다는 것도 경험했다. 마지막 부분에 주의사항을 나름 정리했는데, 이 내용대로 코드를 작성하게 되면 여러 장점들을 가져갈 수 있었다.
 
Combine Framework을 사용해서 mvvm 패턴을 구현하게 되었다. 데이터 흐름을 더 잘 관리할 수 있게 되고, 앱이 더 반응적이며 유지 보수하기 쉬워졌다. 이런 경험을 통해 배운 건, 아키텍처 패턴을 그냥 따르는 게 아니라, 실제 프로젝트의 상황에 맞게 조정하고 적용해야 한다는 것이다. MVVM도 좋은 패턴이지만, 상황에 따라 한계를 알고 적절한 도구를 사용해 보완할 필요가 있다는 점을 깨달았다.
 

뷰 모델 전체 흐름

 
아래 이미지에서 보여지는 내용은 MVVM 패턴에서의 Input과 Output을 구체적으로 다루는 Swift 코드의 일부다.
Input은 사용자 또는 시스템 이벤트를 ViewModel로 전달하는 역할을 하고, Output은 ViewModel에서 View로 정보를 전달할 때 사용되는 부분이다.
이 구조를 통해 더 명확하고 정돈된 데이터 흐름을 만들 수 있어, 앱의 유지 보수성과 확장성이 향상될 수 있다.
 
ViewInput Enum:
ViewInput은 뷰의 생명주기 이벤트(viewDidLoad)와 사용자 이벤트(didTappedButton)를 enum으로 정의하고 있다. 이런식으로 Input을 정의함으로써, View가 ViewModel에 어떤 이벤트를 전달해야 할 때, 해당 이벤트들을 명확하게 구분하고 사용할 수 있다.

Input 타입

 
ViewOutput Enum:
반면 ViewOutput은 ViewModel이 View에 전달해야 할 결과물을 enum으로 정의한다. 예를 들어 didChangeBackgroundColor(backgroundColor: UIColor)와 didChangeButtonTitle(title: String)은
ViewModel에서 처리한 로직의 결과를 뷰에게 알리는 역할을 한다.

Output 타입

 
 
ViewProtocol:
ViewProtocol은 뷰와 뷰모델 사이의 커뮤니케이션을 정의하기 위해 input과 cancellables를 사용하는데, 
input은 뷰 이벤트를 뷰모델로 전달하고, cancellables는 Combine의 구독을 관리하기 위한 집합이다.
ViewController는 ViewProtocol을 준수하며, PassthroughSubject를 사용해 input을 초기화한다. 
 
이 input을 통해 이벤트를 뷰모델로 전달하고, 뷰모델의 transform 메소드를 통해 뷰모델로부터 출력을 받아 뷰를 업데이트한다. 
예를 들어, 뷰모델이 버튼의 타이틀 변경을 요청하면 didChangeButtonTitle 케이스를 통해, 또 배경색 변경을 요청하면 didChangeBackgroundColor 케이스를 통해 뷰를 업데이트한다.
IBAction 함수는 사용자의 인터랙션을 처리하고, 뷰모델의 input에 이벤트를 전달한다. 
 
바인딩 함수 setupBind를 통해 뷰와 뷰모델을 연결하는데, 이 함수 내에서 뷰모델에서 전달받은 Output을 처리한다. 이렇게 하면 모든 Output을 한눈에 볼 수 있어 효과적인 관리가 가능하다. 또한, input을 사용해 뷰모델에 이벤트를 전달하는 코드가 명시되어 있어, "input"과 "output"의 흐름을 쉽게 파악할 수 있다.

ViewProtocol
뷰 프로토콜 구현 결과 예시

 
ViewModelProtocol:
뷰 모델은 ViewModelProtocol을 준수하며, 여기서는 output과 cancellables를 정의하고 있다.
이를 통해 뷰 모델의 output을 관찰하고, 이를 뷰에 반영할 수 있다. 뷰 모델 내에서 발생하는 이벤트들은 output을 통해 뷰에 전달되며, 이를 통해 뷰는 필요한 변경을 수행할 수 있다.
ViewController에서 input을 통해 뷰 모델에 이벤트를 전달하며, 뷰 모델은 이 이벤트들을 처리한 뒤 output을 통해 결과를 뷰에 전달한다. 이 과정에서 transform(input:) 함수를 통해 input을 받고, output으로 변환해 뷰에 제공한다. 예를 들어, input으로부터 .viewDidLoad 이벤트를 받으면, changeBackgroundColor() 함수를 통해 배경색을 변경하고, .didTappedChangeTitleButton 이벤트를 받으면 changeTitle() 함수를 통해 타이틀을 변경한다.

뷰 모델 프로토콜 정의
뷰 모델 프로토콜 구현 예시

 
아래 모델 예시는 참고용으로만 보면 될거 같다.

모델 예시

이로써 View, ViewModel, Model 간의 이벤트 흐름과 상태 관리는 이 구현을 통해 어느 정도 체계적으로 다룰 수 있다.
 
뷰와 뷰 사이의 커뮤니케이션:
View 간의 이벤트 흐름을 delegate로 정의하는 이유는, UIKit이 제공하는 다양한 뷰들이 이 패턴을 사용하기 때문이다. 일관성을 유지하기 위해 커스텀 뷰들도 이 패턴으로 따르도록 작성했다.
 
MVVM 구현 시 주의해야 할 점:
첫째, 뷰에서 viewModel.state와 같은 코드로 상태에 직접 접근하는 것은 피해야 한다. 상태 변경은 반드시 Input을 통해 Output으로 전달되어야 한다.
둘째, Output은 이벤트의 종료를 나타내므로, 네이밍에 'did'나 과거형을 사용해야 한다. 보통 did...FromViewModel()로 함수명을 작성하여 뷰 모델에서 온 Output 이벤트를 처리하고 있다.
셋째, Output을 받은 후에는 Output내부에서 viewModel.event()와 같은 함수를 호출하지 않는 것이 좋다. 이는 Output을 통해 이벤트가 종료되었음을 분명히 하고, 이벤트 처리의 한 사이클이 끝났다는 것을 명확히 하는 데 중요하다.
 
추가적으로 구현 시 주의해야 할 몇 가지 사항:
첫째, 불필요한 바인딩은 최소화한다. 이는 메모리 누수를 방지하고 성능을 최적화하는 데 기여한다. setupBind(), transform(input:) 메소드를 통해서 input, output 바인딩 처리를 하고 있다. 만약 이 부분이 아닌 다른 영역에서도 바인딩을 처리를 하게 되었을때, 불필요한 바인딩 및 관리, 구독을 취소하지 않는 실수가 일어날 수 있다.
둘째, Input과 Output 사이의 커뮤니케이션은 가능한 한 단방향으로 유지한다. 이는 코드의 추적과 디버깅을 용이하게 만들어 준다.
셋째, 재사용 가능한 컴포넌트를 최대한 활용하여 코드 중복을 줄인다. 이는 유지 관리를 간소화하고 프로젝트의 일관성을 높이는 데 기여한다.
 
위와 같이 작성하니 함수형 프로그래밍의 장점도 가져갈 수 있게 되었다.
상태 변화의 예측 가능성: 불변성 덕분에 상태는 항상 예측 가능하고 안정적이다.
부작용 감소: 함수가 데이터를 조작할 때, 부작용이 줄어들어서 버그 잡기가 수월해진다.
선언적 데이터 흐름: Combine처럼 데이터 흐름을 선언적으로 다루니까, 앱의 로직을 따라가기가 직관적이다. 무슨 일이 어디서 벌어지는지 한눈에 보이게 되었다.
UI 동기화 쉬움: UI 업데이트가 동기화돼서 사용자 경험도 좋아진다.
코드 가독성 향상: 함수 하나하나가 자기 할 일을 명확하게 알려주니 코드 읽기가 더 수월해진다.
테스트 용이: 함수가 독립적으로 작동해서, 테스트가 용이해졌다.
 
위에 코드들이 정답은 절대 아니다. 오히려 틀렸다거나 이상한 코드라고 생각이 들 수도 있다. 다만 개인적으로 위와 같은 규칙으로 MVVM패턴을 반영했을때, 나름 잘 정리된 코드를 작성할 수 있다고 생각했고, 나와 같이 고민하는 분들에게 조금이나마 도움이 되었으면 한다.
 
긴 글 읽어줘서 다시한번 감사하단 인사 드립니다. ( _ _ ) 

반응형