본문 바로가기

Flutter

MVVM 패턴과 상태 관리

Monolithic (모노리스) 구조의 특징

하나의 코드 파일에 UI, 비즈니스 로직, 프레젠테이션 로직을 모두 넣는 형식을 흔히 Monolithic Architecture 또는 간단히 Monolith라고 부른다.

 

 

먼저 MVVM 패턴 없이 간단한 구조로 코드를 작성해보자. 모든 로직과 상태 관리를 하나의 파일에 통합하여, UI와 데이터 처리가 한 클래스에서 이루어지는 방식으로 코드를 작성할 수 있다. 이 방식은 MVVM과 같은 디자인 패턴이 없어도 간단한 앱에서는 빠르게 개발할 수 있는 장점이 있다.

 

 

 

 

TODO
import 'package:flutter/material.dart';

void main() => runApp(TodoApp());

class TodoApp extends StatelessWidget {
  const TodoApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: TodoScreen(),
    );
  }
}

class TodoScreen extends StatefulWidget {
  const TodoScreen({super.key});

  @override
  State<TodoScreen> createState() => _TodoScreenState();
}

class _TodoScreenState extends State<TodoScreen> {
  // Todo기능을 개발 하기 위해 필요한 데이터는 뭘까?
  // UI 로직 : 프로젠테이션 로직
  final TextEditingController _controller = TextEditingController();

  // 데이터 : 할 일을 (목록을) 저장하는 리스트(저장 공간)
  List<String> todos = [];

  // 비즈니스 로직 : 할 일을 추가하는 기능
  void addTodo() {
    if(_controller.text.isNotEmpty) {
      // 프로젠테이션 로직
      setState(() {
        todos.add(_controller.text);
        print("todos 확인 : ${todos.toString()}");
      });
      _controller.clear();  // 프로젠테이션 로직
    }
  }

  // 비즈니스 로직 : 할 일을 삭제하는 기능
  void removeTodo(int index) {
    // 삭제 처리
    setState(() {
      todos.removeAt(index);
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Simple Todo List'),
      ),
      body: Column(
        children: [
          // 입력 필드 만들기
          Padding(
            padding: const EdgeInsets.all(16.0),
            child: Row(
              children: [
                Expanded(
                  child: TextField(
                    controller: _controller,
                    decoration: InputDecoration(labelText: '작업을 입력 하시오'),
                  ),
                ),
                IconButton(
                  onPressed: addTodo,
                  icon: Icon(Icons.add),
                )
              ],
            ),
          ), 
          // 아래에 할일 목록 표시 구성 
          Expanded(
            child: ListView.builder(
                itemCount: todos.length,
                itemBuilder: (context, index) {
              return ListTile(
                title: Text(todos[index]),
                trailing: IconButton(
                  icon: Icon(Icons.delete),
                  onPressed: () {
                    // 비즈니스 로직 호출
                    removeTodo(index);
                  },
                ),
              );
            }),
          ),
        ],
      ),
    );
  }
}
  1. UI와 데이터 로직이 한 곳에 있음
    • StatefulWidget을 사용하여 UI와 데이터 로직을 하나의 파일에 결합했다.
  2. 상태 변경 및 UI 업데이트:
    • addTodo와 removeTodo 메서드는 각각 새로운 할 일을 추가하고, 삭제하는 로직을 포함하고 있다.
    • setState()를 호출하여 todos 리스트의 변경을 Flutter에 알리고, UI가 재렌더링되도록 했다.
  3. 간단한 구조:
    • 작은 규모의 앱이나 간단한 기능을 가진 앱에 적합한 구조이다.

이 방식은 디자인 패턴이 없기 때문에 유지 보수가 어렵거나 코드가 복잡해지기 쉬운 단점이 있지만, 빠르고 간단하게 앱을 만들 수 있는 장점이 있음.

 

 

MVVM 패턴이란?

  • MVVM 패턴은 애플리케이션을 세 가지 역할로 나누어, 유지 보수성재사용성을 높이는 디자인 패턴이다.
  • Flutter에서는 UI 업데이트와 상태 관리가 필요하기 때문에 MVVM 패턴을 사용하여 코드 구조를 효율적으로 관리할 수 있다.

 

 

MVVM 패턴의 구성 요소

  • Model: 애플리케이션의 데이터 구조비즈니스 로직을 담당한다. 데이터를 정의하거나 데이터를 처리하는 로직이 포함된다.
  • View: 사용자 인터페이스(UI)를 담당한다. Flutter에서는 화면에 표시되는 위젯들이 View 역할을 하며, ViewModel을 통해 데이터를 받아 화면에 표시한다.
  • ViewModel: 비즈니스 로직과 UI 사이의 중간 역할을 한다. View에서 받은 요청을 처리하고, Model에서 데이터를 가져와 View에 전달하는 역할을 한다.

 

 

model
// todo의 모델 클래스 - 할일 데이터를 정의
class Todo {

