기존의 모노리스 monolith...
모든 로직과 상태관리를 하나의 파일에 통합
간단한 앱 빠르게 개발
MVVM 패턴
모델-뷰-뷰모델
소프트웨어의 세 가지 핵심 영역인 데이터, UI, 그리고 그 둘을 연결하는 로직을 분리하는 디자인 패턴이다.
관심사의 분리(Separation of Concerns) 를 통해
쉬운 유지보수
독립적인 개발
테스트 용이성...
등의 이점을 가져갈 수 있다.
예제) 간단한 카운팅 프로그램 변경
import 'package:flutter/material.dart';
void main() => runApp(CountApp());
class CountApp extends StatelessWidget {
const CountApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
debugShowCheckedModeBanner: false,
home: CountScreen(),
);
}
}
class CountScreen extends StatefulWidget {
const CountScreen({super.key});
@override
State<CountScreen> createState() => _CountScreenState();
}
class _CountScreenState extends State<CountScreen> {
int _counter = 0;
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('뷰 모델없이 코드를 작성해보기'),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
'간단한 뷰의 모델 예제',
style: TextStyle(
fontSize: 20,
),
),
const SizedBox(
height: 20,
),
Text(
'$_counter',
style: TextStyle(
fontSize: 35,
),
),
const SizedBox(
height: 10,
),
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
TextButton(
onPressed: _incrementCounter,
style: TextButton.styleFrom(),
child: Text(
'increment',
style: TextStyle(
fontSize: 20,
),
),
),
IconButton(onPressed: _refresh, icon: Icon(Icons.refresh)),
],
),
],
),
),
);
}
void _incrementCounter() {
setState(() {
_counter++;
});
}
void _refresh() {
setState(() {
_counter = 0;
});
}
}
MVVM 패턴 분리 전략
1. 역할 분리: UI와 로직 분해
UI (View)
화면에 보이는 위젯들 (Text, TextButton, IconButton 등) 이다.
이들은 데이터를 단순히 표시하고
사용자의 입력(터치)을 받아들이는 역할만 한다.
로직 (Model & ViewModel)
_counter 변수와 _incrementCounter(), _refresh() 메서드들
이들은 데이터를 변경하고 관리하는 로직이다.
이 부분들이 UI와 분리돼야 한다.
2. 데이터 계층 Model 설계
모델은 순수한 Dart 클래스다.
즉 UI와 관련된 Flutter 코드를 포함해서는 안된다.
_counter 변수
increment() 메서드
refresh() 메서드..
등을 모두 모델에 추가한다.
class Model {
// 현재 카운터 값을 저장하는 변수
int _count = 0;
// 현재 카운터 값을 반환하는 getter 메서드
int get count => _count;
// 카운터 값을 변경하는 핵심로직 메서드
void increment() {
_count++;
}
// 카운터 값을 초기화하는 핵심로직 메서드
void refresh() {
_count = 0;
}
}
3. 연결 계층 ViewModel 설계
뷰모델은 뷰에게 필요한 모든 데이터를 제공하고 명령을 받는 단일 창구가 된다.
1)뷰가 모델에 직접 접근하는 것을 막고, 뷰모델이 모델의 인스턴스를 소유합니다.
2)모델의 데이터를 뷰에 노출하기 위해 getter를 제공합니다. (viewModel.counter)
3)뷰의 명령(버튼 클릭 등)을 받아서 모델의 핵심 로직을 실행하는 메서드들을 만듭니다. (viewModel.increment(), viewModel.refresh())
import 'model.dart';
class ViewModel {
// 뷰와 모델을 연결하는 모델 인스턴스
final Model model = Model();
// 뷰에 카운터 값을 노출하는 getter 메서드
int get counter => model.count;
// 뷰의 'increment' 명령을 처리하고 핵심로직을 호출하는 메서드
void increment() {
model.increment();
}
// 뷰의 'refresh' 명령을 처리하고 핵심로직을 호출하는 메서드
void refresh() {
model.refresh();
}
}
4. UI 계층 View 리팩토링
1) StatefulWidget에서 데이터(_counter)와 로직(_incrementCounter())을 모두 제거한다.
2) ViewModel 의 인스턴스를 만든다.
3) Text 위젯은 ViewModel.counter를 참조하도록 수정한다.
4) 버튼의 onPressed 콜백은 ViewModel.increment()를 호출하고 setState()를 실행해 화면을 갱신한다.
import 'package:flutter/material.dart';
// 뷰모델 클래스 도입
import 'viewmodel.dart';
void main() => runApp(CountApp());
class CountApp extends StatelessWidget {
const CountApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
debugShowCheckedModeBanner: false,
home: CountScreen(),
);
}
}
class CountScreen extends StatefulWidget {
const CountScreen({super.key});
@override
State<CountScreen> createState() => _CountScreenState();
}
class _CountScreenState extends State<CountScreen> {
//뷰모델 인스턴스 생성
final viewModel = ViewModel();
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('뷰 모델없이 코드를 작성해보기'),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
'간단한 뷰의 모델 예제',
style: TextStyle(
fontSize: 20,
),
),
const SizedBox(
height: 20,
),
Text(
// 뷰모델의 counter getter를 화면에 표시
'${viewModel.counter}',
style: TextStyle(
fontSize: 35,
),
),
const SizedBox(
height: 10,
),
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
TextButton(
onPressed: () {
// 뷰모델의 increment 메서드 호출
viewModel.increment();
setState(() {});
},
style: TextButton.styleFrom(),
child: Text(
'increment',
style: TextStyle(
fontSize: 20,
),
),
),
IconButton(
onPressed: () {
// 뷰모델의 refresh 메서드 호출
viewModel.refresh();
setState(() {});
},
icon: Icon(Icons.refresh)),
],
),
],
),
),
);
}
}
◆ 굳이 코드를 분리하는 이유
처음에는 하나의 파일에 모든 코드를 넣는 것이 더 빠르고 편해 보일 수 있다. 하지만 앱이 성장하면서 다음과 같은 문제가 발생한다.
① 코드의 얽힘: UI(화면), 데이터(값), 로직(계산)이 모두 섞여 코드를 읽고 이해하기 어렵다.
② 유지보수의 어려움: 버그가 발생했을 때 어디서 문제가 생겼는지 찾기 어렵다.UI 수정이 로직에 영향을 주는 등 예상치 못한 오류가 발생할 수 있다.
③ 재사용성 저하: 로직이 UI 코드와 분리되지 않아 다른 화면에서 재사용하기 힘들다.
◆ 해결책: MVVM 패턴
MVVM 패턴은 위 문제를 해결하기 위해 각자의 역할을 명확하게 분리한다.
① View (UI 계층): 오직 화면을 보여주고 사용자 입력을 받는 역할만 한다.
② Model (데이터 계층): 데이터를 관리하고 조작하는 핵심 로직만 담당한다.
③ ViewModel (연결 계층): View와 Model 사이의 다리 역할을 하며, View의 명령을 받아 Model의 로직을 실행한다.
이러한 구조 덕분에 UI 담당 개발자와 로직 담당 개발자가 각자의 역할에만 집중할 수 있고, 새로운 기능이 추가되더라도 기존 코드를 쉽게 수정하거나 확장할 수 있게 된다.
'Flutter' 카테고리의 다른 글
플러터) 게시물작성 코드 구조 (4) | 2025.08.22 |
---|---|
통신을 위한 모델과 리포지터리 설계 (0) | 2025.08.19 |
SharedPreferences (1) | 2025.08.13 |
구글 지도 api 써보기 (1) | 2025.08.13 |
플러터) 콜백메서드, 아이의 일을 부모가 알게 하자 (0) | 2025.07.28 |