Front-end/Flutter

Flutter MVVM 패턴 적용기

DEBTOLEE 2025. 3. 29. 01:19

스파게티 언제까지 만들 거야

나는 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들을 개발할 때 코드의 일관성을 유지하고 통일성 있게 관리하는 것이 중요하다.

좋은 구조는 항상 명확한 책임에서 나온다.

오늘도 고생하셨습니다.