MVVM 은 왜 쓰는걸까? by SwiftUI

Newon·2023년 4월 10일
2
post-thumbnail

주니어 개발자로, SwiftUI 를 통해 MVVM 에 대한 설명 후 SwiftUI 가 MVVM 에 찰떡이라고 느꼈던 주관을 풀어낸 글 입니다.

인스턴스는 다양하게 사용할 수 있는 단어이지만,
여기서는 메모리에 적재되어 실재하는 데이터 덩어리 라는 의미로만 사용됩니다.

독자는 SwiftUI 를 통해 UI 를 작성하는 방법을 알고 있으며,
Modifier 와 Property Wrapper 가 무엇인지 알고 있다고 가정한 글 입니다.



디자인패턴이란?

모바일 개발을 진행하다보면 흔히들 MVC, MVP, MVVM 이라는 디자인 패턴을 보게 된다. 하지만 어떤 글을 보더라도, 뭐라카는데? 라는 느낌이 들 것이다. 왜 그럴까?

What's a design pattern? / Refactoring Guru 에서는 디자인 패턴을 다음과 같이 설명하고 있다.

디자인 패턴은 소프트웨어 개발 중 흔하게 발생하는 문제들을 해결할 수 있는 일반론들을 모아 놓은 것이다. 디자인 패턴은 미리 만들어진 청사진과 같아서, 현재 발생하고 있는 설계 (*design) 문제들을 자신의 코드에 맞게 커스터마이징해서 적용하여 사용하는 것이다.

따라서 특정 디자인 패턴을 찾았다고 해서, 코드에 복붙을 할 수는 없다. 디자인 패턴은 함수나 라이브러리가 아니라서, 특정 코드 블럭을 복붙한다고 해서 적용할 수 있는 것이 아니기 때문이다.

대신 디자인 패턴은 특정 문제를 풀기 위한 넓은 의미로서의 개념과 같다. 디자인 패턴을 사용하되, 세밀하게 현재 코드에 맞도록 적용함으로서 이 개념을 적용할 수 있다.

요약하자면 특정 문제를 해결하기 위한 일반적인 개념 으로 볼 수 있겠다.
그래서 디자인 패턴은 실제 코드와 실제 문제가 있어야만 적용할 수 있는 것이다.



현재 적용한 MVVM 구조

그러니까 바로 실제 MVVM 을 그냥 바로 봐보자. 원래 모르면 맞아야 한다. ㅎ

현재 진행하고 있는 프로젝트의 구조이다.
파란색은 MVVM 의 필수 요소이고, 초록색은 내가 임의로 사용하는 요소이다.

예?


MVVM 디자인 패턴은 구조를 해결하기 위한 문제이다.
마이크로소프트 의 MVVM 을 설명을 빌리자면 다음과 같이 이야기할 수 있다.

MVVM(Model-View-ViewModel)은 UI 및 비 UI 코드(이하 비즈니스 로직) 를 분리하기 위한 UI 아키텍처 디자인 패턴입니다.

MVVM을 사용하여
1) UI를 선언적으로 정의하여 UI 와 관련된 내용만 깔끔하게 정리할 수 있으며,
2) 비즈니스 로직만 모아놓은 파일을 따로 정리할 수 있으며,
3) UI 와 비즈니스 로직을 거친 데이터를 동기화 상태를 유지할 수 있고,
4) 적절한 명령으로 UI 와 비즈니스 로직을 라우팅하여, 느슨할 결합을 제공할 수 있습니다.

*이 내용은 임의로 변형 & 요약한 내용입니다. 원본은 링크에서 확인해주세요.

1번과 2번 항목은 직관적이다.
UI 코드들만 모아서 정리하고 싶고, 비즈니스 로직만 모아서 정리하고 싶다.

  1. 1번을 실제 구조와 비교해보자. 구조의 사진을 다시 보면 Viwe 안에 폴더를 다시 나눠서 메인 화면, 스플래쉬 화면, 로그인 화면, 등등으로 나누어놓고, 펼쳐놓지 않았지만 ㅎ 내부에는 폴더 제목에 해당하는 UI 만 있을 것이라고 쉽게 예상할 수 있다. 실제로 내부에는 UI 와 관련된 내용 + List 등 아이템이 관련된 UI 파일만 있다.
  2. 2번을 실제구조와 비교해보자.
    참고로 Wassup 은 기획에서 나온 UI 컨셉 이름 중 하나이다.
    User 와 관련된 viewModel, Wassup 과 관련된 viewModel, Wassup 에서도 Poster, Video 관련된 viewModel 이 각각 있다.
    각 파일들은 해당 개념, 즉 User / Wassup / Wassup 중에서도 Poster / Wassup 중에서도 Video 와 관련된 비즈니스 로직만 작성했다.



