EP4. Flutter Clean Architecture
클린 아키텍처는 소프트웨어 설계 원칙으로, 코드의 유연성, 유지보수성, 테스트 용이성을 높이기 위해 계층을 분리하고 의존성을 역전시키는 구조입니다. 주요 개념은 비즈니스 로직과 구현 세부 사항을 분리하여, 상위 계층이 하위 계층에 의존하지 않도록 하는 것입니다. 이를 통해 시스템 변경이 특정 계층에만 영향을 미치도록 하여, 시스템의 확장성과 재사용성을 높입니다.
1. MVVM 패턴
MVVM (Model-View-ViewModel) 패턴은 사용자 인터페이스 개발을 위한 아키텍처 패턴으로, 다음 세 가지 주요 구성 요소로 나뉩니다:
• Model: 애플리케이션의 데이터와 비즈니스 로직을 담당합니다.
• View: 사용자 인터페이스(UI)를 담당하며, 사용자가 상호작용하는 화면 요소를 포함합니다.
• ViewModel: View와 Model 사이의 중간 계층으로, View의 상태와 동작을 관리하며 Model과의 데이터 바인딩을 처리합니다.
2. 디렉토리 구조
lib/
├── entities/ 👈 서비스에서 사용되는 Entity class 관리
├── services/
│ ├── push_service.dart/ 👈 예시. 클린아키텍처에서 유스케이스 파트
├── views/ 👈 각 화면별 코드
│ ├── home/ 👈 예시. page(view), viewModel, model로 구성됨
│ │ ├── home_page.dart
│ │ ├── home_viewmodel.dart
│ │ ├── home_model.dart
│ ├── /sign in
│ │ ├── sign_in_page.dart
│ │ ├── sign_in_viewmodel.dart
│ │ ├── sign_in_model.dart
│ └── ...
└── main.dart 👈 애플리케이션의 진입점
Plain Text
복사
디렉토리 구조의 정답은 없습니다. View, ViewModel, Model을 각각 모아두는 방법도 있지만 개인적으로 화면별로 묶어두는게 개발하는 과정에서 편리한 부분이 있어 저는 위 구조로 사용합니다.
3. Service 예시
push_service.dart
class PushService {
final ApiClient _apiClient = ApiClient('https://rkeelyzbzieohfcsmsch.supabase.co/functions/v1/api');
Future<ResponseFormat<bool>> addToken(String token) async {
final body = {'fcmToken': token};
return _apiClient.post<bool>('/push/v1/fcmToken', body, (json) => json as bool);
}
}
Dart
복사
•
Service에서는 외부로부터 데이터를 내부로 가져오는 역할을 수행합니다.
4. Model 예시
push_model.dart
class PushModel {
final PushService _pushService;
final KSNotificationService _notificationService;
PushModel(this._pushService, this._notificationService);
Future<ResponseFormat<bool>> addToken(String token) async {
return await _pushService.addToken(token);
}
}
Dart
복사
•
Model은 Service를 갖습니다.
•
Model의 생성자로 Service를 주입받도록 설계합니다.
•
Service로 부터 가져온 값을 비즈니스에따라 연산하는 역할을 수행합니다.
5. ViewModel 예시
push_view_model.dart
class PushViewModel with ChangeNotifier {
final PushModel _pushModel = PushModel(PushService(), KSNotificationService());
Future<void> addToken() async {
String token = tokenController.text;
if (token.isEmpty) {
setResponse("Token cannot be empty");
return;
}
final response = await _pushModel.addToken(token);
if (response.result != null && response.result!) {
setResponse("Token added successfully");
} else {
setResponse(response.message);
}
}
}
Dart
복사
•
ViewModel은 Model을 갖습니다.
•
ChangeNotifier를 상속받아 구현합니다. View에 그려질 State 역할을 수행합니다.
•
View에 보여질 값을 만듭니다. (ex. 다국어 지원, Date Formatter, Currency Formatter)
6. View 예시
home_view.dart
class HomeView extends StatelessWidget {
const HomeView({super.key});
Widget build(BuildContext context) {
var viewModel = Provider.of<HomeViewModel>(context);
return Scaffold(
body: Center(
child: ElevatedButton(
child: Text('${viewModel.userID()} sign out'),
onPressed: () {
viewModel.signOut();
},
),
),
);
}
}
Dart
복사
•
View의 State를 ViewModel로 관리하므로 View는 StatelessWidget으로 구현합니다.
7. 기존 프로젝트 구조 변경하기
현재 샘플앱을 위 MVVM 패턴 디렉토리 구조로 변경하겠습니다.
1.
샘플앱 수준의 Pubspec에 Provider 추가
flutter pub add provider
flutter pub get
Dart
복사
ChangeNotifier를 상속받은 ViewModel을 View에서 사용하기 위해서 Provider를 이용해야 합니다.
샘플앱 수준의 Pubspec에 provider를 설치합니다.
2.
프로젝트 코드 변경
아래 강의 코드 참고해주세요.