Repository ํจํด๊ณผ MVVM
์ง๋ ๊ธ์์ ํด๋๋ฅผ data์ ui๋ก ๋๋ด๋ค. ์ด๋ฒ์ ๊ทธ ์์ ๋ฌด์์ ๋ด๋์ง๋ฅผ ๋ค๋ฃฌ๋ค. ๋จผ์ ๋๋์ง ์์ ๋ชจ์ต๋ถํฐ ๋ณด์. ํ๋ฉด ํ๋๊ฐ ์๋ฒ ์์ฒญ, ํ์ฑ, ์ํ ๊ด๋ฆฌ๋ฅผ ์ ๋ถ ํ๋ ์ฝ๋๋ค.
class _UserPageState extends State<UserPage> {
List<User> users = [];
bool isLoading = true;
@override
void initState() {
super.initState();
load();
}
Future<void> load() async {
final response = await Dio().get('https://example.com/api/users'); // ํ๋ฉด์ด ์๋ฒ๋ฅผ ์ง์ ๋ถ๋ฆ
final list = response.data as List;
setState(() {
users = list.map((e) => User.fromJson(e as Map<String, dynamic>)).toList();
isLoading = false;
});
}
@override
Widget build(BuildContext context) { /* ... */ }
}๋์๊ฐ๊ธด ํ๋ค. ํ์ง๋ง ์ด๋ฐ ์ผ๋ค์ด ์๊ธด๋ค.
- ํ๋ฉด์ ์ํํ๋ ค๋ฉด ์ค์ ์๋ฒ๊ฐ ํ์ํ๋ค. ์๋ฒ๊ฐ ๊บผ์ ธ ์๊ฑฐ๋ ์ธํฐ๋ท์ด ์์ผ๋ฉด ํ๋ฉด์ ๋ง๋ค๋ค ๋ง๊ณ ๋ฉ์ถ๋ค
- ๊ฐ์ ์ฌ์ฉ์ ๋ชฉ๋ก์ ๋ค๋ฅธ ํ๋ฉด๋ ํ์๋ก ํ๋ฉด ์ด ์ฝ๋๋ฅผ ๋ณต์ฌํด์ ๋ถ์ธ๋ค. ์๋ฒ ์ฃผ์๊ฐ ๋ฐ๋๋ฉด ๋ณต์ฌํ ๊ณณ์ ์ ๋ถ ๊ณ ์ณ์ผ ํ๋ค
- ํ๋ฉด ํ์ผ์ ๋คํธ์ํฌ, ํ์ฑ, ์ํ, ๊ทธ๋ฆฌ๊ธฐ๊ฐ ํ๊บผ๋ฒ์ ์์ฌ์ ํ ์ค์ ๊ณ ์น ๋ ๋ฌด์์ด ๊นจ์ง์ง ์๊ธฐ ์ด๋ ต๋ค
์์ธ: ํ๋์ ํด๋์ค๊ฐ ๋๋ฌด ๋ง์ ์ฑ ์์ ์ง๊ณ ์๋ค
ํ๋ฉด(UserPage)์ ์ผ์ "์ํ๋ฅผ ๋ฐ์์ ๊ทธ๋ฆฌ๋ ๊ฒ"๋ฟ์ด์ด์ผ ํ๋ค. ๋ฐ์ดํฐ๋ฅผ ์ด๋์ ์ด๋ป๊ฒ ๊ฐ์ ธ์ค๋์ง๋ ํ๋ฉด์ด ์ ํ์๊ฐ ์๋ค. ์ฑ
์์ ๋๋๋ ๋ฐฉ๋ฒ์ผ๋ก Flutter ๊ณต์ ์ฑ ์ํคํ
์ฒ ๊ฐ์ด๋๋ ์ฑ์ UI ๊ณ์ธต๊ณผ ๋ฐ์ดํฐ ๊ณ์ธต์ผ๋ก ๋๋๊ณ , ๊ฐ๊ฐ์ ๋๋ก ์ชผ๊ฐ๋ผ๊ณ ์๋ดํ๋ค.
๋ค ๊ฐ์ง ์ญํ
- View(UI ๊ณ์ธต): ์ํ๋ฅผ ๋ฐ์์ ํ๋ฉด์ ๊ทธ๋ฆฐ๋ค. ์ฌ์ฉ์ ์ ๋ ฅ์ ViewModel์ ๋๊ธด๋ค. ์์ ฏ์ด๋ค
- ViewModel(UI ๊ณ์ธต): ํ๋ฉด์ ํ์ํ ์ํ๋ฅผ ๋ค๊ณ , ๋ฐ์ดํฐ๋ฅผ ํ๋ฉด์ฉ์ผ๋ก ๋ฐ๊พธ๊ณ , ์ฌ์ฉ์ ๋์์ ์ฒ๋ฆฌํ๋ค. Cubit์ด๋ Riverpod์ Notifier๊ฐ ๋งก๋ ์๋ฆฌ๋ค
- Repository(๋ฐ์ดํฐ ๊ณ์ธต): ํ ์ข ๋ฅ์ ๋ฐ์ดํฐ์ ๋ํ ๋จ์ผ ์ง์ค ๊ณต๊ธ์์ด๋ค. ๋ฐ์ดํฐ๊ฐ ์ด๋์ ์ค๋ (์๋ฒ, ์บ์, ๋ก์ปฌ DB) ํ๋ฉด์ Repository๋ง ๋ณธ๋ค
- Service(๋ฐ์ดํฐ ๊ณ์ธต): ์๋ฒ API๋ ํ๋ซํผ ๊ธฐ๋ฅ ๊ฐ์ ์ธ๋ถ ๋ฐ์ดํฐ ์์ค ํ๋๋ฅผ ๊ฐ์ธ๋ ์์ ํด๋์ค๋ค. ์ํ๋ฅผ ๊ฐ์ง ์๋๋ค
๊ณต์ ๊ฐ์ด๋๊ฐ ์ ํ ์ง๋ ์๋ค. ์ฑ์ ๋ฐ์ดํฐ ์ข ๋ฅ๋ง๋ค Repository ํ๋, ๋ฐ์ดํฐ ์์ค๋ง๋ค Service ํ๋๋ฅผ ๋๊ณ , ํ Repository๊ฐ ์ฌ๋ฌ Service๋ฅผ ์ธ ์ ์๋ค. ๊ทธ๋ฆฌ๊ณ View์ ViewModel์ ๊ธฐ๋ฅ ํ๋๋น ํ ์์ผ๋ก ๋ง๋ ๋ค.
View โ ViewModel โ Repository โ Service
(ํ๋ฉด) (ํ๋ฉด์ ์ํ) (๋ฐ์ดํฐ์ ์ฃผ์ธ) (์๋ฒ ํธ์ถ)ํ์ดํ๋ ํ ๋ฐฉํฅ์ด๊ณ ์ด์ํ ๊ณ์ธต๋ผ๋ฆฌ๋ง ๋งํ๋ค. View๊ฐ Service๋ฅผ ์ง์ ๋ถ๋ฅด๋ฉด ์์์ ๋ณธ "ํ๋ฉด์ด ์๋ฒ๋ฅผ ์ง์ ๋ถ๋ฅด๋" ์ฝ๋๋ก ๋์๊ฐ ๋ฒ๋ฆฐ๋ค.
ํด๊ฒฐ: ๊ณ์ธต๋ณ๋ก ์ฝ๋๋ฅผ ๋๋๋ค
์ด ๊ธ์ User๋ id์ name๋ง ๊ฐ์ง ๋จ์ํ ๋ชจ๋ธ์ด๋ผ๊ณ ๊ฐ์ ํ๋ค.
Service: ์๋ฒ ํธ์ถ๋ง ํ๋ค
class UserApiService {
final Dio _dio;
UserApiService(this._dio);
Future<List<dynamic>> fetchUsers() async {
final response = await _dio.get('/users');
return response.data as List<dynamic>; // ํ์ฑ์ ํ์ง ์๊ณ JSON ๊ทธ๋๋ก
}
}Repository: ๋ฐ์ดํฐ๋ฅผ ๋ชจ๋ธ๋ก ๋ฐ๊พธ๊ณ ํ๋ฉด์ ์ฝ์ํ๋ค
๊ณต์ ๊ฐ์ด๋๋ Repository๋ฅผ ์ถ์ ํด๋์ค๋ก ๋จผ์ ์ ์ํ๋ผ๊ณ ๊ถํ๋ค. ๊ทธ๋ฌ๋ฉด ๊ฐ๋ฐ์ฉ, ํ
์คํธ์ฉ ๋ฑ ์๋ก ๋ค๋ฅธ ๊ตฌํ์ ๋ผ์ ๋ฃ์ ์ ์๋ค. ์์ ๊ธ์์ ๋ณธ implements๊ฐ ์ฌ๊ธฐ์ ์ฐ์ธ๋ค.
abstract class UserRepository {
Future<List<User>> getUsers();
}
class UserRepositoryImpl implements UserRepository {
final UserApiService _service;
UserRepositoryImpl(this._service);
@override
Future<List<User>> getUsers() async {
final json = await _service.fetchUsers();
return json.map((e) => User.fromJson(e as Map<String, dynamic>)).toList();
}
}UserRepository๋ "์ฌ์ฉ์ ๋ชฉ๋ก์ ๋ฌ๋ผ"๋ ์ฝ์(๊ป๋ฐ๊ธฐ)๋ง ์ ํ๊ณ , UserRepositoryImpl์ด ์ค์ ๋ก ์๋ฒ์์ ๊ฐ์ ธ์จ๋ค. ์ํ์ฉ ๊ฐ์ง ๊ตฌํ๋ ๊ฐ์ ์ฝ์์ ๋ฐ๋ฅธ๋ค.
class FakeUserRepository implements UserRepository {
@override
Future<List<User>> getUsers() async => [
User(id: 1, name: 'ํ๊ธธ๋'),
User(id: 2, name: '๊น์ฒ ์'),
];
}ViewModel: ํ๋ฉด์ ์ํ๋ฅผ ๋ค๊ณ ์๋ค
Riverpod์ ์ฐ๊ณ ์์ผ๋ AsyncNotifier๊ฐ ViewModel ์ญํ ์ ํ๋ค. ๋ก๋ฉ, ๋ฐ์ดํฐ, ์๋ฌ ์ํ๋ฅผ AsyncValue๊ฐ ์ด๋ฏธ ๊ฐ์ง๊ณ ์์ด์ ์ง์ ๋ง๋ค ํ์๊ฐ ์๋ค.
class UserListViewModel extends AsyncNotifier<List<User>> {
@override
Future<List<User>> build() {
return ref.watch(userRepositoryProvider).getUsers(); // ์ฒ์ ์ํ
}
Future<void> refresh() async {
ref.invalidateSelf(); // ๋ค์ ๋ถ๋ฌ์ค๊ธฐ
await future;
}
}
final userListViewModelProvider =
AsyncNotifierProvider<UserListViewModel, List<User>>(UserListViewModel.new);build()๊ฐ ์ฒ์ ๊ฐ์ ๋ง๋ ๋ค. ์ด๋ ์๋ฒ๋ฅผ ์ง์ ๋ถ๋ฅด์ง ์๊ณ Repository๋ฅผ ๊ฑฐ์น๋ค. ๋ค์ ๋ถ๋ฌ์ค๊ธฐ(refresh)์ฒ๋ผ ์ฌ์ฉ์ ๋์์ ๋ํ ์ฒ๋ฆฌ๋ ViewModel์ ๋๋ค.
์กฐ๋ฆฝ: ๋๊ฐ ๋๊ตฌ๋ฅผ ์ฐ๋์ง ์ฐ๊ฒฐํ๋ค
final dioProvider = Provider<Dio>(
(ref) => Dio(BaseOptions(baseUrl: 'https://example.com/api')),
);
final userApiServiceProvider = Provider<UserApiService>(
(ref) => UserApiService(ref.watch(dioProvider)),
);
final userRepositoryProvider = Provider<UserRepository>(
(ref) => UserRepositoryImpl(ref.watch(userApiServiceProvider)),
);Dio๋ฅผ Service์ ๋๊ธฐ๊ณ , Service๋ฅผ Repository์ ๋๊ธฐ๊ณ , Repository๋ฅผ ViewModel์ด ์ด๋ค. ์ด๋ ๊ฒ ํ์ํ ๊ฑธ ๋ฐ์์ ๋ฃ์ด์ฃผ๋ ๊ตฌ์กฐ๋ฅผ ์์กด์ฑ ์ฃผ์
์ด๋ผ๊ณ ๋ถ๋ฅด๊ณ , ๋ค์์์ ๋ฐ๋ก ๋ค๋ฃฌ๋ค. ์ง๊ธ์ ref.watch(...)๋ก ์ฐ๊ฒฐํ๋ค๋ ๋ชจ์๋ง ๋ด๋์.
View: ์ํ๋ฅผ ๊ทธ๋ฆฌ๊ธฐ๋ง ํ๋ค
class UserListView extends ConsumerWidget {
const UserListView({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final users = ref.watch(userListViewModelProvider);
return users.when(
loading: () => const Center(child: CircularProgressIndicator()),
error: (e, _) => ErrorView(
message: errorMessage(e),
onRetry: () => ref.read(userListViewModelProvider.notifier).refresh(),
),
data: (list) => ListView.builder(
itemCount: list.length,
itemBuilder: (context, i) => ListTile(title: Text(list[i].name)),
),
);
}
}ํ๋ฉด ์ฝ๋์ Dio๋, fromJson๋, ์๋ฒ ์ฃผ์๋ ์๋ค. "์ํ๋ฅผ ๋ฐ์ ๊ทธ๋ฆฌ๊ณ , ๋ฒํผ์ ๋๋ฅด๋ฉด ViewModel์ ์๋ฆฐ๋ค"๋ง ๋จ์๋ค. ErrorView, errorMessage๋ ์ ๊ธ์์ ๋ง๋ ๊ฒ์ ๊ทธ๋๋ก ์ด๋ค.
ํจ๊ณผ: ๊ฐ์ง Repository๋ก ๋ฐ๊ฟ ๋ผ์ฐ๊ธฐ
์ฒ์ ๊ฒช์๋ ๋ฌธ์ , ์๋ฒ ์์ด๋ ํ๋ฉด์ ๋ง๋ค ์ ์๋ค๋ ๊ฒ์ด ํ๋ฆฐ๋ค.
void main() {
runApp(
ProviderScope(
overrides: [
userRepositoryProvider.overrideWithValue(FakeUserRepository()),
],
child: const MyApp(),
),
);
}overrideWithValue๋ userRepositoryProvider๊ฐ ๋๋ ค์ค ๊ฐ์ ๊ฐ์ง๋ก ๋ฐ๊ฟ ๋ผ์ด๋ค. ์๋ฒ๊ฐ ์์ด๋ FakeUserRepository์ ๋ ์ฌ๋์ด ํ๋ฉด์ ๋ฌ๋ค. ๊ฐ์ ๋ฐฉ์์ผ๋ก ํ
์คํธ์์๋ ๊ฐ์ง๋ฅผ ๋ฃ์ ์ ์๋ค. ๊ณต์ ๊ฐ์ด๋๋ Service, Repository, ViewModel ํด๋์ค๋ง๋ค ๋จ์ ํ
์คํธ๋ฅผ ์ฐ๋ผ๊ณ ๊ถํ๋๋ฐ, ๊ณ์ธต์ด ๋๋์ด ์์ด์ผ ๊ฐ๋ฅํ ์ผ์ด๋ค.
์ฝ์ง ํฌ์ธํธ: ViewModel์ BuildContext๋ฅผ ๋ฃ๊ณ ์ถ์ด์ง๋ค
์ ์ฅ์ด ๋๋๋ฉด ์ค๋ต๋ฐ๋ฅผ ๋์ฐ๊ณ ์ถ์ด์ ViewModel ํจ์์ BuildContext๋ฅผ ๋๊ธฐ๋ ์ฝ๋๋ฅผ ์ฐ๊ธฐ ์ฝ๋ค. ํ์ง๋ง BuildContext๋ ์์ ฏ ํธ๋ฆฌ์์์ ์์น์ด๊ณ , ViewModel์ด ํ๋ฉด์ ๋ถ์ด๋ฒ๋ฆฌ๋ฉด ์ํํ๊ธฐ ์ด๋ ต๊ณ await ๋ค์ ํ๋ฉด์ด ์ฌ๋ผ์ก์ ๋ ๋ฌธ์ ๊ฐ ๋๋ค. ํ๋ฉด์ ๊ทธ๋ฆฌ๋ ์ผ(์ค๋ต๋ฐ, ์ด๋)์ View๊ฐ ํ๊ณ , ViewModel์ "์ ์ฅ์ด ๋๋ฌ๋ค"๋ ์ํ๋ ๊ฒฐ๊ณผ๋ง ์๋ฆฐ๋ค. View๊ฐ ๊ทธ ์ํ๋ฅผ ๋ณด๊ณ ์ค๋ต๋ฐ๋ฅผ ๋์ด๋ค.
๋ ํ๋, Repository๊ฐ ์ฌ์ฉ์ ๋ชฉ๋ก ์บ์๋ฅผ ๋ค๊ณ ์๋ค๋ฉด "์๋ฒ์์ ์๋ก ๋ฐ์์ง, ์บ์๋ฅผ ์ธ์ง"๋ฅผ ์ ํ๋ ์ผ๋ Repository์ ์ฑ ์์ด๋ค. ViewModel์ด Service๋ฅผ ์ฐํํด์ ์ง์ ๋ถ๋ฅด๋ ์๊ฐ ์ด ๊ท์น์ด ๊นจ์ง๋ค.
Android, iOS์ ๋น๊ตํ๋ฉด
// Kotlin - Android ๊ณต์ ์ํคํ
์ฒ
class UserRepository(private val api: UserApi) {
suspend fun getUsers(): List<User> = api.users().map { it.toUser() }
}
class UserViewModel(private val repo: UserRepository) : ViewModel() {
val state = MutableStateFlow<UiState>(UiState.Loading)
}// Swift - protocol๋ก Repository ์ถ์ํ
protocol UserRepository {
func getUsers() async throws -> [User]
}
@Observable class UserViewModel {
var users: [User] = []
init(repo: UserRepository) { self.repo = repo }
}- View: Flutter ์์ ฏ / Compose
@Composable/ SwiftUIView - ViewModel: Flutter
Notifier,Cubit/ AndroidViewModel/ SwiftUI@Observableํด๋์ค - Repository: ์ธ ๊ณณ ๋ชจ๋ ๊ฐ์ ์ด๋ฆ, ๊ฐ์ ์ญํ (๋ฐ์ดํฐ์ ๋จ์ผ ์ถ์ฒ)
- Service: Flutter
Service/ Android์DataSource, Retrofit ์ธํฐํ์ด์ค์ ๊ฐ๊น์ / iOSAPIํด๋ผ์ด์ธํธ - ์ถ์ํ: Flutter
abstract class์implements/ Kotlininterface/ Swiftprotocol
Android์ ๊ณต์ ์ํคํ
์ฒ์ ๊ตฌ์กฐ๊ฐ ๊ฑฐ์ ๊ฐ๋ค. Flutter ๊ฐ์ด๋์ Service๊ฐ ์๋๋ก์ด๋์ ๋ฐ์ดํฐ ์์ค(Retrofit API ์ธํฐํ์ด์ค ๋ฑ)์ ๊ฐ๊น๋ค๊ณ ๋ณด๋ฉด ์ดํด๊ฐ ๋น ๋ฅด๋ค. Swift์ protocol๋ก Repository๋ฅผ ์ถ์ํํ๋ ๊ฒ๋ ์ค๋ ์ด abstract class + implements์ ๊ฐ์ ๋ฐ์์ด๋ค.
์ ๋ฆฌ
- ํ๋ฉด์ด ์๋ฒ ํธ์ถ, ํ์ฑ, ์ํ๋ฅผ ๋ชจ๋ ํ๋ฉด ์ํํ๊ธฐ ์ด๋ ต๊ณ ๊ฐ์ ์ฝ๋๊ฐ ๋ณต์ฌ๋๋ฏ๋ก, ์ฑ ์์ View, ViewModel, Repository, Service๋ก ๋๋๋ค
- View๋ ์ํ๋ฅผ ๊ทธ๋ฆฌ๊ธฐ๋ง ํ๊ณ , ViewModel์ ํ๋ฉด์ ์ํ์ ์ฌ์ฉ์ ๋์์ ๋งก์ผ๋ฉฐ, Repository๋ ํ ์ข ๋ฅ ๋ฐ์ดํฐ์ ๋จ์ผ ์ถ์ฒ์ด๊ณ , Service๋ ์ธ๋ถ ๋ฐ์ดํฐ ์์ค ํ๋๋ฅผ ๊ฐ์ผ๋ค
- ์ด์ํ ๊ณ์ธต๋ผ๋ฆฌ๋ง ๋ํํ๊ณ , View๊ฐ Service๋ฅผ ์ง์ ๋ถ๋ฅด์ง ์๋๋ค
- Repository๋ ์ถ์ ํด๋์ค๋ก ์ฝ์์ ๋จผ์ ์ ํ๊ณ , ์ค์ ๊ตฌํ๊ณผ ๊ฐ์ง ๊ตฌํ์
implements๋ก ๋ฐ๊ฟ ๋ผ์ด๋ค - Riverpod์์๋
AsyncNotifier๊ฐ ViewModel ์ญํ ์ ํ๊ณ ๋ก๋ฉ, ๋ฐ์ดํฐ, ์๋ฌ๋ฅผAsyncValue๊ฐ ๊ฐ์ง๊ณ ์๋ค - ์ค๋ต๋ฐ๋ ํ๋ฉด ์ด๋ ๊ฐ์ ํ๋ฉด ์ผ์ ViewModel์ด ์๋๋ผ View๊ฐ ํ๊ณ ,
BuildContext๋ฅผ ViewModel์ ๋๊ธฐ์ง ์๋๋ค
๋ค์ ๊ธ์์๋ ๊ณ์ธต์ ์๋ก ์ฐ๊ฒฐํ๋ ์์กด์ฑ ์ฃผ์
(get_it, Riverpod)์ ๋ค๋ค๋ณด๊ฒ ๋ค.