3번과 4번은 의미가 묘하니, 조금 더 살펴보자.

3번, UI비즈니스 로직을 거친 데이터 를 동기화를 한다는 이야기를 풀어보면 비즈니스 로직 은 결국 로직일 뿐, 실제로 UI 에 들어가는 것은 데이터라는 것을 집중하면 쉽게 이해할 수 있다. UI 에서 참여하기 버튼 등을 클릭했을 때, 그것을 특정 비즈니스 로직 (실제로 참여할 수 있는가? 없는가? 등을 해결한 후) 데이터적으로 반응해야 하는데 MVVM 은 이를 쉽게 해결할 수 있다는 이야기다.

4번, 적절한 명령 으로 UI비즈니스 로직 을 라우팅하여, 느슨한 결합 을 제공한다는 이야기를 풀어보면, 얼핏 보면 3번의 내용이 코드적으로 너무 복잡하고, 또 까딱하면 데이터가 서로 엮여서 수정하려면 대규모 리팩토링이 될 것 같은데 MVVM 은 적절한 명령 을 통해 느슨하게 연결해서, 코드가 종속적이지 않고 수정을 쉽게끔 할 수 있다는 이야기이다.

곰곰히 생각해보면 3번과 4번은 하나의 행동, UI데이터 를 연결에 대해서만 이야기 하고 있다. 적절한 명령 을 쓰면 UI비즈니스 로직을 처리한 데이터 를 동기화 하면서도 느슨한 결합 을 제공할 수 있다는 것이다.


애플은 마이크로소프트만큼 좋은 회사여서 그런지, SwiftUI 역시 적절한 명령 을 가지고 있다. 다음 실제 코드를 살펴보며 3번 내용 먼저 확인해보자.

@StateObject private var viewModel: UserViewModel 코드는
UserViewModel 자료형을 가진 변수에, @StateObject 라는 미리 SwiftUI 가 선언한 속성을 부여하고 있다.

공식문서 가라사대, @StateObject 는 SwiftUI View 의 파라미터에 쓸 수 있는 속성이다. 입력값이 ObservableObject, 관측 가능한 오브젝트, 이며 viewObservableObject 의 속성이 변경될 때 마다 Publish, 출력하기를 바란다면 사용하기를 권장하고 있다.

Publish 가 강조된 이유는 단순히 출력, 출판을 한다는 의미가 아니라 Combine 에서 사용된 Publish 의 개념이기 때문이다.

이 글에서 Combine 개념은 필요없지만
궁금하다면 Introducing Combine 를 확인하면 알 수 있다.

쉽게말하면 값이 바뀔 때 마다 view 에 적용하고 싶으면 @StateObject 를 붙여서 사용하라고 할 수 있다. 단, 값 또한 관측이 가능해야 한다고 한다.

값의 관측은 viewModel 의 이야기이니, 이미 처리했다라고 생각하고
우선 UI 에 집중하며 계속해서 코드를 다시 정리해보자.

@StateObject private var viewModel: UserViewModel = UserViewModel() // 선언

var body: some Scene {
	windowGroupd {
    	SplashView()
        	.environmentObject(viewModel) 
			// 원하는 뷰에 적절한 명령을 통하여
            // 똑같은 인스턴스를 제공해주고 있다.
    }
}

이 코드에서 viewModel 을 초기화하였고, 이를 SplashView 에 .environmentObject() Modifier 를 사용하여 viewModel 을 전달하고 있다.

@StateObjectObservableObject 를 선언할 때 사용하며,
@ObservedObject 가 하나의 파일에서, 전달받아서 사용할 때,
@environmentObject 는 인스턴스된 ObservableObject 를 상위뷰로부터 전달받을 때 사용된다.

주석

