Flutter에서 MVVM(Model-View-ViewModel) 패턴을 사용하면 코드의 유지보수성과 테스트 용이성이 크게 향상됩니다. 이번 글에서는 MVVM 개념부터 실제 구현까지 단계별로, 이해를 돕기 위해 Todo 리스트 예제를 중심으로 설명드리겠습니다.
앱 아키텍처
앱 아키텍처는 애플리케이션의 구조와 구성 요소 간의 상호작용을 정의하는 설계 방법론입니다. 쉽게 말해, 코드의 역할을 명확히 나누고 정리된 방식으로 구현하는 것입니다.
앱 아키텍처의 핵심 목표
- 역할 분리: 각 컴포넌트가 자신의 역할에만 집중하도록 설계
- 유지보수 용이성: 코드 수정이나 기능 추가가 간편
- 확장성: 프로젝트가 커져도 쉽게 관리 가능
MVC vs MVVM 비교
항목 | MVC | MVVM |
구조 | Model, View, Controller | Model, View, ViewModel |
UI와 로직의 결합도 | View와 Controller 간 강한 결합 | View와 ViewModel 간 느슨한 결합 |
테스트 용이성 | 로직 테스트가 비교적 어려움 | ViewModel 단독 테스트 가능 |
UI 상태 관리 | View에서 직접 관리 | ViewModel이 UI 상태 관리 |
적용 복잡성 | 간단한 구조 | 상대적으로 복잡 |
MVVM 패턴
구성 요소
- Model
- 데이터와 관련된 모든 로직 처리
- 데이터베이스 또는 API와 상호작용
- View
- 사용자 인터페이스(UI)
- 데이터를 표시하고 사용자 입력을 ViewModel로 전달
- ViewModel
- Model과 View 사이의 중개자 역할
- UI 상태 관리 및 비즈니스 로직 처리
MVVM으로 TodoList 앱 만들기 📝
프로젝트 구조
lib/
├── models/ # 데이터 모델 정의
│ └── todo_item.dart
├── view_models/ # ViewModel 정의
│ └── todo_list_view_model.dart
├── views/ # UI 정의
│ └── todo_list_view.dart
└── main.dart # 앱 진입점
1단계: Model 생성
Todo 데이터를 정의하는 모델을 작성합니다.
// models/todo_item.dart
class TodoItem {
String title; // Todo 항목의 제목
bool isDone; // 완료 여부
TodoItem({required this.title, this.isDone = false});
}
2단계: ViewModel 생성
ViewModel은 데이터 로직과 UI 상태를 관리합니다.
// view_models/todo_list_view_model.dart
import '../models/todo_item.dart';
class TodoListViewModel {
final List<TodoItem> _items = []; // Todo 리스트 상태
// Todo 리스트 상태 접근자
List<TodoItem> get items => _items;
// Todo 항목 추가
void addItem(String title) {
_items.add(TodoItem(title: title));
}
// Todo 항목 완료 상태 토글
void toggleItem(TodoItem todo) {
todo.isDone = !todo.isDone;
}
}
3단계: View 생성
UI를 정의하고 ViewModel과 연결합니다.
// views/todo_list_view.dart
import 'package:flutter/material.dart';
import '../models/todo_item.dart';
import '../view_models/todo_list_view_model.dart';
class TodoListView extends StatefulWidget {
const TodoListView({Key? key}) : super(key: key);
@override
State<TodoListView> createState() => _TodoListViewState();
}
class _TodoListViewState extends State<TodoListView> {
final TodoListViewModel _viewModel = TodoListViewModel();
final TextEditingController _controller = TextEditingController();
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('MVVM TodoList')),
body: Column(
children: [
Padding(
padding: const EdgeInsets.all(16.0),
child: TextField(
controller: _controller,
decoration: InputDecoration(
hintText: 'Enter Todo...',
suffixIcon: IconButton(
icon: Icon(Icons.add),
onPressed: () {
setState(() {
_viewModel.addItem(_controller.text); // Todo 추가
_controller.clear();
});
},
),
),
),
),
Expanded(
child: ListView.builder(
itemCount: _viewModel.items.length,
itemBuilder: (context, index) {
final item = _viewModel.items[index];
return ListTile(
title: Text(item.title),
trailing: Checkbox(
value: item.isDone,
onChanged: (value) {
setState(() {
_viewModel.toggleItem(item); // 완료 상태 토글
});
},
),
);
},
),
),
],
),
);
}
}
4단계: 앱 시작점 작성
// main.dart
import 'package:flutter/material.dart';
import 'views/todo_list_view.dart';
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
home: TodoListView(),
);
}
}
MVVM 장단점 요약
장점 | 단점 |
UI와 비즈니스 로직의 철저한 분리 | 초기 학습 곡선이 다소 높음 |
코드 재사용성 및 유지보수성 향상 | 간단한 앱에는 구조가 과도할 수 있음 |
ViewModel 단위 테스트가 용이 | 상대적으로 코드량이 증가 |
MVVM 패턴을 활용하면 Flutter 앱에서 더 나은 구조와 확장성을 얻을 수 있습니다. 이번 예제에서 살펴본 것처럼 UI(View)와 로직(ViewModel)을 분리하면 코드 관리가 쉬워지고 테스트도 용이해집니다.