많이 들어본 MVVM(Model-View-ViewModel) 패턴은, 단순히 말해서 하나의 파일에 모든 코드를 때려박지 않고 ui와 로직을 분리하는 방법이다.
적절하게 분리한 단위가 모델, 뷰, 뷰모델 세 가지라 MVVM 패턴이라고 부른당
관심사의 분리
프로그램을 서로 다른 영역으로 나누어 각 영역이 서로 다른 관심사를 다루도록 설계함으로써, 프로그램의 복잡성을 줄일 수 있습니다.
테스트 용이성
뷰와 뷰모델 사이의 인터페이스를 통해 뷰모델의 테스트가 용이해집니다.
구조화된 프로젝트 구조
MVVM 패턴은 뷰, 뷰모델 및 모델로 구성되며 각각의 구성 요소가 서로 분리되어 개발이 가능합니다.
UI의 병렬 개발
MVVM 패턴을 사용하면 뷰와 뷰모델을 분리함으로써 UI의 개발과 동시에 뷰모델을 병렬적으로 개발할 수 있습니다.
뷰의 추상화
뷰를 추상화한다는 말은 뷰에서 직접적으로 데이터를 처리하지 않고, 뷰모델을 통해 데이터를 주고받는 것을 말합니다. 이렇게 하면 뷰는 단순히 UI만을 구성하는 역할을 하게 되어 UI와 비즈니스 로직이 서로 분리되어 유지보수성이 향상되며, 뷰모델은 UI와 비즈니스 로직 사이에서 중간 매개체 역할을 하여 UI와 비즈니스 로직 사이의 결합도를 낮춥니다. 추상화를 통해 뷰와 뷰모델을 독립적으로 개발할 수 있으며, 뷰의 변경이 뷰모델에 영향을 주지 않아 코드의 안정성과 확장성이 높아집니다.
→ 요약하자면, ui와 로직을 분리하고 뷰모델을 테스트 가능하도록 만드는 것. 이를 통해 ui를 변경해도 로직에 영향이 가지 않게 한다!
Model
애플리케이션의 데이터 및 비즈니스 로직을 나타냅니다.
데이터 가져오기 및 조작을 담당하고 ViewModel과 통신하여 UI에 데이터를 제공합니다.
모델은 비즈니스 로직과 데이터 소스를 다루는 코드가 포함됩니다.
데이터 소스는 원격 데이터베이스, 로컬 데이터베이스 또는 RESTful API와 같은 것이 될 수 있습니다.
데이터 소스에 액세스하는 것은 데이터 레포지토리(Data Repository)를 통해 이루어집니다.
ViewModel
Model과 View 사이의 중개자 역할을 합니다.
Model에서 데이터를 가져오고 View에 표시할 준비를 담당합니다. 또한 View에서 이벤트를 수신하고 그에 따라 Model을 업데이트합니다.
뷰모델은 뷰(View)와 모델(Model) 간의 중개자 역할을 합니다.
뷰(View)에 표시되는 데이터를 관리합니다.
뷰(View)에서 발생한 사용자 입력을 처리합니다.
모델(Model)과 데이터 레포지토리(Data Repository)에 액세스합니다.
View
애플리케이션의 사용자 인터페이스를 나타냅니다.
UI 요소를 렌더링하고 사용자 이벤트에 응답하는 역할을 합니다. ViewModel과 통신하여 표시해야 하는 데이터를 가져옵니다.
티비로 비유하자면, 스크린은 사용자에게 직접 보여지는 View이고, 리모컨은 ViewModel, 채널 변경, 음량 조절 등의 기능은 Model에 속한다.
MVVM 패턴에서는 View와 Model 간의 직접적인 의존성이 없으며, 대신 ViewModel을 통해 상호 작용을 해야한다. 시청자들이 채널을 바꾸려고 티비를 뜯어야하는 일은 없어야한다는 소리 헤헤
그렇다면 패턴을 적용하여 직접 구현해보자.
각종 위젯과 스크린은 뷰의 역할을, 상태관리를 쓴다면 GetX에서는 Controller, Riverpod에서는 stateNotifier, MobX에서는 store가 뷰모델의 역할을 담당한다고 생각하면 될것같다.
유저 정보를 다루는 기능과 화면이 있다고 가정해보자. 상태관리는 Mobx를 사용한다.
우선은 유저에 대한 클래스가 있어야 한다.
class User {
String name;
int age;
User({required this.name, required this.age});
User copyWith({String? name, int? age}) {
return User(
name: name ?? this.name,
age: age ?? this.age,
);
}
}
뷰는 아래와 같이 생성한다. 이름과 나이를 수정하면, 수정된 값이 즉시 반영된다.
class UserInfoPage extends StatelessWidget {
const UserInfoPage({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
final userViewModel = UserViewModel();
return Scaffold(
body: Observer(
builder: (context) => Column(
children: [
Text('Name: ${userViewModel.user.name}'),
TextFormField(
initialValue: userViewModel.user.name,
onChanged: (value) => userViewModel.changeUserName(value)),
Text('Age: ${userViewModel.user.age}'),
TextFormField(
initialValue: userViewModel.user.age.toString(),
onChanged: (value) =>
userViewModel.changeUserAge(int.parse(value))),
],
),
),
);
}
}
이렇게 되면 유저는 뷰모델을 거치지 않고서는 이름과 나이를 수정할 수 없다.
class UserViewModel = _UserViewModel with _$UserViewModel;
abstract class _UserViewModel with Store {
@observable
User user = User(name: 'John', age: 30);
@action
void changeUserName(String name) {
user = user.copyWith(name : name);
}
@action
void changeUserAge(int age) {
user = user.copyWith(age : age);
}
}
실제 프로젝트에서는 뷰모델이 이렇게 단순하지 않고, Repository를 참조한다.
Repository와 DataSource은 로직과 기능을 관리하는 모델단에 속함. 이것에 대해서는 다음 편에서 더 자세히 다룰 예정이다.
참고
https://ctoahn.tistory.com/10
https://blog.devgenius.io/flutter-mvvm-architecture-with-provider-a81164ef6da6
https://betterprogramming.pub/how-to-use-mvvm-in-flutter-4b28b63da2ca
https://medium.com/flutterworld/flutter-mvvm-architecture-f8bed2521958
https://unsungit.tistory.com/203