  final String id;
  final String title;

  Todo({required this.id, required this.title});
}

 

 

view_model
// ViewModel 클래스 - 상태와 로직을 담당 한다.

import 'package:my_mvvm_v01/start02/models/todo.dart';

class TodoViewModel {

  // 데이터가 필요하다.
  List<Todo> todos = [];

  // 할 일을 추가하는 기능
  void addTodo(String title) {
    final newTodo = Todo(id: DateTime.now().toString(), title: title);
    todos.add(newTodo);
  }

  // 할 일을 삭제하는 기능
  void removeTodo(String id) {
    todos.removeWhere( (todo) => todo.id == id);
  }

}

 

 

view
import 'package:flutter/material.dart';
import 'package:my_mvvm_v01/start02/view_models/todo_view_model.dart';

void main() => runApp(MaterialApp(home: TodoScreen()));

class TodoScreen extends StatefulWidget {
  const TodoScreen({super.key});

  @override
  State<TodoScreen> createState() => _TodoScreenState();
}

class _TodoScreenState extends State<TodoScreen> {
  // MVVM 패턴이기 때문에 View 는 ViewModel 클래스만 참조 하면 된다.
  final TodoViewModel todoViewModel = TodoViewModel();
  final TextEditingController _controller = new TextEditingController();


  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('MVVM Basic Todo List'),
      ),
      body: Column(
        children: [
          // 입력 필드 만들기
          Padding(
            padding: const EdgeInsets.all(16.0),
            child: Row(
              children: [
                Expanded(
                  child: TextField(
                    controller: _controller,
                    decoration: InputDecoration(labelText: '작업을 입력 하시오'),
                  ),
                ),
                IconButton(
                  onPressed: () {
                    if(_controller.text.isNotEmpty) {
                      setState(() {
                        todoViewModel.addTodo(_controller.text);
                      });
                      _controller.clear();
                    }
                  },
                  icon: Icon(Icons.add),
                )
              ],
            ),
          ),
          // 아래에 할일 목록 표시 구성
          Expanded(
            child: ListView.builder(
                itemCount: todoViewModel.todos.length,
                itemBuilder: (context, index) {
                 // 뷰모델에 있는 자료구조 안에 각 인덱스에 맵핑된 객체 Todo인스턴스 하나
                 final todo = todoViewModel.todos[index];

                  return ListTile(
                    title: Text(todo.title),
                    trailing: IconButton(
                      icon: Icon(Icons.delete),
                      onPressed: () {
                        setState(() {
                          todoViewModel.removeTodo(todo.id);
                        });
                      },
                    ),
                  );
                }),
          ),
        ],
      ),
    );
  }
}

 

 

 

 

상태 변화가 있을 때 UI에 자동으로 알림 주기 (코드 발전 시키기 1)

ChangeNotifier는 Flutter에서 상태 변화가 있을 때 UI에 자동으로 알림을 주기 위한 클래스로, MVVM 패턴에서 ViewModel을 통해 UI와 데이터를 연결하는 데 매우 유용하다.

 

 

뷰 모델
// ViewModel 클래스 - 상태와 로직을 담당 한다.

import 'package:flutter/material.dart';
import 'package:my_mvvm_v01/start02/models/todo.dart';

// ChangeNotifier 상속 한다.
class TodoViewModel extends ChangeNotifier {

  // 데이터가 필요하다.
  List<Todo> todos = [];

  // 할 일을 추가하는 기능
  void addTodo(String title) {
    final newTodo = Todo(id: DateTime.now().toString(), title: title);
    todos.add(newTodo);
    // 상태 알림 호출
    notifyListeners();
  }

  // 할 일을 삭제하는 기능
  void removeTodo(String id) {
    todos.removeWhere( (todo) => todo.id == id);
    // UI에 상태가 변경되었다고 알림
    notifyListeners();
  }

}

 

 

ChangeNotifier 를 활용한 view 클래스
import 'package:flutter/material.dart';
import 'package:my_mvvm_v01/start03/view_models/todo_view_model.dart';

void main() => runApp(MaterialApp(home: TodoScreen()));

class TodoScreen extends StatefulWidget {
  const TodoScreen({super.key});

  @override
  State<TodoScreen> createState() => _TodoScreenState();
}

class _TodoScreenState extends State<TodoScreen> {
  // MVVM 패턴이기 때문에 View 는 ViewModel 클래스만 참조 하면 된다.
  final TodoViewModel todoViewModel = TodoViewModel();
  final TextEditingController _controller = new TextEditingController();


  @override
  void initState() {
    super.initState();
    // 단 한번만 호출 되는 메서드
    todoViewModel.addListener(() {
      // UI 재 렌더링 메서드
      setState(() {});
    });
  }

  @override
  void dispose() {
    todoViewModel.dispose();
    _controller.dispose();
    super.dispose();
  }

