Flutter에서 MVVM (Model-View-ViewModel) 패턴을 적용하면 UI와 비즈니스 로직을 분리할 수 있어 유지보수가 쉬워지고, 확장성이 높아집니다. 이번 포스팅에서는 MVVM 패턴의 개념과 실전 적용 방법을 정리해 보겠습니다.
📌 MVVM 패턴이란?
MVVM 패턴은 애플리케이션을 Model, View, ViewModel로 나누어 관리하는 설계 패턴입니다.
개념역할
Model | 데이터 구조 및 비즈니스 로직 관리 |
View | UI를 담당 |
ViewModel | View와 Model 사이에서 데이터를 관리하고 상태를 유지 |
📌 MVVM 디렉토리 구조 예시
Flutter에서 MVVM을 적용할 때, 다음과 같은 구조를 사용할 수 있습니다.
lib/
├── config/ # 앱 설정 (라우트 등)
├── core/ # 상수, UI 공통 요소
├── data/
│ ├── datasource/ # API, Firebase 등 데이터 원본
│ ├── model/ # 데이터 모델 정의
│ ├── repository/ # 데이터 소스를 통합 관리
│ ├── view_model/ # 상태 관리 및 UI 데이터 제공
├── ui/
│ ├── home/
│ ├── login/
│ ├── mypage/
│ ├── place/
└── main.dart
📌 ViewModel과 Repository는 왜 분리해야 할까?
많은 사람들이 ViewModel과 Repository가 같은 개념이라고 착각하는데, 사실 역할이 다릅니다.
개념하는 역할
ViewModel | UI 상태를 관리하고, 데이터를 Repository에서 가져옴 |
Repository | 데이터 소스(API, Firebase 등)를 관리하여 ViewModel에 제공 |
🚨 ViewModel에서 직접 API 호출하면?
- API 변경 시 ViewModel을 수정해야 함 → 유지보수 어려움
- 여러 ViewModel에서 같은 데이터를 사용할 경우 중복 발생 → 재사용성 낮음
- UI 로직과 데이터 로직이 섞임 → 가독성 낮음
💡 ✅ 해결책: Repository를 추가하여 데이터 관리 역할을 분리하면 유지보수가 쉬워짐!
📌 DataSource는 왜 필요할까?
Repository에서 데이터를 직접 다루는 것이 아니라
DataSource를 따로 분리하면 여러 개의 데이터 소스를 쉽게 관리할 수 있습니다.
예제: API와 Firebase를 분리한 DataSource
📂 lib/data/datasource/place_api_datasource.dart
import 'dart:convert';
import 'package:http/http.dart' as http;
import '../model/place_model.dart';
class PlaceApiDataSource {
final String baseUrl = "https://api.example.com";
Future<list> fetchPlacesFromApi() async {
final response = await http.get(Uri.parse("$baseUrl/places"));
if (response.statusCode == 200) {
List data = jsonDecode(response.body);
return data.map((json) => PlaceModel.fromJson(json)).toList();
} else {
throw Exception("Failed to load places from API");
}
}
}</list
📂 lib/data/datasource/place_firebase_datasource.dart
import 'package:cloud_firestore/cloud_firestore.dart';
import '../model/place_model.dart';
class PlaceFirebaseDataSource {
final FirebaseFirestore _firestore = FirebaseFirestore.instance;
Future<List<PlaceModel>> fetchPlacesFromFirebase() async {
QuerySnapshot snapshot = await _firestore.collection("places").get();
return snapshot.docs
.map((doc) => PlaceModel.fromJson(doc.data() as Map<String, dynamic>))
.toList();
}
}
📂 lib/data/repository/place_repository.dart
import '../datasource/place_api_datasource.dart';
import '../datasource/place_firebase_datasource.dart';
import '../model/place_model.dart';
class PlaceRepository {
final PlaceApiDataSource apiDataSource;
final PlaceFirebaseDataSource firebaseDataSource;
PlaceRepository({
required this.apiDataSource,
required this.firebaseDataSource,
});
Future<List<PlaceModel>> getPlaces({bool useFirebase = false}) async {
return useFirebase
? await firebaseDataSource.fetchPlacesFromFirebase()
: await apiDataSource.fetchPlacesFromApi();
}
}
📂 lib/data/view_model/place_view_model.dart
import 'package:flutter/material.dart';
import '../model/place_model.dart';
import '../repository/place_repository.dart';
class PlaceViewModel extends ChangeNotifier {
final PlaceRepository repository;
PlaceViewModel({required this.repository});
List<PlaceModel> _places = [];
List<PlaceModel> get places => _places;
bool _isLoading = false;
bool get isLoading => _isLoading;
Future<void> loadPlaces({bool useFirebase = false}) async {
_isLoading = true;
notifyListeners();
try {
_places = await repository.getPlaces(useFirebase: useFirebase);
} catch (e) {
print("Error fetching places: $e");
}
_isLoading = false;
notifyListeners();
}
}
📌 View에서 ViewModel을 사용하기
📂 lib/ui/place/place_screen.dart
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import '../../data/view_model/place_view_model.dart';
class PlaceScreen extends StatefulWidget {
@override
_PlaceScreenState createState() => _PlaceScreenState();
}
class _PlaceScreenState extends State<PlaceScreen> {
bool useFirebase = false;
@override
void initState() {
super.initState();
Provider.of<PlaceViewModel>(context, listen: false)
.loadPlaces(useFirebase: useFirebase);
}
@override
Widget build(BuildContext context) {
final placeViewModel = Provider.of<PlaceViewModel>(context);
return Scaffold(
appBar: AppBar(
title: const Text("장소 목록"),
actions: [
Switch(
value: useFirebase,
onChanged: (value) {
setState(() {
useFirebase = value;
placeViewModel.loadPlaces(useFirebase: useFirebase);
});
},
)
],
),
body: placeViewModel.isLoading
? const Center(child: CircularProgressIndicator())
: ListView.builder(
itemCount: placeViewModel.places.length,
itemBuilder: (context, index) {
final place = placeViewModel.places[index];
return ListTile(
title: Text(place.name),
subtitle: Text(place.description),
);
},
),
);
}
}
📌 결론: MVVM을 적용하면?
1. ViewModel과 Repository를 분리하면 유지보수성이 높아진다.
2. DataSource를 활용하면 API와 Firebase 같은 다양한 데이터 소스를 쉽게 전환할 수 있다.
3. ViewModel은 UI 상태 관리에 집중하고, Repository는 데이터 로직을 담당하는 것이 이상적이다.
🔥 MVVM 패턴을 적용하면 코드가 깔끔해지고 확장성이 높아진다!
'IT > flutter' 카테고리의 다른 글
[Flutter] Form 위젯과 validate 사용 - 유효성 검사 프로세스 (0) | 2025.03.18 |
---|---|
[디자인패턴] MVVM 기본 예시 with Riverpod (0) | 2025.02.26 |
[flutter] Container위젯과 DecoratedBox 위젯의 효율적인 사용 (0) | 2025.01.23 |
[상태관리] flutter Riverpod을 이용하여 todo 리스트 만들어 보기 (0) | 2024.12.19 |
[상태관리] Riverpod - StateNotifier와 StateNotifierProvider (0) | 2024.12.19 |