@ObservedObject@EnvironmentObject 의 차이는
파라미터로 전달해야 하는가, Modifier 로 전달해야 하는가의 차이라고 생각한다.

코드로 보자면

struct MyView: View {
    @StateObject private var model = DataModel() // @State 로 선언

    var body: some View {
        Text(model.name)
        MySubView(model: model) // @ObervedObject 를 파라미터로 전달
        YourSubView()
        	.environmentObject(model: model) 
            // @EnvironmentObject 를 Modifier 로 전달
    }
}

struct MySubView: View {
    @ObservedObject var model: DataModel

    var body: some View {
        Toggle("Enabled", isOn: $model.isEnabled)
    }
}

struct YourSubView: View {
    @EnvironmentObject var model: DataModel

    var body: some View {
        Toggle("Enabled", isOn: $model.isEnabled)
    }
}

공식문서에서는
@ObservedObject - for Subview
@EnvironmentObject - for descendant
로 구분하였는데, 큰 설명은 없기에 취사선택 하면 되는 것 같다.

단 선언은 반드시 @StateObject 를 사용하며
외부에서 쓰지 못하도록 private 를 쓸 것을 권장하고 있다.

이를 무시하면 내부적으로 SwiftUI 프레임워크의 메모리 관리에 충돌을 일으키고 예상치 못한 결과를 야기한다고 한다.

전달된 EnvironmentObject 는 다음과 같이 사용할 수 있다.

참고

Auth.auth().currentUser?.uid == nil 은 비즈니스 로직이니, viewModel 안에 있어야 하는게 맞고 그에 따라 전달받은 viewModel 내부에서 동기화된 데이터를 처리하는 것이 여태까지의 흐름으로는 맞다.

그럼에도 불구하고 Auth.auth().currentUser?.uid 를 직접 사용한 이유는 해당 속성은 Firebase 라는 라이브러리에서 직접 온 속성이자, 싱글턴 속성이기에 다이렉트로 작성하는 것이 가독성면에서 더 좋다고 판단하여 밖으로 꺼내었다.

디자인 패턴은 결국 특정 문제를 해결하고자 하는 것이니, 크게 어긋나지 않는 선에서 적절하게 맞추어도 괜찮다고 생각한다.
사실 혼자 하는 프로젝트라서 마음대로 한 것도 없지 않아 있긴 있다. 근데 파이어베이스는 저렇게 빼놓는게 더 직관적이지 않나? ..


정리하자면 UI 에서 적절한 명령@ObservedObject@EnvironmentObject 로, 인스턴스 여부에 따라 취사선택하여 비즈니스 로직을 처리한 데이터UI 에 동기화 할 수 있다.


이제 4번 내용을 viewModel 과 함께 살펴보자.
UI적절한 명령@ObservedObject 를 살펴볼 때, 이 입력값은 관측 가능한 오브젝트이어야 하며, 이는 viewModel 이 내부에서 처리를 해주어야 한다.

이 내용을 실제 코드와 함께 살펴보자.

UserViewModel 이라는 classObservableObject 프로토콜을 따르면서, viewModel 이라는 이름의 class관측 가능한 오브젝트가 될 수 있도록 해주고 있다.

관측 가능한 오브젝트 가 된 viewModel 은 비즈니스 로직을 잔뜩 담고 있다. 크게 @Published 변수 와 이를 변경하는 함수들로 구분할 수 있다.
login(), signUp() 등의 유저와 관련된 함수들은 내부에서 각각 자신이 담당하는 @Published 변수 를 변경하는 내용을 담고 있다.

함수 내용은 공개 불가능합니다.

놀랍게도 4번의 내용은 이렇게 끝이다. 그저 클래스가 ObservableObject 프로토콜을 따르며 (이 프로토콜은 바라는 내용도 없다!), 입력값에 따라 view 가 반응하기를 원하는 변수 앞에 @Published 를 붙임으로써 UI비즈니스 로직을 처리한 데이터 를 라우팅하고 있다.

느슨한 결합 은 어떨까?
지금의 MVVM 구조를 통해 비즈니스 로직 내부에서도 서로 연관된 내용이 아니면 풀려있으며, 비즈니스 로직도 개발자가 원하는 크기와 단위, 개념으로 나눌 수 있게 된다. 이미 현재 적용한 MVVM 구조에서 살펴보았듯 User 와 Wassup 의 비즈니스 로직이 분리되어 있고, Wassup 내부에서 사용되는 Poster 와 Video 도 필요에 따라 분리되어 있는 것을 볼 수 있다.

