스파게티 언제까지 만들 거야
나는 Back-end 개발자지만 회사 내 업무들을 보다 보면 다방면의 개발 분야를 접하게 된다. 그중 많은 부분을 차지하는 Front-end 부분을 자주 개발하게 되는데, 회사에서 Front-end로 사용하는 Flutter를 통해 개발을 진행하다 보니 내가 작성한 코드임에도 규모가 커지고 복잡해지면서 다시금 디자인 패턴에 대해 생각하게 되었다. 이에 Flutter에서 많이 사용하는 MVVM 패턴을 적용해 보기로 했다.
Flutter 앱 개발에서 프로젝트 규모가 커질수록 코드가 복잡해지고 유지보수가 어려워진다. 이런 문제를 해결하기 위해 많은 개발자가 MVVM(Model-View-ViewModel) 패턴을 사용한다. 이번 글에서는 Flutter 프로젝트에서 MVVM 패턴을 쉽게 이해하고 적용할 수 있도록 간단한 예제를 통해 설명한다.

MVVM이란 무엇인가?
MVVM은 세 가지 주요 컴포넌트인 Model, View, ViewModel로 이루어진 아키텍처 패턴이다.
- Model: 데이터 구조를 정의하고 데이터를 관리한다.
- View: 사용자 인터페이스(UI)를 나타내는 컴포넌트로, 사용자와 직접 상호작용한다.
- ViewModel: View와 Model 사이에서 데이터 흐름을 관리하고, 비즈니스 로직을 처리한다.
MVC, MVP와의 주요 차이점은 View와 비즈니스 로직이 완전히 분리되어 있다는 점이다.
실생활로 예를 들면, 인터넷 쇼핑몰을 생각해 볼 수 있다. 사용자는 쇼핑몰 웹사이트(View)에서 상품을 보고 장바구니에 담는다. 웹사이트는 직접 상품 데이터를 관리하지 않고, 사용자가 장바구니에 상품을 담는 요청을 ViewModel에 전달한다. ViewModel은 상품 재고, 가격 등의 데이터를 확인한 후 장바구니에 추가하고, 데이터의 변경을 Model에 반영한다. 이처럼 사용자는 웹사이트의 UI만을 통해 간단히 상호작용하고, ViewModel이 데이터 흐름을 중간에서 관리하는 것이다.
MVVM 패턴의 장단점
장점 | 단점 |
---|---|
코드 유지보수 용이 | 작은 프로젝트에서는 과도한 설계가 될 수 있음 |
테스트 용이성 향상 | ViewModel의 복잡도가 높아질 가능성 |
재사용 가능한 ViewModel 코드 작성 가능 |
상태 관리 방법의 선택
Flutter에서는 다양한 상태 관리 방법이 존재한다. 자주 사용되는 상태 관리 방법은 다음과 같다.
- Provider: 가장 널리 사용되며 쉽고 직관적이다.
- Riverpod: Provider의 단점을 보완하고 더욱 강력한 기능을 제공하는 최신 라이브러리이다.
- Bloc: 명확한 상태 변화 흐름을 제공하여 복잡한 상태 관리에 적합하다.
- GetX: 빠른 개발 속도와 간단한 사용법으로 인기를 끌고 있다.
이 글에서는 쉽게 접근할 수 있고 커뮤니티의 지지가 높은 Provider를 선택하여 예제를 작성하였다.
MVVM 구조로 다음과 같이 진행하였다.
lib/
├── models/
├── view_models/
├── views/
├── services/
└── utils/
TODO List 앱 MVVM 예시
1. Model 정의
Model은 데이터 자체를 의미한다. 여기서는 Task라는 간단한 할 일 항목을 정의한다.
class TaskModel {
String title;
bool isDone;
TaskModel({required this.title, this.isDone = false});
}
2. ViewModel 정의
ViewModel은 UI에 보여줄 데이터를 관리하고, 상태 변경 로직을 담당한다.
import 'package:flutter/material.dart';
import 'task_model.dart';
class TaskViewModel extends ChangeNotifier {
final List<TaskModel> _tasks = [];
List<TaskModel> get tasks => _tasks;
void addTask(String title) {
_tasks.add(TaskModel(title: title));
notifyListeners();
}
void toggleTaskStatus(int index) {
_tasks[index].isDone = !_tasks[index].isDone;
notifyListeners();
}
}
3. View 정의
Flutter에서 Provider 패키지를 사용하여 ViewModel과 View를 연결한다.
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'task_view_model.dart';
class TaskView extends StatelessWidget {
@override
Widget build(BuildContext context) {
return ChangeNotifierProvider(
create: (_) => TaskViewModel(),
child: Scaffold(
appBar: AppBar(title: Text('MVVM ToDo List')),
body: Consumer<TaskViewModel>(
builder: (context, viewModel, child) => ListView.builder(
itemCount: viewModel.tasks.length,
itemBuilder: (context, index) => ListTile(
title: Text(viewModel.tasks[index].title),
trailing: Checkbox(
value: viewModel.tasks[index].isDone,
onChanged: (_) => viewModel.toggleTaskStatus(index),
),
),
),
),
floatingActionButton: FloatingActionButton(
child: Icon(Icons.add),
onPressed: () => context.read<TaskViewModel>().addTask('새로운 할 일'),
),
),
);
}
}
MVVM 패턴 적용 시 주의할 점
- 명확한 책임 분리: View는 UI만 담당하고, ViewModel은 비즈니스 로직만 처리하도록 철저히 분리한다.
- 상태 변경 관리: ViewModel의 상태 변경은 반드시 notifyListeners()로 관리하여 UI에 즉시 반영되도록 한다.
- 흔한 실수 방지: 특히 View에서 API 호출이나 비즈니스 로직을 처리하는 코드를 작성하지 않도록 한다. 비슷하거나 중복되는 View들을 개발할 때 코드의 일관성을 유지하고 통일성 있게 관리하는 것이 중요하다.
좋은 구조는 항상 명확한 책임에서 나온다.
오늘도 고생하셨습니다.