๐ Today I Learned 23ํ์ฐจ - MVVM & Riverpod ์ํ ๊ด๋ฆฌ ๐ก
์ค๋์ Flutter ์ฑ ๊ฐ๋ฐ์์ ํต์ฌ์ ์ธ MVVM ์ํคํ ์ฒ์ Riverpod์ ํ์ฉํ ์ํ ๊ด๋ฆฌ์ ๋ํด ํ์ตํ์ต๋๋ค. MVVM ํจํด์ด ์ ํ์ํ์ง, ๊ทธ๋ฆฌ๊ณ Riverpod์ด ์ด๋ป๊ฒ ์ด๋ฅผ ํจ์จ์ ์ผ๋ก ๊ตฌํํ๋๋ก ๋์์ฃผ๋์ง ์ค์ต์ ํตํด ์ตํ์ด์! ๐
๐ฏ ํ์ต ๋ชฉํ
- MVVM ์ํคํ ์ฒ์ ๊ฐ๋ ๊ณผ ๊ฐ ๊ณ์ธต์ ์ญํ ์ดํดํ๊ธฐ
- Riverpod ๋ผ์ด๋ธ๋ฌ๋ฆฌ๋ฅผ ์ฌ์ฉํ ์ํ ๊ด๋ฆฌ ๋ฐฉ๋ฒ ์ตํ๊ธฐ
Notifier์NotifierProvider๋ฅผ ํ์ฉํ ViewModel ๊ตฌํํ๊ธฐConsumer์์ ฏ์ ํตํ ์ํ ๊ตฌ๋ ๋ฐ UI ์ ๋ฐ์ดํธ ๊ตฌํํ๊ธฐ
๐ ์ฃผ์ ๋ด์ฉ
โญ MVVM ์ํคํ ์ฒ
MVVM์ Model + View + ViewModel์ ์ฝ์๋ก, ์ฑ์ ๊ตฌ์กฐ๋ฅผ ์ธ ๊ฐ์ง ๊ณ์ธต์ผ๋ก ๋ถ๋ฆฌํ๋ ์ํคํ ์ฒ ํจํด์ ๋๋ค.
๐ MVVM ๋์ ํ๋ฆ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ View (UI) โ
โ โโโ Widget โ
โโโโโโโโโโโโโโโโฌโโโโโโโโโโโโโโโโโโโโโโฒโโโโโโโโโโโโโโโโโ
โ โ
โ ์ฌ์ฉ์ ์ก์
โฆ ์ํ ๋ณ๊ฒฝ ๊ฐ์ง
(๋ฒํผ ํด๋ฆญ ๋ฑ) (ํ๋ฉด ์
๋ฐ์ดํธ)
โ โ
โผ โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ ViewModel โ
โ โโโ ์ํ ๊ด๋ฆฌ ๋ฐ ๋น์ฆ๋์ค ๋ก์ง โ
โ (View๋ฅผ ์์ง ๋ชปํจ) โ
โโโโโโโโโโโโโโโโฌโโโโโโโโโโโโโโโโโโโโโโฒโโโโโโโโโโโโโโโโโ
โ โ
โข ๋ก์ง ์ฒ๋ฆฌ โค ๋ฐ์ดํฐ ๊ฐ๊ณต
โฃ ๋ฐ์ดํฐ ์์ฒญ โฅ ์ํ ์
๋ฐ์ดํธ
โ โ
โผ โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ Model (Data) โ
โ โโโ Repository, API โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
๐ก MVVM์ ์ฅ์
| ์ฅ์ | ์ค๋ช |
|---|---|
| ์ฝ๋ ๋ถ๋ฆฌ | ๊ฐ ๊ณ์ธต์ ์ญํ ์ ๋ง๊ฒ ์ฝ๋๋ฅผ ์์ฑํ์ฌ ์ฝ๋๊ฐ ๊น๋ํด์ง |
| ํ ์คํธ ์ฉ์ด | ๊ฐ ๊ณ์ธต๋ณ๋ก ๋ ๋ฆฝ์ ์ธ ํ ์คํธ ์์ฑ์ด ๊ฐ๋ฅ |
| ๋ฎ์ ๊ฒฐํฉ๋ | View๋ ViewModel์ ์ฐธ์กฐํ์ง๋ง, ViewModel์ ์ด๋ค View๊ฐ ์ฐธ์กฐํ๋์ง ๋ชจ๋ฆ |
| ์ ์ง๋ณด์์ฑ | ๋ก์ง๊ณผ UI๊ฐ ๋ถ๋ฆฌ๋์ด ์์ ์ด ์ฉ์ดํจ |
๐ก ํต์ฌ: Flutter์์๋ ์ํ ๊ด๋ฆฌ ๋ผ์ด๋ธ๋ฌ๋ฆฌ(Riverpod, Provider ๋ฑ)๋ฅผ ํตํด MVVM์ ํจ๊ณผ์ ์ผ๋ก ๊ตฌํํ ์ ์์ต๋๋ค!
โก Riverpod ์ํ ๊ด๋ฆฌ
Riverpod์ Flutter์์ ์ํ ๊ด๋ฆฌ๋ฅผ ํธํ๊ฒ ํ ์ ์๋ ๊ฐ๋ ฅํ ๋ผ์ด๋ธ๋ฌ๋ฆฌ์ ๋๋ค.
๐ Riverpod์ ์ฃผ์ ํน์ง
- ๊ฐ๋ฐ์ ์นํ์ : ์ํ ๊ด๋ฆฌ๋ฅผ ๊ฐ๋จํ๊ณ ์ง๊ด์ ์ผ๋ก ๊ตฌํ ๊ฐ๋ฅ
- ViewModel ์ญํ ์ง์: ์ํ๋ฅผ ๊ด๋ฆฌํ๊ณ ๋ณ๊ฒฝ์ ์๋ฆฌ๋ ์ญํ ์ ์ฝ๊ฒ ๊ตฌํ
- View์ ViewModel ์ฐ๊ฒฐ:
Consumer์์ ฏ์ ํตํด ์ฝ๊ฒ ์ํ๋ฅผ ๊ด์ฐฐํ๊ณ UI ์ ๋ฐ์ดํธ
๐ ๏ธ Riverpod ํต์ฌ ๊ตฌ์ฑ ์์
| ๊ตฌ์ฑ ์์ | ์ญํ | ์ฃผ์ ๋ฉ์๋/ํน์ง |
|---|---|---|
| Notifier | ์ํ๋ฅผ ์ ์ฅํ๊ณ ์ ๋ฐ์ดํธํ๋ ํด๋์ค | build(): ์ต์ด ์ํ ๋ฐํstate: ์ํ ์
๋ฐ์ดํธ ์ ์ ๊ฐ์ฒด ์์ฑ |
| NotifierProvider | ViewModel์ ์์ ฏ์ ๊ณต๊ธํ๋ ๊ฐ์ฒด | ViewModel์ ์ ์ญ์ ์ผ๋ก ์ ๊ณต |
| Consumer | ViewModel์ ์ํ๋ฅผ ๊ตฌ๋ ํ๋ ์์ ฏ | ์ํ ๋ณ๊ฒฝ ์ ์๋์ผ๋ก UI ๋ฆฌ๋น๋ |
| ProviderScope | ๋ชจ๋ Provider๋ฅผ ์ฌ์ฉ ๊ฐ๋ฅํ๊ฒ ํ๋ ์ต์์ ์์ ฏ | main() ํจ์์์ ์ฑ ์ ์ฒด๋ฅผ ๊ฐ์ |
๐ป ์ค์ต ๋ฐ ์์
๐ฆ 1. Riverpod ํจํค์ง ์ค์น
# pubspec.yaml์ ํจํค์ง ์ถ๊ฐ
flutter pub add flutter_riverpod
๐๏ธ 2. ํ๋ก์ ํธ ๊ตฌ์กฐ
lib/
โโโ main.dart # ์ฑ ์ง์
์
โโโ models/
โ โโโ user.dart # ๋ฐ์ดํฐ ๋ชจ๋ธ
โโโ repositories/
โ โโโ user_repository.dart # ๋ฐ์ดํฐ ๊ฐ์ ธ์ค๊ธฐ
โโโ viewmodels/
โ โโโ user_viewmodel.dart # ์ํ ๊ด๋ฆฌ ๋ก์ง
โโโ views/
โโโ user_list_screen.dart # UI ์์ ฏ
๐ง 3. ๊ตฌํ ์์
Model ํด๋์ค ๋ง๋ค๊ธฐ
// models/user.dart
class User {
final int id;
final String name;
final String email;
// ์์ฑ์
User({
required this.id,
required this.name,
required this.email,
});
// ์ํ๋ฅผ ๋ถ๋ณ์ผ๋ก ๊ด๋ฆฌํ๊ธฐ ์ํ copyWith ๋ฉ์๋
User copyWith({
int? id,
String? name,
String? email,
}) {
return User(
id: id ?? this.id,
name: name ?? this.name,
email: email ?? this.email,
);
}
}
Repository ํด๋์ค ๋ง๋ค๊ธฐ
// repositories/user_repository.dart
class UserRepository {
// API๋ ๋ก์ปฌ DB์์ ๋ฐ์ดํฐ ๊ฐ์ ธ์ค๊ธฐ
Future<List<User>> fetchUsers() async {
// ์ค์ ๋ก๋ HTTP ์์ฒญ์ด๋ DB ์ฟผ๋ฆฌ
await Future.delayed(Duration(seconds: 1)); // ๋คํธ์ํฌ ์ง์ฐ ์๋ฎฌ๋ ์ด์
return [
User(id: 1, name: '๊น์ฒ ์', email: 'kim@example.com'),
User(id: 2, name: '์ด์ํฌ', email: 'lee@example.com'),
User(id: 3, name: '๋ฐ๋ฏผ์', email: 'park@example.com'),
];
}
}
ViewModel ๋ง๋ค๊ธฐ (Notifier ์์)
// viewmodels/user_viewmodel.dart
import 'package:flutter_riverpod/flutter_riverpod.dart';
// ์ํ ํด๋์ค ์ ์
class UserListState {
final List<User> users;
final bool isLoading;
UserListState({
required this.users,
required this.isLoading,
});
// ์ด๊ธฐ ์ํ
factory UserListState.initial() {
return UserListState(
users: [],
isLoading: false,
);
}
// ์ํ ๋ณต์ฌ ๋ฉ์๋
UserListState copyWith({
List<User>? users,
bool? isLoading,
}) {
return UserListState(
users: users ?? this.users,
isLoading: isLoading ?? this.isLoading,
);
}
}
// ViewModel ํด๋์ค (Notifier ์์)
class UserViewModel extends Notifier<UserListState> {
final UserRepository _repository = UserRepository();
// build(): ์ต์ด ์ํ ๋ฐํ
@override
UserListState build() {
return UserListState.initial();
}
// ์ฌ์ฉ์ ๋ชฉ๋ก ๋ก๋ ๋ฉ์๋
Future<void> loadUsers() async {
// ๋ก๋ฉ ์ํ๋ก ๋ณ๊ฒฝ (์๋ก์ด ๊ฐ์ฒด ์์ฑ)
state = state.copyWith(isLoading: true);
try {
// Repository์์ ๋ฐ์ดํฐ ๊ฐ์ ธ์ค๊ธฐ
final users = await _repository.fetchUsers();
// ์ํ ์
๋ฐ์ดํธ (์๋ก์ด ๊ฐ์ฒด ์์ฑ)
state = state.copyWith(
users: users,
isLoading: false,
);
} catch (e) {
// ์๋ฌ ์ฒ๋ฆฌ
state = state.copyWith(isLoading: false);
}
}
// ์ฌ์ฉ์ ์ถ๊ฐ ๋ฉ์๋
void addUser(User user) {
state = state.copyWith(
users: [...state.users, user],
);
}
}
// NotifierProvider ๊ฐ์ฒด ์์ฑ (์ ์ญ ๋ณ์)
final userViewModelProvider = NotifierProvider<UserViewModel, UserListState>(() {
return UserViewModel();
});
View์์ ์ฌ์ฉํ๊ธฐ (Consumer ์์ ฏ)
// main.dart
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
void main() {
// ProviderScope๋ก ์ฑ ์ ์ฒด๋ฅผ ๊ฐ์ธ๊ธฐ (ํ์!)
runApp(
ProviderScope(
child: MyApp(),
),
);
}
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
home: UserListScreen(),
);
}
}
// views/user_list_screen.dart
class UserListScreen extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
// ViewModel์ ์ํ๋ฅผ ๊ตฌ๋
(watch)
final userState = ref.watch(userViewModelProvider);
// ViewModel์ ๋ฉ์๋์ ์ ๊ทผ (read)
final viewModel = ref.read(userViewModelProvider.notifier);
return Scaffold(
appBar: AppBar(
title: Text('์ฌ์ฉ์ ๋ชฉ๋ก'),
),
body: userState.isLoading
? Center(child: CircularProgressIndicator())
: ListView.builder(
itemCount: userState.users.length,
itemBuilder: (context, index) {
final user = userState.users[index];
return ListTile(
leading: CircleAvatar(child: Text('${user.id}')),
title: Text(user.name),
subtitle: Text(user.email),
);
},
),
floatingActionButton: FloatingActionButton(
onPressed: () {
// ViewModel์ ๋ฉ์๋ ํธ์ถ
viewModel.loadUsers();
},
child: Icon(Icons.refresh),
),
);
}
}
๐ฏ Riverpod ์ฌ์ฉ ์ ํต์ฌ ํฌ์ธํธ
๐ก ์ํ ์ ๋ฐ์ดํธ ์์น: ์ํ๋ฅผ ์ ๋ฐ์ดํธํ ๋๋ ํญ์ ์๋ก์ด ๊ฐ์ฒด๋ฅผ ๋ง๋ค์ด์ ๋ณ๊ฒฝํด์ผ ํฉ๋๋ค!
// โ ์๋ชป๋ ๋ฐฉ๋ฒ - ์ง์ ์์ ํ๋ฉด UI๊ฐ ์
๋ฐ์ดํธ๋์ง ์์
state.users.add(newUser);
// โ
์ฌ๋ฐ๋ฅธ ๋ฐฉ๋ฒ - ์๋ก์ด ๊ฐ์ฒด ์์ฑ
state = state.copyWith(
users: [...state.users, newUser],
);
๐ Riverpod ์ฌ์ฉ์ ์ฅ์ ์ ๋ฆฌ
| ์ฅ์ | ์ค๋ช |
|---|---|
| ์ ์ญ ์ํ ๊ด๋ฆฌ | Provider๊ฐ ViewModel์ ์ ์ญ์ ์ผ๋ก ๊ณต๊ธํ์ฌ ์ด๋์๋ ์ ๊ทผ ๊ฐ๋ฅ |
| ๋ถํ์ํ ํ๋ผ๋ฏธํฐ ์ ๋ฌ ์ ๊ฑฐ | ํ์ ์์ ฏ์ props๋ก ๊ณ์ ์ ๋ฌํ ํ์ ์์ |
| ์ฝ๋ ๋ถ๋ฆฌ | ์์ ฏ์ UI๋ง, ViewModel์ ์ํ ๊ด๋ฆฌ๋ง ๋ด๋นํ์ฌ ์ ์ง๋ณด์ ์ฉ์ด |
| ํ์ ์์ ์ฑ | ์ปดํ์ผ ํ์์ ์ค๋ฅ ๊ฐ์ง ๊ฐ๋ฅ |
๐ ๋ง๋ฌด๋ฆฌ
โ ์ค๋ ๋ฐฐ์ด ๊ฒ
- MVVM ์ํคํ ์ฒ: Model, View, ViewModel์ ์ญํ ๋ถ๋ฆฌ๋ฅผ ํตํด ๊น๋ํ ์ฝ๋ ๊ตฌ์กฐ ๊ตฌํ
- Riverpod์ ํต์ฌ ๊ฐ๋
:
Notifier,NotifierProvider,Consumer์์ ฏ์ ์ญํ ๊ณผ ์ฌ์ฉ๋ฒ - ์ํ ๊ด๋ฆฌ ํจํด: ๋ถ๋ณ ์ํ ๊ด๋ฆฌ๋ฅผ ์ํ
copyWith๋ฉ์๋ ํ์ฉ - ์ค์ต ๊ตฌํ: ์ฌ์ฉ์ ๋ชฉ๋ก์ ๊ด๋ฆฌํ๋ ์์ ํ MVVM + Riverpod ์์ ์์ฑ
๋๊ธ๋จ๊ธฐ๊ธฐ