  // 프레젠테이션 로직을 함수화 시키자
  void _addTodo() {
    if(_controller.text.isNotEmpty) {
      todoViewModel.addTodo(_controller.text);
      _controller.clear();
    }
  }

  // 프레젠테이션 로직
  void _removeTodo(String id) {
    todoViewModel.removeTodo(id);
  }


  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('MVVM Basic Todo List'),
      ),
      body: Column(
        children: [
          // 입력 필드 만들기
          Padding(
            padding: const EdgeInsets.all(16.0),
            child: Row(
              children: [
                Expanded(
                  child: TextField(
                    controller: _controller,
                    decoration: InputDecoration(labelText: '작업을 입력 하시오'),
                  ),
                ),
                IconButton(
                  onPressed: _addTodo,
                  icon: Icon(Icons.add),
                )
              ],
            ),
          ),
          // 아래에 할일 목록 표시 구성
          Expanded(
            child: ListView.builder(
                itemCount: todoViewModel.todos.length,
                itemBuilder: (context, index) {
                 // 뷰모델에 있는 자료구조 안에 각 인덱스에 맵핑된 객체 Todo인스턴스 하나
                 final todo = todoViewModel.todos[index];
                  return ListTile(
                    title: Text(todo.title),
                    trailing: IconButton(
                      icon: Icon(Icons.delete),
                      onPressed: () => _removeTodo(todo.id),
                    ),
                  );
                }),
          ),
        ],
      ),
    );
  }
}

ViewModel을 ChangeNotifier를 상속하도록 수정하여, 상태 변경 시 알림을 방송할 수 있도록 한다.

 

 

 

 

Provider 라이브러리를 사용해보기 (코드 발전 시키기 2)

dependencies:
  flutter:
    sdk: flutter
  provider: ^6.1.2

 

import 'package:flutter/material.dart';
import 'package:my_mvvm_v01/start04/view_models/todo_view_model.dart';
import 'package:provider/provider.dart';

// MaterialApp 앱 안에서 외부 라이브러리(프로바이더) 위젯을 감싸 주어야 한다.
void main() => runApp(
      MaterialApp(
        //  (_) => TodoViewModel() -> 매개 변수를 사용안할 꺼면 _ 를 선언한다.
        home: ChangeNotifierProvider(
          create: (_) => TodoViewModel(),
          builder: (context, child) {
            return TodoScreen();
          },
        ),
      ),
    );

class TodoScreen extends StatelessWidget {
  TodoScreen({super.key});

  final TextEditingController _controller = TextEditingController();

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('MVVM provider Todo List'),
      ),
      body: Column(
        children: [
          // 입력 필드 만들기
          Padding(
            padding: const EdgeInsets.all(16.0),
            child: Row(
              children: [
                Expanded(
                  child: TextField(
                    controller: _controller,
                    decoration: InputDecoration(labelText: '작업을 입력 하시오'),
                  ),
                ),
                IconButton(
                  onPressed: () {
                    // 여기에서 뷰 모델 클래스를 가져오자 --> DI 처리
                    final todoViewModel =
                        Provider.of<TodoViewModel>(context, listen: false);
                    if (_controller.text.isNotEmpty) {
                      todoViewModel.addTodo(_controller.text);
                      _controller.clear();
                    }
                  },
                  icon: Icon(Icons.add),
                )
              ],
            ),
          ),
          // 아래에 할일 목록 표시 구성
          Expanded(
            child: Consumer<TodoViewModel>(
              builder: (context, todoViewModel, child) {
                return ListView.builder(
                  itemCount: todoViewModel.todos.length,
                  itemBuilder: (context, index) {
                    // 뷰모델에 있는 자료구조 안에 각 인덱스에 맵핑된 객체 Todo인스턴스 하나
                    final todo = todoViewModel.todos[index];
                    return ListTile(
                      title: Text(todo.title),
                      trailing: IconButton(
                        icon: Icon(Icons.delete),
                        onPressed: () => todoViewModel.removeTodo(todo.id),
                      ),
                    );
                  },
                );
              },
            ),
          ),
        ],
      ),
    );
  }
}

statelessWidget으로 작성해도 괜찮은 이유는 상태 관리를 ViewModel이 담당하기 때문이다. Provider와 ChangeNotifier를 통해 ViewModel이 상태 변화를 관리하고 UI에 반영하기 때문에, UI 위젯이 반드시 StatefulWidget일 필요가 없다.

 

 

정리

  • StatefulWidget일 필요가 없는 이유: ChangeNotifier를 통해 ViewModel이 상태를 관리하고, UI가 자동으로 알림을 받기 때문에, UI 자체가 상태를 가지지 않아도 된다.
  • UI 업데이트 범위 제한: 단, Consumer 위젯으로 변경이 필요한 위젯만 감싸, 필요한 부분만 다시 빌드하게 만든다. 이 방식은 성능 최적화에도 유리한다.
728x90