viewModel 에게 ObervableObject@Published 라는 적절한 명령 을 통해 UI 와 비즈니스 로직을 연결한 것이다.


그러면 현재 구조에서 사용되었던 Model 은 언제 쓰일까?

class UserModel: ObservableObject {
	@Published var user: UserModel = UserModel()
	...
}

struct UserModel {
   .....
}

바로 유저 와 같이 큰 개념을 담아야 하는 변수의 커스텀한 자료형을 만들 때 사용된다. User 에는 프로필 사진, 이름 등 다양한 정보가 들어가야 하는데 이를 일일히 @Pubslih 를 사용하기보다 필요한 데이터만 모아서 깔끔하게 정리하고, 이후 초기화가 필요할 때도 Model 만 불러서 초기화할 수 있게 되었다.


무엇을 해결할 수 있었는가? (주관적 내용)

위에서 MVVM 의 설명을 이렇게 설명하였다.

MVVM(Model-View-ViewModel)은 UI 및 비 UI 코드(이하 비즈니스 로직) 를 분리하기 위한 UI 아키텍처 디자인 패턴입니다.

MVVM을 사용하여
1) UI를 선언적으로 정의하여 UI 와 관련된 내용만 깔끔하게 정리할 수 있으며,
2) 비즈니스 로직만 모아놓은 파일을 따로 정리할 수 있으며,
3) UI 와 비즈니스 로직을 거친 데이터를 동기화 상태를 유지할 수 있고,
4) 적절한 명령으로 UI 와 비즈니스 로직을 라우팅하여, 느슨할 결합을 제공할 수 있습니다.

*이 내용은 임의로 변형 & 요약한 내용입니다. 원본은 링크에서 확인해주세요.

이제 이걸 다시 정리해보자면
MVVM 디자인 패턴은 결국 코드를 어떻게 정리하면 이해하기 쉬울까? 에 대한 답변 중 하나라고 말할 수 있다.

하나의 파일에 UI비즈니스 로직 을 몽땅 함께 담아버리면
나와 신만이 아는 코드에서 신만이 아는 코드가 될 수도 있고,
그렇다고 어설프게 나누면 뭐가 무엇이였는지 까먹어서 마찬가지로 신만이 아는 코드가 될 수도 있다.

MVVM 디자인 패턴은 코드를 UI비즈니스 로직 으로 크게 나누고,
이 두개를 적절한 명령 을 통해 동기화느슨한 결합 이라는 이점을 제공해주고 있다.

기나긴 글을 지나 MVVM 구조에 대해서 이야기를 나누어보았다.
이제는 실제 사례에서 어떤 점이 편하게 작용할 수 있었는지만 간편하게 살펴보자 :)


첫번째. UI 와 비즈니스 로직 코드가 분리되었기에, 테스트 하기 쉬워졌다.
UI 를 먼저 작업하여 원하는 만큼 Preview 로 확인하고, 이후 비즈니스 로직을 넣어볼 수 있게 되어 개발할 때 API 호출 횟수 등에 신경쓰지 않고 미리 다 확인해 볼 수 있게 된 것이다. 이 이점은 다른 개발자와 함께 할 때 더욱 크게 다가오는데, 연결할 API 가 준비 안되어 있으면 다른 UI 작업을 하면 되고, UI 를 분업할 때도 어느 파트를 맡을 지 더욱 직관적으로 정리할 수 있게 된다.

두번째. MVVM 구조에 SwiftUI적절한 명령 을 섞으니 마음이 편안한 선언형 프로그래밍이 가능해졌다. 이름, 개념 단위로 파일명을 작성하니 구조만 보더라도 무엇을 담당하는 파일인지 쉽게 알 수 있게되고, 무엇보다 재활용성이 높아졌다.
반복적으로 나오는 UI 혹은 두번 이상 쓰이는 비즈니스 로직들을 호출만으로 처리할 수 있게 되니 그 자체만으로 생산성 자체가 높아졌다.

세번째. 더욱 복잡한 구조를 설계할 수 있게 되었다.

