๐Ÿ“š Today I Learned 23ํšŒ์ฐจ - MVVM & Riverpod ์ƒํƒœ ๊ด€๋ฆฌ ๐Ÿ’ก

4 ๋ถ„ ์†Œ์š”

์˜ค๋Š˜์€ 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 ์˜ˆ์ œ ์ž‘์„ฑ

๋Œ“๊ธ€๋‚จ๊ธฐ๊ธฐ