스파게티 언제까지 만들 거야
백엔드 개발자였지만, 회사 업무를 하면서 프론트엔드까지 도맡게 되는 일이 잦아졌다. 처음에는 간단한 앱으로 시작했지만, 프로젝트가 성장할수록 코드가 뒤엉켜버리는 이른바 ‘스파게티 코드’의 늪에 빠지고 말았다. 앱의 규모가 커질수록 유지보수는 점점 더 어려워졌고, 끝없는 버그 수정과 코드 수정으로 업무 효율성은 바닥을 쳤다. 결국, 이 고통스러운 현실에서 벗어나기 위해 디자인 패턴에 관심을 가지게 되었고, 특히 Flutter 개발자들이 가장 선호하는 MVVM 패턴에 주목하게 되었다.
MVVM 패턴이란 무엇인가?
MVVM은 다음 MVVM 패턴이란 무엇인가?
MVVM(Model-View-ViewModel) 패턴은 다음의 세 가지 주요 구성 요소로 이루어진 매우 강력한 디자인 패턴이다.
- Model: 앱의 핵심 데이터를 정의하고 관리하며, 앱 전반에서 사용되는 데이터의 일관성을 책임진다.
- View: 사용자에게 매력적인 UI를 제공하며, 사용자의 입력을 받아 명확하고 직관적인 인터페이스를 구성한다.
- ViewModel: View와 Model 사이에서 중재자로 작동하여, UI와 데이터 사이의 모든 로직과 데이터 흐름을 효과적으로 관리한다.
이 패턴의 가장 큰 장점은 MVC나 MVP와 다르게 UI(View)에서 모든 비즈니스 로직을 완벽하게 분리한다는 점이다. 덕분에 코드의 가독성과 유지보수성이 극대화되며, 테스트 환경 구축 또한 매우 용이해진다.
실제로 인터넷 쇼핑몰을 예로 들어보자. 사용자가 상품을 장바구니에 추가할 때, UI(View)는 이 요청을 ViewModel에 전달한다. ViewModel은 상품 재고를 확인하고, 가격을 검증한 후에 최종적으로 Model에 데이터를 반영한다. 이 흐름 덕분에 사용자는 단순히 UI만을 통해 편리하게 상호작용하고, 내부적으로는 ViewModel이 복잡한 로직을 처리하여 UI와 데이터를 명확하게 분리할 수 있게 된다.

MVVM 패턴의 강력한 장점과 주의할 단점
MVVM 패턴은 많은 강점을 가지고 있지만, 동시에 주의할 점도 존재한다.
장점 | 단점 |
---|---|
유지보수가 뛰어난 명확한 구조 | 소규모 앱에서는 불필요한 복잡성 초래 가능성 |
테스트가 용이 | ViewModel이 복잡해질 수 있음 |
ViewModel의 재사용성 향상 | 설계가 잘못되면 ViewModel이 너무 비대해질 수 있음 |
Flutter에서 상태 관리 방법을 선택하는 전략
Flutter에서는Flutter에서 상태 관리는 앱의 성능과 사용자 경험을 결정짓는 매우 중요한 부분이다. 대표적으로 다음과 같은 뛰어난 상태 관리 방법들이 있다.
- Provider: 직관적이고 간단하며, 접근성이 뛰어나 초보자부터 전문가까지 폭넓게 사용된다.
- Riverpod: Provider의 장점을 계승하고 단점을 보완한, 보다 강력하고 안전한 최신 기술이다.
- Bloc: 상태의 변화를 명확히 정의하고 추적할 수 있게 하여, 복잡한 로직을 가진 앱에 적합하다.
- GetX: 빠른 개발 속도와 쉬운 문법으로 최근 급격히 인기가 상승하고 있다.
이번 예제에서는 접근성이 높고, Flutter 커뮤니티에서 강력히 지지하는 Provider를 사용하여 MVVM을 보다 명확하게 설명할 것이다.
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 패턴 적용 Tip
MVVM 패턴을 효과적으로 적용하기 위해서는 몇 가지 중요한 원칙을 반드시 지켜야 한다.
- 책임 분리의 명확화: View는 UI 표현만, ViewModel은 비즈니스 로직만 다루어야 한다.
- 상태 변경의 즉각적 반영: ViewModel은 상태 변화가 발생할 때마다 즉시
notifyListeners()
를 호출하여 UI를 최신 상태로 유지해야 한다. - API 및 데이터 로직의 분리: View에서는 절대 API 호출이나 데이터 처리 로직을 수행하지 않도록 주의하자. ViewModel에서 데이터와 로직을 철저히 관리한다.
- 확장성 및 미래를 위한 설계: 프로젝트 초반부터 코드가 성장할 가능성을 염두에 두고, 확장 가능한 구조로 미리 설계하여 장기적인 유지보수와 개발 효율성을 확보한다.
좋은 구조는 명확한 책임 분리에서 시작된다
오늘도 고생하셨습니다.