1. MVVM 패턴
MVVM은 앱의 기능을 Model - View - ViewModel 로 구분하는 아키텍처 패턴입니다.
Model : 데이터 구조와 비즈니스 로직 담당
View : 사용자의 입력을 받고 UI를 구성
ViewModel : 상태를 관리하고, View와 Model사이에서 데이터를 연결

2. 권장 앱 아키텍처 구조

Flutter 공식 문서에서는 앱을 UI Layer와 Data Layer로 나누고, 필요에 따라 중간 레이어인 Domain Layer를 추가하여 복잡한 로직을 분리할 수도 있습니다.

1) UI Layer, Presentation Layer (View, ViewModel)
UI Layer는 Presentation Layer라고도 불립니다.
이는 사용자가 직접적으로 보는 화면으로, 사용자의 입력을 받는 Layer입니다.
사용자의 입력을 받는다는 것은 버튼을 클릭하거나 텍스트를 입력하는 등의 행위를 말합니다.
MVVM패턴에서는 View와 ViewModel이 해당하는데요.
- View
View는 화면에 보여줄 UI를 구성하는 위젯들의 조합으로 비즈니스 로직을 포함해서는 안됩니다.
여기서 비즈니스 로직은 앱이 해야 할 핵심 동작을 처리하는 코드입니다.
예를 들어 상품을 장바구니에 추가할 때 상품이 품절인지 확인하고, 이미 장바구니에 있는지 확인하며, 있으면 수량 +1, 없으면 새로 추가하는 등의 코드가 비즈니스 로직입니다.
View는 렌더링에 필요한 모든 데이터를 ViewModel에서 전달받아야 합니다.
또한, 사용자의 입력 이벤트를 ViewModel에 전달하는 역할을 합니다.
그러니까 사용자가 버튼을 클릭하면 버튼을 클릭한 상태라는 것을 ViewModel에 알리는 것이지요.
- ViewModel
ViewModel은 View를 렌더링하는 데 필요한 상태 데이터를 관리합니다.
예를 들어 Data 계층의 Repository에서 받은 데이터를 UI에 맞는 형식으로 가공합니다.
Flutter에서 ViewModel은 StateNotifier, Notifier를 상속받아 작성하고, Provider로 주입합니다.
2) Data Layer (Model)
Data Layer는 앱의 실제 데이터와 로직을 담당하는 계층입니다.
- Service
Service는 REST API, 플랫폼 기능, 로컬 저장소 등 외부 리소스와 연결합니다.
데이터를 가져오거나 보내는 비동기 작업만 수행하여
Future, Stream 형태로 응답을 전달합니다.
또한, 상태를 가지지 않습니다.
- Repository
Repository는 서버나 로컬 DB에서 데이터를 가져와 ViewModel에게 전달합니다.
주로 캐싱, 오류 처리, 재시도, 데이터 새로고침, 사용자 액션에 따른 데이터 새로고침 등의 작업도 담당합니다.
- Model
MVVM에서 Model은 데이터 구조를 정의하여 앱의 실제 데이터와 외부 연동을 담당하는 계층입니다.
예를 들어 사용자에게 프로필 정보를 화면에 보여줄 때,
UserApiService -> UserRepository -> UserViewModel -> UserView
이렇게 데이터가 흘러갑니다.
UserApiService에서 실제 API를 호출하고, http 같은 라이브러리로 서버와 통신합니다.
데이터를 있는 그대로 가져오기만 하며, 가공하지는 않습니다.
그 후, 데이터는 UserRepository에게 전달됩니다.
UserRepository에서는 데이터를 받아 에러처리 등 데이터를 가공하여 UserViewModel에게 전달합니다.
UserViewModel에서는 데이터를 상태로 저장하여 View에게 보여줄 형식으로 가공합니다.
또한, 상태 관리, 버튼 클릭, 사용자 입력 같은 이벤트 처리도 담당합니다.
그 후, UserView는 ViewModel로부터 상태를 구독하고 UI를 그립니다.
또한, 사용자의 이벤트를 ViewModel에게 전달하기도 합니다.
이렇게 데이터가 보여지게 되는 것입니다.
3) Domain Layer (optional)
앱의 규모가 커지고 기능이 추가됨에 따라 비즈니스 로직을 담당하는 ViewModel이 복잡해질 수 있습니다.
이때 Domain Layer를 두고 복잡한 로직을 UseCase로 분리할 수 있습니다.
- UseCase
UseCase는 ViewModel과 Repository 사이에서 비즈니스 로직을 담당합니다.
여러 Repository의 데이터를 결합하거나, 재사용 가능한 로직이 있을 경우, 조건문이나 상태 변환이 복잡할 때 사용합니다.
예를 들어 유저의 정보와 그 유저의 게시글을 한 번에 가져오는 기능이 있다고 가정하겠습니다.
FetchUserWithPostsUseCase 내부에서 사용자의 정보를 가져오는 UserRepository와 게시글을 가져오는 PostRepository를 조합해서 데이터를 가져옵니다.
그 후, ViewModel에서 FetchUserWithPostsUseCase를 호출해서 결과를 받고 데이터를 상태로 저장하여 UI에 필요한 형태로 가공합니다.
마지막으로 View에서 ViewModel의 상태를 구독하여 사용자의 정보와 게시글을 화면에 보여줍니다.

3. 참고자료
https://docs.flutter.dev/app-architecture/guide
https://docs.flutter.dev/get-started/fundamentals/state-management
https://medium.com/@codemax120/flutter-clean-architecture-mvvm-f8802e3df564
'Flutter' 카테고리의 다른 글
| [Flutter] API 키 숨기기 (feat. dotenv) (0) | 2025.04.22 |
|---|---|
| [Flutter] 지역 검색 앱 프로젝트 소개 (0) | 2025.04.22 |
| [Flutter] Riverpod 상태 관리부터 개념 정리까지 (0) | 2025.04.11 |
| [Flutter] StatelessWidget, StatefulWidget, build() (0) | 2025.03.26 |
| [Dart] 조건문 (if, if-else, switch) (0) | 2025.03.10 |