이처럼 복잡한 화면을 처리해야할 때,
비즈니스 로직을 담당하는 viewModel 자체가 느슨한 결합 상태이니 핸들링이 쉬워졌다. 무엇보다도 위의 화면처럼 여러 리스트가 있을 때, 리스트 아이템에게도 viewModel 을 만들어줌으로써 코드들을 분리하여 관리할 수 있었고, 또 손 쉽게 리스트에게 데이터를 전달해줄 수 있었다.

응용하면 이런 내용도 해결할 수 있게 되었다.

해당 내용은 리스트를 생성하고 삭제해야하는 상황으로,
1. 리스트 아이템의 내용이 바로바로 반영될 것.
2. 삭제가 가능해야할 것.

이라는 문제를 해결해야 했던 상황이였다.

문제는 리스트를 위한 viewModel, 리스트 아이템을 위한 viewModel 로 구분하다보니 리스트를 삭제하기 꽤나 번거로운 상황이였다.

UI 코드 상으로는 리스트의 아이템을 표시할 때 아이템의 viewModel 만 사용하게 되는데, 막상 삭제할때는 UI 는 아이템 뷰에 있고, 메소드는 부모가 필요한 상황이라는 아이러니한 조건이었다.
이때 아이템의 viewModel 를 초기화할 때, 리스트 그 자체의 접근을 위한 viewModelparent 로 함께 보내주어 직관적으로 아이템이 부모를 호출하여 스스로를 삭제할 수 있게 된 것이다.

결론

MVVM 구조를 SwiftUI 와 함께 알아보았다.
MVVMView(UI) - ViewModel(비즈니스 로직) - Model 의 축약어일 뿐이므로, 지금의 MVVM 구조가 진리도 아니고 개발자마다, 문제마다 다른 MVVM 구조를 가질 수 있다.

적절한 방법 이라는 워딩을 사용하였지만, 이는 마이크로소프트가 제시할 때의 이야기이고, 애플과 SwiftUI 가 직접 MVVM 을 지원한다고 명시한 것은 아니다.

MVVM 이라는 단어는 애플 공식문서에서 공식적으로 언급이 된 적이 없다고 한다.
SwiftUI에서 MVVM 사용을 멈추자"라고 생각이 들었던 이유

그럼에도 불구하고, 이번 구조는 MVVM 이 이루고자 했던 가장 큰 틀
: UI 와 비즈니스 로직을 구분하는 아키텍처 패턴
를 따랐다고 생각한다.


선언형 UI 를 사용하는 개발자가 늘어나면서 MVVM 이 꼭 필요한가? 에 대해서도 많은 논의가 있는 것으로 알고 있다.

개인적으로도 안드로이드의 Jetpack compose 를 사용할때는 워낙 자유롭기도 하고, by remember { }, LaunchedEffect 등의 코드를 UI 단에서 사용해야 하다보니 이렇게까지 비즈니스 로직을 UI 에서 섞을거라면 MVVM 을 왜 써야하지? 라는 의문도 있었다.

하지만 SwiftUI 를 기준으로는

  • UI 내부에서 비즈니스 로직을 처리하기 까다롭다는 점
    *버튼이나 onAppear{} 등을 사용하지 않으면 body 내에서는 모든 코드가 View 프로토콜을 따라야 한다.
  • Combine 을 비롯한 다수의 비즈니스 로직을 viewModel 이라는 레이어를 만들면 한 곳에서 직관적으로 풀 수 있다는 점
  • PropertyWrapper 를 활용해서 Model 과 View 가 직접적으로 연결될 수 있다고 하더라도, MVC 패턴처럼 모델이 직접 상태를 관리할 것이 아니라면, 결국 누군가는 상태를 관리하고 관련된 메소드를 품고 있어야 한다는 점(Flux 패턴 등)

와 같은 문제를 속 시원하게 까지는 아니지만, 해결한다는 점에서 여전히 괜찮은 옵션 중 하나라고 생각이 든다.

출처

What's a design pattern? from Refactoring Guru
데이터 바인딩 및 MVVM from 마이크로소프트
@StateObject, @ObservedObject, Introducing Combine 등의 공식문서
from Apple

profile
나만 고양이 없어

1개의 댓글

comment-user-thumbnail
2023년 4월 11일

잘 읽었습니다.

답글 달기