주니어 개발자로, 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(Model-View-ViewModel)은 UI 및 비 UI 코드(이하 비즈니스 로직) 를 분리하기 위한 UI 아키텍처 디자인 패턴입니다.
MVVM을 사용하여
1) UI를 선언적으로 정의하여 UI 와 관련된 내용만 깔끔하게 정리할 수 있으며,
2) 비즈니스 로직만 모아놓은 파일을 따로 정리할 수 있으며,
3) UI 와 비즈니스 로직을 거친 데이터를 동기화 상태를 유지할 수 있고,
4) 적절한 명령으로 UI 와 비즈니스 로직을 라우팅하여, 느슨할 결합을 제공할 수 있습니다.*이 내용은 임의로 변형 & 요약한 내용입니다. 원본은 링크에서 확인해주세요.
1번과 2번 항목은 직관적이다.
UI 코드들만 모아서 정리하고 싶고, 비즈니스 로직만 모아서 정리하고 싶다.
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
, 관측 가능한 오브젝트, 이며 view
가 ObservableObject
의 속성이 변경될 때 마다 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 을 전달하고 있다.
@StateObject
는 ObservableObject
를 선언할 때 사용하며,
@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
이라는 class
는 ObservableObject
프로토콜을 따르면서, 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
를 초기화할 때, 리스트 그 자체의 접근을 위한 viewModel
을 parent
로 함께 보내주어 직관적으로 아이템이 부모를 호출하여 스스로를 삭제할 수 있게 된 것이다.
MVVM
구조를 SwiftUI
와 함께 알아보았다.
MVVM
이 View(UI)
- ViewModel(비즈니스 로직)
- Model
의 축약어일 뿐이므로, 지금의 MVVM 구조가 진리도 아니고 개발자마다, 문제마다 다른 MVVM
구조를 가질 수 있다.
또적절한 방법
이라는 워딩을 사용하였지만, 이는 마이크로소프트가 제시할 때의 이야기이고, 애플과 SwiftUI 가 직접 MVVM 을 지원한다고 명시한 것은 아니다.
MVVM
이라는 단어는 애플 공식문서에서 공식적으로 언급이 된 적이 없다고 한다.
SwiftUI에서 MVVM 사용을 멈추자"라고 생각이 들었던 이유
그럼에도 불구하고, 이번 구조는 MVVM
이 이루고자 했던 가장 큰 틀
: UI 와 비즈니스 로직을 구분하는 아키텍처 패턴
를 따랐다고 생각한다.
선언형 UI 를 사용하는 개발자가 늘어나면서 MVVM
이 꼭 필요한가? 에 대해서도 많은 논의가 있는 것으로 알고 있다.
개인적으로도 안드로이드의 Jetpack compose
를 사용할때는 워낙 자유롭기도 하고, by remember { }
, LaunchedEffect
등의 코드를 UI 단에서 사용해야 하다보니 이렇게까지 비즈니스 로직을 UI 에서 섞을거라면 MVVM 을 왜 써야하지? 라는 의문도 있었다.
하지만 SwiftUI 를 기준으로는
MVC
패턴처럼 모델이 직접 상태를 관리할 것이 아니라면, 결국 누군가는 상태를 관리하고 관련된 메소드를 품고 있어야 한다는 점(Flux 패턴 등)와 같은 문제를 속 시원하게 까지는 아니지만, 해결한다는 점에서 여전히 괜찮은 옵션 중 하나라고 생각이 든다.
What's a design pattern? from Refactoring Guru
데이터 바인딩 및 MVVM from 마이크로소프트
@StateObject, @ObservedObject, Introducing Combine 등의 공식문서
from Apple
잘 읽었습니다.