{"id":85,"date":"2025-07-16T14:46:49","date_gmt":"2025-07-16T05:46:49","guid":{"rendered":"http:\/\/34.64.61.65\/?p=85"},"modified":"2025-07-16T14:51:59","modified_gmt":"2025-07-16T05:51:59","slug":"4-flutter-%ec%83%81%ed%83%9c-%ea%b4%80%eb%a6%ac-%ec%99%84%eb%b2%bd-%ea%b0%80%ec%9d%b4%eb%93%9c","status":"publish","type":"post","link":"https:\/\/hed-g.me\/?p=85","title":{"rendered":"4. [Flutter] \uc0c1\ud0dc \uad00\ub9ac \uc644\ubcbd \uac00\uc774\ub4dc"},"content":{"rendered":"\n<h1 class=\"wp-block-heading is-style-text-subtitle is-style-text-subtitle--1\">Flutter \uc0c1\ud0dc \uad00\ub9ac \uc644\ubcbd \uac00\uc774\ub4dc: \ub124\uc774\ud2f0\ube0c \uac1c\ubc1c\uc790\ub97c \uc704\ud55c \uc2e4\ubb34 \uc120\ud0dd \uc804\ub7b5<\/h1>\n\n\n\n<blockquote class=\"wp-block-quote is-layout-flow wp-block-quote-is-layout-flow\">\n<p>\uc548\ub4dc\ub85c\uc774\ub4dc\/iOS \uac1c\ubc1c\uc790\uac00 \uac00\uc7a5 \uace0\ubbfc\ud558\ub294 &#8220;\uc5b4\ub5a4 \uc0c1\ud0dc \uad00\ub9ac\ub97c \uc120\ud0dd\ud574\uc57c \ud560\uae4c?&#8221;\uc5d0 \ub300\ud55c \uacb0\uc815\uc801 \ub2f5\ubcc0<\/p>\n<\/blockquote>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\">\uc11c\ub860: \uc0c1\ud0dc \uad00\ub9ac, \uc65c \uc774\ub807\uac8c \ubcf5\uc7a1\ud560\uae4c?<\/h2>\n\n\n\n<p>\uc548\ub4dc\ub85c\uc774\ub4dc \uac1c\ubc1c\uc790\ub77c\uba74 MVVM\uc758 LiveData\ub098 DataBinding\uc744, iOS \uac1c\ubc1c\uc790\ub77c\uba74 MVC\ub098 MVVM\uc758 \uad00\ucc30\uc790 \ud328\ud134\uc744 \uc0ac\uc6a9\ud574\ubd24\uc744 \uac83\uc785\ub2c8\ub2e4. \uc6f9 \ud504\ub860\ud2b8\uc5d4\ub4dc \uac1c\ubc1c\uc790\ub77c\uba74 Redux\ub098 MobX, Vue\uc758 Vuex \uac19\uc740 \uc0c1\ud0dc \uad00\ub9ac \ub77c\uc774\ube0c\ub7ec\ub9ac\uc5d0 \uc775\uc219\ud558\uc2e4 \uac81\ub2c8\ub2e4.<\/p>\n\n\n\n<p><strong>\uadf8\ub7f0\ub370 Flutter\ub294 \uc65c \uc774\ub807\uac8c \ub9ce\uc740 \uc0c1\ud0dc \uad00\ub9ac \uc635\uc158\uc774 \uc788\uc744\uae4c\uc694?<\/strong><\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li><strong>\ub0b4\uc7a5 \uc0c1\ud0dc \uad00\ub9ac<\/strong>: setState, InheritedWidget<\/li>\n\n\n\n<li><strong>Provider \uc0dd\ud0dc\uacc4<\/strong>: Provider, Riverpod<\/li>\n\n\n\n<li><strong>\ubc18\uc751\ud615 \ub77c\uc774\ube0c\ub7ec\ub9ac<\/strong>: GetX, MobX<\/li>\n\n\n\n<li><strong>BLoC \ud328\ud134<\/strong>: flutter_bloc<\/li>\n\n\n\n<li><strong>\uae30\ud0c0<\/strong>: Redux, fish-redux \ub4f1\ub4f1&#8230;<\/li>\n<\/ul>\n\n\n\n<p>\uc774 \uae00\uc5d0\uc11c\ub294 <strong>\uc2e4\uc81c \ud504\ub85c\uc81d\ud2b8 \uacbd\ud5d8\uc744 \ubc14\ud0d5\uc73c\ub85c<\/strong> \uac01 \uc0c1\ud0dc \uad00\ub9ac\uc758 \uc7a5\ub2e8\uc810\uacfc \uc120\ud0dd \uae30\uc900\uc744 \uc81c\uc2dc\ud558\uaca0\uc2b5\ub2c8\ub2e4. \ub354 \uc774\uc0c1 &#8220;\uc5b4\ub5a4 \uac78 \uc368\uc57c \ud560\uc9c0 \ubaa8\ub974\uaca0\ub2e4&#8221;\ub294 \uace0\ubbfc\uc740 \ud558\uc9c0 \ub9c8\uc138\uc694!<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\">\ub124\uc774\ud2f0\ube0c vs Flutter: \uc0c1\ud0dc \uad00\ub9ac \ucca0\ud559\uc758 \ucc28\uc774<\/h2>\n\n\n\n<h3 class=\"wp-block-heading\">\uc548\ub4dc\ub85c\uc774\ub4dc\uc758 \uc0c1\ud0dc \uad00\ub9ac<\/h3>\n\n\n\n<pre class=\"wp-block-code\"><code>\/\/ Android MVVM with LiveData\nclass UserViewModel : ViewModel() {\n    private val _users = MutableLiveData&lt;List&lt;User&gt;&gt;()\n    val users: LiveData&lt;List&lt;User&gt;&gt; = _users\n\n    private val _isLoading = MutableLiveData&lt;Boolean&gt;()\n    val isLoading: LiveData&lt;Boolean&gt; = _isLoading\n\n    fun loadUsers() {\n        _isLoading.value = true\n        repository.getUsers { userList -&gt;\n            _users.value = userList\n            _isLoading.value = false\n        }\n    }\n}\n\n\/\/ Activity\/Fragment\uc5d0\uc11c \uad00\ucc30\nviewModel.users.observe(this) { users -&gt;\n    adapter.submitList(users)\n}<\/code><\/pre>\n\n\n\n<h3 class=\"wp-block-heading\">iOS\uc758 \uc0c1\ud0dc \uad00\ub9ac<\/h3>\n\n\n\n<pre class=\"wp-block-code\"><code>\/\/ iOS MVVM with Combine (\ub610\ub294 RxSwift)\nclass UserViewModel: ObservableObject {\n    @Published var users: &#91;User] = &#91;]\n    @Published var isLoading: Bool = false\n\n    private let repository: UserRepository\n    private var cancellables = Set&lt;AnyCancellable&gt;()\n\n    func loadUsers() {\n        isLoading = true\n        repository.getUsers()\n            .sink(\n                receiveCompletion: { _ in self.isLoading = false },\n                receiveValue: { users in self.users = users }\n            )\n            .store(in: &amp;cancellables)\n    }\n}<\/code><\/pre>\n\n\n\n<h3 class=\"wp-block-heading\">Flutter\uc758 \uc811\uadfc\ubc95<\/h3>\n\n\n\n<p>Flutter\ub294 <strong>\uc120\uc5b8\uc801 UI<\/strong>\ub97c \uae30\ubc18\uc73c\ub85c \ud558\ubbc0\ub85c, \uc0c1\ud0dc\uac00 \ubcc0\uacbd\ub418\uba74 UI\uac00 \uc790\ub3d9\uc73c\ub85c \uc7ac\ube4c\ub4dc\ub429\ub2c8\ub2e4. \uc774\ub294 React\uc758 \ucca0\ud559\uacfc \uc720\uc0ac\ud569\ub2c8\ub2e4.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>\/\/ Flutter\uc758 \uae30\ubcf8 \uc811\uadfc\ubc95\nclass UserScreen extends StatefulWidget {\n  @override\n  _UserScreenState createState() =&gt; _UserScreenState();\n}\n\nclass _UserScreenState extends State&lt;UserScreen&gt; {\n  List&lt;User&gt; users = &#91;];\n  bool isLoading = false;\n\n  @override\n  Widget build(BuildContext context) {\n    return Scaffold(\n      body: isLoading \n        ? CircularProgressIndicator()\n        : ListView.builder(\n            itemCount: users.length,\n            itemBuilder: (context, index) =&gt; UserTile(users&#91;index]),\n          ),\n    );\n  }\n}<\/code><\/pre>\n\n\n\n<p><strong>\ud575\uc2ec \ucc28\uc774\uc810:<\/strong><\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li><strong>\ub124\uc774\ud2f0\ube0c<\/strong>: \uc0c1\ud0dc \ubcc0\uacbd \u2192 \uad00\ucc30\uc790 \uc54c\ub9bc \u2192 \uc218\ub3d9 UI \uc5c5\ub370\uc774\ud2b8<\/li>\n\n\n\n<li><strong>Flutter<\/strong>: \uc0c1\ud0dc \ubcc0\uacbd \u2192 \uc704\uc82f \uc7ac\ube4c\ub4dc \u2192 \uc790\ub3d9 UI \uc5c5\ub370\uc774\ud2b8<\/li>\n<\/ul>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\">\ud504\ub85c\uc81d\ud2b8 \uaddc\ubaa8\ubcc4 \uc0c1\ud0dc \uad00\ub9ac \uc120\ud0dd \uac00\uc774\ub4dc<\/h2>\n\n\n\n<h3 class=\"wp-block-heading\">\ud83c\udfe0 \uc18c\uaddc\ubaa8 \ud504\ub85c\uc81d\ud2b8 (\ud654\uba74 5\uac1c \uc774\ud558)<\/h3>\n\n\n\n<p><strong>\ucd94\ucc9c: setState + InheritedWidget<\/strong><\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>\/\/ \uac04\ub2e8\ud55c \uce74\uc6b4\ud130 \uc571\nclass CounterApp extends StatefulWidget {\n  @override\n  _CounterAppState createState() =&gt; _CounterAppState();\n}\n\nclass _CounterAppState extends State&lt;CounterApp&gt; {\n  int counter = 0;\n\n  void increment() {\n    setState(() {\n      counter++;\n    });\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    return MaterialApp(\n      home: Scaffold(\n        body: Column(\n          mainAxisAlignment: MainAxisAlignment.center,\n          children: &#91;\n            Text('Count: $counter'),\n            ElevatedButton(\n              onPressed: increment,\n              child: Text('Increment'),\n            ),\n          ],\n        ),\n      ),\n    );\n  }\n}<\/code><\/pre>\n\n\n\n<p><strong>\uc7a5\uc810:<\/strong><\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>\ud559\uc2b5 \uace1\uc120\uc774 \ub0ae\uc74c<\/li>\n\n\n\n<li>\ucd94\uac00 \uc758\uc874\uc131 \uc5c6\uc74c<\/li>\n\n\n\n<li>\uac04\ub2e8\ud558\uace0 \uc9c1\uad00\uc801<\/li>\n<\/ul>\n\n\n\n<p><strong>\ub2e8\uc810:<\/strong><\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>\uc0c1\ud0dc \uacf5\uc720\uac00 \uc5b4\ub824\uc6c0<\/li>\n\n\n\n<li>\ubcf5\uc7a1\ud574\uc9c0\uba74 \uad00\ub9ac \uc5b4\ub824\uc6c0<\/li>\n<\/ul>\n\n\n\n<h3 class=\"wp-block-heading\">\ud83c\udfe2 \uc911\uac04 \uaddc\ubaa8 \ud504\ub85c\uc81d\ud2b8 (\ud654\uba74 5-20\uac1c)<\/h3>\n\n\n\n<p><strong>\ucd94\ucc9c: Riverpod<\/strong><\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>\/\/ \uc0ac\uc6a9\uc790 \ubaa9\ub85d \uad00\ub9ac\n@riverpod\nclass UserNotifier extends _$UserNotifier {\n  @override\n  FutureOr&lt;List&lt;User&gt;&gt; build() async {\n    return await ref.read(userRepositoryProvider).getUsers();\n  }\n\n  Future&lt;void&gt; addUser(User user) async {\n    state = const AsyncLoading();\n    state = await AsyncValue.guard(() async {\n      await ref.read(userRepositoryProvider).addUser(user);\n      return await ref.read(userRepositoryProvider).getUsers();\n    });\n  }\n\n  Future&lt;void&gt; deleteUser(String userId) async {\n    state = const AsyncLoading();\n    state = await AsyncValue.guard(() async {\n      await ref.read(userRepositoryProvider).deleteUser(userId);\n      return await ref.read(userRepositoryProvider).getUsers();\n    });\n  }\n}\n\n\/\/ UI\uc5d0\uc11c \uc0ac\uc6a9\nclass UserListScreen extends ConsumerWidget {\n  @override\n  Widget build(BuildContext context, WidgetRef ref) {\n    final usersAsync = ref.watch(userNotifierProvider);\n\n    return Scaffold(\n      appBar: AppBar(title: Text('Users')),\n      body: usersAsync.when(\n        data: (users) =&gt; ListView.builder(\n          itemCount: users.length,\n          itemBuilder: (context, index) {\n            final user = users&#91;index];\n            return ListTile(\n              title: Text(user.name),\n              trailing: IconButton(\n                icon: Icon(Icons.delete),\n                onPressed: () =&gt; ref\n                    .read(userNotifierProvider.notifier)\n                    .deleteUser(user.id),\n              ),\n            );\n          },\n        ),\n        loading: () =&gt; Center(child: CircularProgressIndicator()),\n        error: (error, stack) =&gt; Center(\n          child: Text('Error: $error'),\n        ),\n      ),\n      floatingActionButton: FloatingActionButton(\n        onPressed: () =&gt; _showAddUserDialog(context, ref),\n        child: Icon(Icons.add),\n      ),\n    );\n  }\n}\n\n\/\/ \uc758\uc874\uc131 \uc8fc\uc785\n@riverpod\nUserRepository userRepository(UserRepositoryRef ref) {\n  return UserRepository(ref.read(httpClientProvider));\n}<\/code><\/pre>\n\n\n\n<p><strong>Riverpod \uc120\ud0dd \uc774\uc720:<\/strong><\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li><strong>\ud0c0\uc785 \uc548\uc804\uc131<\/strong>: \ucef4\ud30c\uc77c \ud0c0\uc784 \uc624\ub958 \uac80\ucd9c<\/li>\n\n\n\n<li><strong>\uac1c\ubc1c\uc790 \uacbd\ud5d8<\/strong>: \ub6f0\uc5b4\ub09c \ub514\ubc84\uae45 \ub3c4\uad6c<\/li>\n\n\n\n<li><strong>\ud14c\uc2a4\ud2b8 \uce5c\ud654\uc801<\/strong>: \uc26c\uc6b4 \ubaa8\ud0b9\uacfc \ud14c\uc2a4\ud2b8<\/li>\n\n\n\n<li><strong>\uc131\ub2a5<\/strong>: \ucd5c\uc801\ud654\ub41c \uc7ac\ube4c\ub4dc \uc2dc\uc2a4\ud15c<\/li>\n<\/ul>\n\n\n\n<h3 class=\"wp-block-heading\">\ud83c\udfed \ub300\uaddc\ubaa8 \ud504\ub85c\uc81d\ud2b8 (\ud654\uba74 20\uac1c \uc774\uc0c1)<\/h3>\n\n\n\n<p><strong>\ucd94\ucc9c: BLoC Pattern<\/strong><\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>\/\/ \uc774\ubca4\ud2b8 \uc815\uc758\nabstract class UserEvent extends Equatable {\n  const UserEvent();\n\n  @override\n  List&lt;Object&gt; get props =&gt; &#91;];\n}\n\nclass UserLoadRequested extends UserEvent {}\n\nclass UserAdded extends UserEvent {\n  const UserAdded(this.user);\n\n  final User user;\n\n  @override\n  List&lt;Object&gt; get props =&gt; &#91;user];\n}\n\nclass UserDeleted extends UserEvent {\n  const UserDeleted(this.userId);\n\n  final String userId;\n\n  @override\n  List&lt;Object&gt; get props =&gt; &#91;userId];\n}\n\n\/\/ \uc0c1\ud0dc \uc815\uc758\nabstract class UserState extends Equatable {\n  const UserState();\n\n  @override\n  List&lt;Object&gt; get props =&gt; &#91;];\n}\n\nclass UserInitial extends UserState {}\n\nclass UserLoadInProgress extends UserState {}\n\nclass UserLoadSuccess extends UserState {\n  const UserLoadSuccess(this.users);\n\n  final List&lt;User&gt; users;\n\n  @override\n  List&lt;Object&gt; get props =&gt; &#91;users];\n}\n\nclass UserLoadFailure extends UserState {\n  const UserLoadFailure(this.error);\n\n  final String error;\n\n  @override\n  List&lt;Object&gt; get props =&gt; &#91;error];\n}\n\n\/\/ BLoC \uad6c\ud604\nclass UserBloc extends Bloc&lt;UserEvent, UserState&gt; {\n  UserBloc({required UserRepository userRepository})\n      : _userRepository = userRepository,\n        super(UserInitial()) {\n    on&lt;UserLoadRequested&gt;(_onUserLoadRequested);\n    on&lt;UserAdded&gt;(_onUserAdded);\n    on&lt;UserDeleted&gt;(_onUserDeleted);\n  }\n\n  final UserRepository _userRepository;\n\n  Future&lt;void&gt; _onUserLoadRequested(\n    UserLoadRequested event,\n    Emitter&lt;UserState&gt; emit,\n  ) async {\n    emit(UserLoadInProgress());\n    try {\n      final users = await _userRepository.getUsers();\n      emit(UserLoadSuccess(users));\n    } catch (error) {\n      emit(UserLoadFailure(error.toString()));\n    }\n  }\n\n  Future&lt;void&gt; _onUserAdded(\n    UserAdded event,\n    Emitter&lt;UserState&gt; emit,\n  ) async {\n    try {\n      await _userRepository.addUser(event.user);\n      add(UserLoadRequested());\n    } catch (error) {\n      emit(UserLoadFailure(error.toString()));\n    }\n  }\n\n  Future&lt;void&gt; _onUserDeleted(\n    UserDeleted event,\n    Emitter&lt;UserState&gt; emit,\n  ) async {\n    try {\n      await _userRepository.deleteUser(event.userId);\n      add(UserLoadRequested());\n    } catch (error) {\n      emit(UserLoadFailure(error.toString()));\n    }\n  }\n}\n\n\/\/ UI\uc5d0\uc11c \uc0ac\uc6a9\nclass UserListScreen extends StatelessWidget {\n  @override\n  Widget build(BuildContext context) {\n    return BlocProvider(\n      create: (context) =&gt; UserBloc(\n        userRepository: context.read&lt;UserRepository&gt;(),\n      )..add(UserLoadRequested()),\n      child: Scaffold(\n        appBar: AppBar(title: Text('Users')),\n        body: BlocBuilder&lt;UserBloc, UserState&gt;(\n          builder: (context, state) {\n            if (state is UserLoadInProgress) {\n              return Center(child: CircularProgressIndicator());\n            } else if (state is UserLoadSuccess) {\n              return ListView.builder(\n                itemCount: state.users.length,\n                itemBuilder: (context, index) {\n                  final user = state.users&#91;index];\n                  return ListTile(\n                    title: Text(user.name),\n                    trailing: IconButton(\n                      icon: Icon(Icons.delete),\n                      onPressed: () =&gt; context\n                          .read&lt;UserBloc&gt;()\n                          .add(UserDeleted(user.id)),\n                    ),\n                  );\n                },\n              );\n            } else if (state is UserLoadFailure) {\n              return Center(child: Text('Error: ${state.error}'));\n            }\n            return Container();\n          },\n        ),\n        floatingActionButton: FloatingActionButton(\n          onPressed: () =&gt; _showAddUserDialog(context),\n          child: Icon(Icons.add),\n        ),\n      ),\n    );\n  }\n}<\/code><\/pre>\n\n\n\n<p><strong>BLoC \uc120\ud0dd \uc774\uc720:<\/strong><\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li><strong>\uba85\ud655\ud55c \uc544\ud0a4\ud14d\ucc98<\/strong>: \uc774\ubca4\ud2b8 \uae30\ubc18\uc758 \uc608\uce21 \uac00\ub2a5\ud55c \ud750\ub984<\/li>\n\n\n\n<li><strong>\ud14c\uc2a4\ud2b8 \uc6a9\uc774\uc131<\/strong>: \ube44\uc988\ub2c8\uc2a4 \ub85c\uc9c1\uacfc UI \uc644\uc804 \ubd84\ub9ac<\/li>\n\n\n\n<li><strong>\ud655\uc7a5\uc131<\/strong>: \ub300\uaddc\ubaa8 \ud300 \uac1c\ubc1c\uc5d0 \uc801\ud569<\/li>\n\n\n\n<li><strong>\ub514\ubc84\uae45<\/strong>: \ubaa8\ub4e0 \uc0c1\ud0dc \ubcc0\ud654 \ucd94\uc801 \uac00\ub2a5<\/li>\n<\/ul>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\">\uc2ec\ud654: \uac01 \uc0c1\ud0dc \uad00\ub9ac \ud328\ud134\uc758 \uc2e4\ubb34 \ud65c\uc6a9<\/h2>\n\n\n\n<h3 class=\"wp-block-heading\">Riverpod: \ud604\ub300\uc801 \uc0c1\ud0dc \uad00\ub9ac\uc758 \uc815\uc218<\/h3>\n\n\n\n<h4 class=\"wp-block-heading\">1. Provider \uc885\ub958\ubcc4 \ud65c\uc6a9\ubc95<\/h4>\n\n\n\n<pre class=\"wp-block-code\"><code>\/\/ StateProvider: \uac04\ub2e8\ud55c \uc0c1\ud0dc\nfinal counterProvider = StateProvider&lt;int&gt;((ref) =&gt; 0);\n\n\/\/ FutureProvider: \ube44\ub3d9\uae30 \ub370\uc774\ud130\nfinal userProvider = FutureProvider&lt;User&gt;((ref) async {\n  final userId = ref.watch(currentUserIdProvider);\n  return ref.read(userRepositoryProvider).getUser(userId);\n});\n\n\/\/ StreamProvider: \uc2e4\uc2dc\uac04 \ub370\uc774\ud130\nfinal messagesProvider = StreamProvider&lt;List&lt;Message&gt;&gt;((ref) {\n  final chatId = ref.watch(currentChatIdProvider);\n  return ref.read(messageRepositoryProvider).watchMessages(chatId);\n});\n\n\/\/ NotifierProvider: \ubcf5\uc7a1\ud55c \uc0c1\ud0dc \ub85c\uc9c1\n@riverpod\nclass ShoppingCart extends _$ShoppingCart {\n  @override\n  List&lt;CartItem&gt; build() =&gt; &#91;];\n\n  void addItem(Product product) {\n    final existingIndex = state.indexWhere((item) =&gt; item.product.id == product.id);\n\n    if (existingIndex &gt;= 0) {\n      state = &#91;\n        ...state.sublist(0, existingIndex),\n        state&#91;existingIndex].copyWith(quantity: state&#91;existingIndex].quantity + 1),\n        ...state.sublist(existingIndex + 1),\n      ];\n    } else {\n      state = &#91;...state, CartItem(product: product, quantity: 1)];\n    }\n  }\n\n  void removeItem(String productId) {\n    state = state.where((item) =&gt; item.product.id != productId).toList();\n  }\n\n  double get totalPrice =&gt; state.fold(\n    0.0,\n    (sum, item) =&gt; sum + (item.product.price * item.quantity),\n  );\n}<\/code><\/pre>\n\n\n\n<h4 class=\"wp-block-heading\">2. \uc758\uc874\uc131 \uc8fc\uc785\uacfc \ud14c\uc2a4\ud2b8<\/h4>\n\n\n\n<pre class=\"wp-block-code\"><code>\/\/ \ub9ac\ud3ec\uc9c0\ud1a0\ub9ac \ud504\ub85c\ubc14\uc774\ub354\n@riverpod\nUserRepository userRepository(UserRepositoryRef ref) {\n  return UserRepository(\n    httpClient: ref.read(httpClientProvider),\n    localDatabase: ref.read(localDatabaseProvider),\n  );\n}\n\n\/\/ \ud14c\uc2a4\ud2b8\uc5d0\uc11c \uc624\ubc84\ub77c\uc774\ub4dc\nvoid main() {\n  testWidgets('user list displays users', (tester) async {\n    await tester.pumpWidget(\n      ProviderScope(\n        overrides: &#91;\n          userRepositoryProvider.overrideWithValue(\n            MockUserRepository(), \/\/ \ubaa9 \uac1d\uccb4\ub85c \ub300\uccb4\n          ),\n        ],\n        child: UserListScreen(),\n      ),\n    );\n\n    \/\/ \ud14c\uc2a4\ud2b8 \ub85c\uc9c1...\n  });\n}<\/code><\/pre>\n\n\n\n<h4 class=\"wp-block-heading\">3. \uc0c1\ud0dc \uc870\ud569\uacfc \ucd5c\uc801\ud654<\/h4>\n\n\n\n<pre class=\"wp-block-code\"><code>\/\/ \uc5ec\ub7ec \uc0c1\ud0dc\ub97c \uc870\ud569\nfinal searchResultsProvider = Provider&lt;List&lt;User&gt;&gt;((ref) {\n  final users = ref.watch(usersProvider).value ?? &#91;];\n  final searchQuery = ref.watch(searchQueryProvider);\n\n  if (searchQuery.isEmpty) return users;\n\n  return users.where((user) =&gt; \n    user.name.toLowerCase().contains(searchQuery.toLowerCase())\n  ).toList();\n});\n\n\/\/ \uc120\ud0dd\uc801 \ub9ac\ube4c\ub4dc\nclass UserTile extends ConsumerWidget {\n  const UserTile({required this.userId, Key? key}) : super(key: key);\n\n  final String userId;\n\n  @override\n  Widget build(BuildContext context, WidgetRef ref) {\n    \/\/ \ud2b9\uc815 \uc0ac\uc6a9\uc790\ub9cc watch\n    final user = ref.watch(userProvider(userId)).value;\n\n    if (user == null) return SizedBox.shrink();\n\n    return ListTile(\n      title: Text(user.name),\n      subtitle: Text(user.email),\n    );\n  }\n}<\/code><\/pre>\n\n\n\n<h3 class=\"wp-block-heading\">GetX: \ube60\ub978 \uac1c\ubc1c\uc744 \uc704\ud55c \uc62c\uc778\uc6d0 \uc194\ub8e8\uc158<\/h3>\n\n\n\n<pre class=\"wp-block-code\"><code>\/\/ GetX Controller\nclass UserController extends GetxController {\n  \/\/ \ubc18\uc751\ud615 \ubcc0\uc218\n  final users = &lt;User&gt;&#91;].obs;\n  final isLoading = false.obs;\n  final selectedUser = Rx&lt;User?&gt;(null);\n\n  \/\/ \uc758\uc874\uc131 \uc8fc\uc785\n  final UserRepository _repository = Get.find&lt;UserRepository&gt;();\n\n  @override\n  void onInit() {\n    super.onInit();\n    loadUsers();\n  }\n\n  Future&lt;void&gt; loadUsers() async {\n    try {\n      isLoading.value = true;\n      final userList = await _repository.getUsers();\n      users.assignAll(userList);\n    } catch (e) {\n      Get.snackbar('Error', 'Failed to load users');\n    } finally {\n      isLoading.value = false;\n    }\n  }\n\n  void selectUser(User user) {\n    selectedUser.value = user;\n    Get.toNamed('\/user-detail', arguments: user);\n  }\n\n  \/\/ \uacc4\uc0b0\ub41c \uac12\n  List&lt;User&gt; get activeUsers =&gt; users.where((user) =&gt; user.isActive).toList();\n\n  \/\/ \uc6cc\ucee4 (\ubd80\uc791\uc6a9 \ucc98\ub9ac)\n  @override\n  void onReady() {\n    super.onReady();\n\n    \/\/ \uc0ac\uc6a9\uc790\uac00 \uc120\ud0dd\ub420 \ub54c\ub9c8\ub2e4 \ubd84\uc11d \uc774\ubca4\ud2b8 \uc804\uc1a1\n    ever(selectedUser, (User? user) {\n      if (user != null) {\n        Analytics.track('user_selected', {'userId': user.id});\n      }\n    });\n\n    \/\/ \uc0ac\uc6a9\uc790 \ubaa9\ub85d\uc774 \ube44\uc5b4\uc788\uc73c\uba74 \uacbd\uace0 \ud45c\uc2dc\n    ever(users, (List&lt;User&gt; userList) {\n      if (userList.isEmpty) {\n        Get.dialog(AlertDialog(\n          title: Text('No Users'),\n          content: Text('No users found. Please add some users.'),\n        ));\n      }\n    });\n  }\n}\n\n\/\/ UI\uc5d0\uc11c \uc0ac\uc6a9\nclass UserListScreen extends GetView&lt;UserController&gt; {\n  @override\n  Widget build(BuildContext context) {\n    return Scaffold(\n      appBar: AppBar(title: Text('Users')),\n      body: Obx(() {\n        if (controller.isLoading.value) {\n          return Center(child: CircularProgressIndicator());\n        }\n\n        return ListView.builder(\n          itemCount: controller.users.length,\n          itemBuilder: (context, index) {\n            final user = controller.users&#91;index];\n            return ListTile(\n              title: Text(user.name),\n              onTap: () =&gt; controller.selectUser(user),\n            );\n          },\n        );\n      }),\n      floatingActionButton: FloatingActionButton(\n        onPressed: controller.loadUsers,\n        child: Icon(Icons.refresh),\n      ),\n    );\n  }\n}\n\n\/\/ \ub77c\uc6b0\ud305\uacfc \uc758\uc874\uc131 \uc8fc\uc785\nclass AppPages {\n  static final routes = &#91;\n    GetPage(\n      name: '\/users',\n      page: () =&gt; UserListScreen(),\n      binding: UserBinding(), \/\/ \uc758\uc874\uc131 \ubc14\uc778\ub529\n    ),\n    GetPage(\n      name: '\/user-detail',\n      page: () =&gt; UserDetailScreen(),\n    ),\n  ];\n}\n\nclass UserBinding extends Bindings {\n  @override\n  void dependencies() {\n    Get.lazyPut&lt;UserRepository&gt;(() =&gt; UserRepositoryImpl());\n    Get.lazyPut&lt;UserController&gt;(() =&gt; UserController());\n  }\n}<\/code><\/pre>\n\n\n\n<p><strong>GetX \uc7a5\uc810:<\/strong><\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li><strong>\ube60\ub978 \uac1c\ubc1c<\/strong>: \uc0c1\ud0dc \uad00\ub9ac + \ub77c\uc6b0\ud305 + \uc758\uc874\uc131 \uc8fc\uc785 \ud1b5\ud569<\/li>\n\n\n\n<li><strong>\uac04\ub2e8\ud55c \ubb38\ubc95<\/strong>: \ubc18\uc751\ud615 \ubcc0\uc218\uc640 Obx \uc704\uc82f<\/li>\n\n\n\n<li><strong>\uc131\ub2a5<\/strong>: \uc2a4\ub9c8\ud2b8\ud55c \ub9ac\ube4c\ub4dc \uc2dc\uc2a4\ud15c<\/li>\n<\/ul>\n\n\n\n<p><strong>GetX \ub2e8\uc810:<\/strong><\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li><strong>\uac15\ud55c \uacb0\ud569<\/strong>: GetX\uc5d0 \uc885\uc18d\uc801<\/li>\n\n\n\n<li><strong>\ub514\ubc84\uae45 \uc5b4\ub824\uc6c0<\/strong>: \ub9e4\uc9c1 \uac19\uc740 \ub3d9\uc791\uc73c\ub85c \ucd94\uc801 \uc5b4\ub824\uc6c0<\/li>\n\n\n\n<li><strong>\ud14c\uc2a4\ud2b8 \ubcf5\uc7a1\uc131<\/strong>: \uae00\ub85c\ubc8c \uc0c1\ud0dc\ub85c \uc778\ud55c \ud14c\uc2a4\ud2b8 \uaca9\ub9ac \uc5b4\ub824\uc6c0<\/li>\n<\/ul>\n\n\n\n<h3 class=\"wp-block-heading\">BLoC: \uc5d4\ud130\ud504\ub77c\uc774\uc988\uae09 \uc544\ud0a4\ud14d\ucc98<\/h3>\n\n\n\n<h4 class=\"wp-block-heading\">1. \ubcf5\uc7a1\ud55c \ube44\uc988\ub2c8\uc2a4 \ub85c\uc9c1 \ucc98\ub9ac<\/h4>\n\n\n\n<pre class=\"wp-block-code\"><code>\/\/ \ubcf5\uc7a1\ud55c \uac80\uc0c9 \ub85c\uc9c1\uc744 \uac00\uc9c4 BLoC\nclass ProductSearchBloc extends Bloc&lt;ProductSearchEvent, ProductSearchState&gt; {\n  ProductSearchBloc({\n    required ProductRepository productRepository,\n    required UserPreferencesRepository preferencesRepository,\n  }) : _productRepository = productRepository,\n       _preferencesRepository = preferencesRepository,\n       super(ProductSearchInitial()) {\n\n    on&lt;ProductSearchQueryChanged&gt;(_onQueryChanged);\n    on&lt;ProductSearchFiltersChanged&gt;(_onFiltersChanged);\n    on&lt;ProductSearchRequested&gt;(_onSearchRequested);\n    on&lt;ProductSearchSortChanged&gt;(_onSortChanged);\n  }\n\n  final ProductRepository _productRepository;\n  final UserPreferencesRepository _preferencesRepository;\n\n  \/\/ \ub514\ubc14\uc6b4\uc2f1\uc744 \ud1b5\ud55c \uac80\uc0c9 \ucd5c\uc801\ud654\n  Timer? _debounceTimer;\n\n  Future&lt;void&gt; _onQueryChanged(\n    ProductSearchQueryChanged event,\n    Emitter&lt;ProductSearchState&gt; emit,\n  ) async {\n    _debounceTimer?.cancel();\n\n    final currentState = state;\n    if (currentState is ProductSearchSuccess) {\n      emit(currentState.copyWith(query: event.query));\n    }\n\n    _debounceTimer = Timer(Duration(milliseconds: 500), () {\n      add(ProductSearchRequested());\n    });\n  }\n\n  Future&lt;void&gt; _onFiltersChanged(\n    ProductSearchFiltersChanged event,\n    Emitter&lt;ProductSearchState&gt; emit,\n  ) async {\n    final currentState = state;\n    if (currentState is ProductSearchSuccess) {\n      emit(currentState.copyWith(filters: event.filters));\n      add(ProductSearchRequested());\n    }\n  }\n\n  Future&lt;void&gt; _onSearchRequested(\n    ProductSearchRequested event,\n    Emitter&lt;ProductSearchState&gt; emit,\n  ) async {\n    try {\n      final currentState = state;\n\n      if (currentState is ProductSearchSuccess) {\n        emit(currentState.copyWith(isLoading: true));\n      } else {\n        emit(ProductSearchLoading());\n      }\n\n      final searchParams = _buildSearchParams(currentState);\n      final products = await _productRepository.searchProducts(searchParams);\n\n      \/\/ \uc0ac\uc6a9\uc790 \uc120\ud638\ub3c4\uc5d0 \ub530\ub978 \uc815\ub82c\n      final userPreferences = await _preferencesRepository.getPreferences();\n      final sortedProducts = _sortProducts(products, userPreferences);\n\n      emit(ProductSearchSuccess(\n        products: sortedProducts,\n        query: searchParams.query,\n        filters: searchParams.filters,\n        sortOption: searchParams.sortOption,\n        hasMore: products.length &gt;= searchParams.limit,\n      ));\n\n    } catch (error) {\n      emit(ProductSearchFailure(error.toString()));\n    }\n  }\n\n  SearchParams _buildSearchParams(ProductSearchState currentState) {\n    \/\/ \ud604\uc7ac \uc0c1\ud0dc\uc5d0\uc11c \uac80\uc0c9 \ud30c\ub77c\ubbf8\ud130 \uad6c\uc131\n    return SearchParams(\n      query: currentState is ProductSearchSuccess ? currentState.query : '',\n      filters: currentState is ProductSearchSuccess ? currentState.filters : {},\n      sortOption: currentState is ProductSearchSuccess ? currentState.sortOption : SortOption.relevance,\n      limit: 20,\n    );\n  }\n\n  List&lt;Product&gt; _sortProducts(List&lt;Product&gt; products, UserPreferences preferences) {\n    \/\/ \uc0ac\uc6a9\uc790 \uc120\ud638\ub3c4 \uae30\ubc18 \uc815\ub82c \ub85c\uc9c1\n    return products..sort((a, b) {\n      \/\/ \ubcf5\uc7a1\ud55c \uc815\ub82c \ub85c\uc9c1...\n      return a.relevanceScore.compareTo(b.relevanceScore);\n    });\n  }\n\n  @override\n  Future&lt;void&gt; close() {\n    _debounceTimer?.cancel();\n    return super.close();\n  }\n}<\/code><\/pre>\n\n\n\n<h4 class=\"wp-block-heading\">2. \uba40\ud2f0 BLoC \ud1b5\uc2e0<\/h4>\n\n\n\n<pre class=\"wp-block-code\"><code>\/\/ \uc8fc\ubb38 \uacfc\uc815\uc5d0\uc11c \uc5ec\ub7ec BLoC \uac04 \ud1b5\uc2e0\nclass CheckoutBloc extends Bloc&lt;CheckoutEvent, CheckoutState&gt; {\n  CheckoutBloc({\n    required this.cartBloc,\n    required this.userBloc,\n    required this.paymentBloc,\n  }) : super(CheckoutInitial()) {\n\n    \/\/ \ub2e4\ub978 BLoC\uc758 \uc0c1\ud0dc \ubcc0\ud654 \uac10\uc9c0\n    _cartSubscription = cartBloc.stream.listen((cartState) {\n      if (cartState is CartUpdated) {\n        add(CheckoutCartUpdated(cartState.items));\n      }\n    });\n\n    _userSubscription = userBloc.stream.listen((userState) {\n      if (userState is UserAddressUpdated) {\n        add(CheckoutAddressUpdated(userState.address));\n      }\n    });\n\n    on&lt;CheckoutStarted&gt;(_onCheckoutStarted);\n    on&lt;CheckoutCartUpdated&gt;(_onCartUpdated);\n    on&lt;CheckoutAddressUpdated&gt;(_onAddressUpdated);\n    on&lt;CheckoutPaymentRequested&gt;(_onPaymentRequested);\n  }\n\n  final CartBloc cartBloc;\n  final UserBloc userBloc;\n  final PaymentBloc paymentBloc;\n\n  StreamSubscription? _cartSubscription;\n  StreamSubscription? _userSubscription;\n\n  Future&lt;void&gt; _onPaymentRequested(\n    CheckoutPaymentRequested event,\n    Emitter&lt;CheckoutState&gt; emit,\n  ) async {\n    emit(CheckoutProcessing());\n\n    try {\n      \/\/ \uacb0\uc81c \ucc98\ub9ac\n      paymentBloc.add(PaymentProcessRequested(\n        amount: event.amount,\n        paymentMethod: event.paymentMethod,\n      ));\n\n      \/\/ \uacb0\uc81c \uacb0\uacfc \ub300\uae30\n      await for (final paymentState in paymentBloc.stream) {\n        if (paymentState is PaymentSuccess) {\n          \/\/ \uc8fc\ubb38 \uc644\ub8cc \ucc98\ub9ac\n          emit(CheckoutSuccess(paymentState.transactionId));\n          break;\n        } else if (paymentState is PaymentFailure) {\n          emit(CheckoutFailure(paymentState.error));\n          break;\n        }\n      }\n    } catch (error) {\n      emit(CheckoutFailure(error.toString()));\n    }\n  }\n\n  @override\n  Future&lt;void&gt; close() {\n    _cartSubscription?.cancel();\n    _userSubscription?.cancel();\n    return super.close();\n  }\n}<\/code><\/pre>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\">\uc2e4\ubb34 \ud504\ub85c\uc81d\ud2b8 \uc544\ud0a4\ud14d\ucc98 \ud328\ud134<\/h2>\n\n\n\n<h3 class=\"wp-block-heading\">\ud074\ub9b0 \uc544\ud0a4\ud14d\ucc98 + Riverpod<\/h3>\n\n\n\n<pre class=\"wp-block-code\"><code>\/\/ Domain Layer\nabstract class UserRepository {\n  Future&lt;List&lt;User&gt;&gt; getUsers();\n  Future&lt;User&gt; getUser(String id);\n  Future&lt;void&gt; createUser(User user);\n  Future&lt;void&gt; updateUser(User user);\n  Future&lt;void&gt; deleteUser(String id);\n}\n\nclass GetUsersUseCase {\n  GetUsersUseCase(this._repository);\n\n  final UserRepository _repository;\n\n  Future&lt;List&lt;User&gt;&gt; call() async {\n    try {\n      return await _repository.getUsers();\n    } catch (e) {\n      throw UserException('Failed to load users: $e');\n    }\n  }\n}\n\n\/\/ Infrastructure Layer\nclass UserRepositoryImpl implements UserRepository {\n  UserRepositoryImpl({\n    required this.remoteDataSource,\n    required this.localDataSource,\n  });\n\n  final UserRemoteDataSource remoteDataSource;\n  final UserLocalDataSource localDataSource;\n\n  @override\n  Future&lt;List&lt;User&gt;&gt; getUsers() async {\n    try {\n      final users = await remoteDataSource.getUsers();\n      await localDataSource.cacheUsers(users);\n      return users;\n    } catch (e) {\n      \/\/ \ub124\ud2b8\uc6cc\ud06c \uc624\ub958 \uc2dc \uce90\uc2dc\ub41c \ub370\uc774\ud130 \ubc18\ud658\n      return await localDataSource.getCachedUsers();\n    }\n  }\n}\n\n\/\/ Presentation Layer (Riverpod)\n@riverpod\nUserRepository userRepository(UserRepositoryRef ref) {\n  return UserRepositoryImpl(\n    remoteDataSource: ref.read(userRemoteDataSourceProvider),\n    localDataSource: ref.read(userLocalDataSourceProvider),\n  );\n}\n\n@riverpod\nGetUsersUseCase getUsersUseCase(GetUsersUseCaseRef ref) {\n  return GetUsersUseCase(ref.read(userRepositoryProvider));\n}\n\n@riverpod\nclass UserNotifier extends _$UserNotifier {\n  @override\n  FutureOr&lt;List&lt;User&gt;&gt; build() {\n    return ref.read(getUsersUseCaseProvider)();\n  }\n\n  Future&lt;void&gt; refresh() async {\n    state = const AsyncLoading();\n    state = await AsyncValue.guard(() =&gt; \n      ref.read(getUsersUseCaseProvider)()\n    );\n  }\n}<\/code><\/pre>\n\n\n\n<h3 class=\"wp-block-heading\">MVVM + GetX<\/h3>\n\n\n\n<pre class=\"wp-block-code\"><code>\/\/ Model\nclass User {\n  final String id;\n  final String name;\n  final String email;\n\n  User({required this.id, required this.name, required this.email});\n}\n\n\/\/ Repository\nabstract class UserRepository {\n  Future&lt;List&lt;User&gt;&gt; getUsers();\n  Future&lt;void&gt; createUser(User user);\n}\n\nclass UserRepositoryImpl implements UserRepository {\n  final ApiService _apiService;\n\n  UserRepositoryImpl(this._apiService);\n\n  @override\n  Future&lt;List&lt;User&gt;&gt; getUsers() =&gt; _apiService.getUsers();\n\n  @override\n  Future&lt;void&gt; createUser(User user) =&gt; _apiService.createUser(user);\n}\n\n\/\/ ViewModel (Controller)\nclass UserViewModel extends GetxController {\n  final UserRepository _repository;\n\n  UserViewModel(this._repository);\n\n  \/\/ Observable state\n  final users = &lt;User&gt;&#91;].obs;\n  final isLoading = false.obs;\n  final error = RxnString();\n\n  @override\n  void onInit() {\n    super.onInit();\n    loadUsers();\n  }\n\n  Future&lt;void&gt; loadUsers() async {\n    try {\n      isLoading.value = true;\n      error.value = null;\n\n      final userList = await _repository.getUsers();\n      users.assignAll(userList);\n    } catch (e) {\n      error.value = e.toString();\n    } finally {\n      isLoading.value = false;\n    }\n  }\n\n  Future&lt;void&gt; addUser(User user) async {\n    try {\n      await _repository.createUser(user);\n      users.add(user);\n      Get.snackbar('Success', 'User added successfully');\n    } catch (e) {\n      Get.snackbar('Error', 'Failed to add user: $e');\n    }\n  }\n}\n\n\/\/ View\nclass UserListView extends GetView&lt;UserViewModel&gt; {\n  @override\n  Widget build(BuildContext context) {\n    return Scaffold(\n      appBar: AppBar(title: Text('Users')),\n      body: Obx(() {\n        if (controller.isLoading.value) {\n          return Center(child: CircularProgressIndicator());\n        }\n\n        if (controller.error.value != null) {\n          return Center(\n            child: Column(\n              mainAxisAlignment: MainAxisAlignment.center,\n              children: &#91;\n                Text('Error: ${controller.error.value}'),\n                ElevatedButton(\n                  onPressed: controller.loadUsers,\n                  child: Text('Retry'),\n                ),\n              ],\n            ),\n          );\n        }\n\n        return ListView.builder(\n          itemCount: controller.users.length,\n          itemBuilder: (context, index) {\n            final user = controller.users&#91;index];\n            return ListTile(\n              title: Text(user.name),\n              subtitle: Text(user.email),\n            );\n          },\n        );\n      }),\n    );\n  }\n}\n\n\/\/ Dependency Injection\nclass UserBinding extends Bindings {\n  @override\n  void dependencies() {\n    Get.lazyPut&lt;ApiService&gt;(() =&gt; ApiServiceImpl());\n    Get.lazyPut&lt;UserRepository&gt;(() =&gt; UserRepositoryImpl(Get.find()));\n    Get.lazyPut&lt;UserViewModel&gt;(() =&gt; UserViewModel(Get.find()));\n  }\n}<\/code><\/pre>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\">\uc131\ub2a5 \ucd5c\uc801\ud654\uc640 \ubca0\uc2a4\ud2b8 \ud504\ub799\ud2f0\uc2a4<\/h2>\n\n\n\n<h3 class=\"wp-block-heading\">1. \ubd88\ud544\uc694\ud55c \uc7ac\ube4c\ub4dc \ubc29\uc9c0<\/h3>\n\n\n\n<pre class=\"wp-block-code\"><code>\/\/ \u274c \uc798\ubabb\ub41c \uc608: \uc804\uccb4 \ud654\uba74\uc774 \uc7ac\ube4c\ub4dc\ub428\nclass BadCounterScreen extends ConsumerWidget {\n  @override\n  Widget build(BuildContext context, WidgetRef ref) {\n    final counter = ref.watch(counterProvider);\n\n    return Scaffold(\n      appBar: AppBar(title: Text('Counter')),\n      body: Column(\n        children: &#91;\n          ExpensiveWidget(), \/\/ counter \ubcc0\uacbd \uc2dc \ubd88\ud544\uc694\ud558\uac8c \uc7ac\ube4c\ub4dc\ub428\n          Text('Count: $counter'),\n          AnotherExpensiveWidget(), \/\/ \uc774\uac83\ub3c4 \uc7ac\ube4c\ub4dc\ub428\n        ],\n      ),\n    );\n  }\n}\n\n\/\/ \u2705 \uc62c\ubc14\ub978 \uc608: \ud544\uc694\ud55c \ubd80\ubd84\ub9cc \uc7ac\ube4c\ub4dc\nclass GoodCounterScreen extends StatelessWidget {\n  @override\n  Widget build(BuildContext context) {\n    return Scaffold(\n      appBar: AppBar(title: Text('Counter')),\n      body: Column(\n        children: &#91;\n          ExpensiveWidget(), \/\/ \uc7ac\ube4c\ub4dc\ub418\uc9c0 \uc54a\uc74c\n          Consumer(\n            builder: (context, ref, child) {\n              final counter = ref.watch(counterProvider);\n              return Text('Count: $counter'); \/\/ \uc774 \ubd80\ubd84\ub9cc \uc7ac\ube4c\ub4dc\n            },\n          ),\n          AnotherExpensiveWidget(), \/\/ \uc7ac\ube4c\ub4dc\ub418\uc9c0 \uc54a\uc74c\n        ],\n      ),\n    );\n  }\n}<\/code><\/pre>\n\n\n\n<h3 class=\"wp-block-heading\">2. \uc0c1\ud0dc \uc120\ud0dd\uc801 \uad6c\ub3c5<\/h3>\n\n\n\n<pre class=\"wp-block-code\"><code>\/\/ \ud070 \uac1d\uccb4\uc5d0\uc11c \ud2b9\uc815 \ud544\ub4dc\ub9cc \uad6c\ub3c5\n@riverpod\nclass UserProfile extends _$UserProfile {\n  @override\n  UserProfileState build() {\n    return UserProfileState(\n      name: '',\n      email: '',\n      avatar: '',\n      preferences: UserPreferences.empty(),\n    );\n  }\n\n  void updateName(String name) {\n    state = state.copyWith(name: name);\n  }\n\n  void updateEmail(String email) {\n    state = state.copyWith(email: email);\n  }\n}\n\n\/\/ \uc774\ub984\ub9cc \ubcc0\uacbd\ub418\uc5b4\ub3c4 \uc804\uccb4\uac00 \uc7ac\ube4c\ub4dc\ub418\ub294 \ubb38\uc81c \ud574\uacb0\nclass UserNameWidget extends ConsumerWidget {\n  @override\n  Widget build(BuildContext context, WidgetRef ref) {\n    \/\/ \uc774\ub984\ub9cc \uc120\ud0dd\uc801\uc73c\ub85c \uad6c\ub3c5\n    final name = ref.watch(userProfileProvider.select((user) =&gt; user.name));\n\n    return Text(name);\n  }\n}\n\nclass UserEmailWidget extends ConsumerWidget {\n  @override\n  Widget build(BuildContext context, WidgetRef ref) {\n    \/\/ \uc774\uba54\uc77c\ub9cc \uc120\ud0dd\uc801\uc73c\ub85c \uad6c\ub3c5\n    final email = ref.watch(userProfileProvider.select((user) =&gt; user.email));\n\n    return Text(email);\n  }\n}<\/code><\/pre>\n\n\n\n<h3 class=\"wp-block-heading\">3. \ube44\ub3d9\uae30 \uc0c1\ud0dc \ucc98\ub9ac \ud328\ud134<\/h3>\n\n\n\n<pre class=\"wp-block-code\"><code>\/\/ \ub85c\ub529, \uc5d0\ub7ec, \uc131\uacf5 \uc0c1\ud0dc\ub97c \uc6b0\uc544\ud558\uac8c \ucc98\ub9ac\n@riverpod\nclass ProductList extends _$ProductList {\n  @override\n  FutureOr&lt;List&lt;Product&gt;&gt; build() async {\n    \/\/ \uce90\uc2dc\ub41c \ub370\uc774\ud130\uac00 \uc788\uc73c\uba74 \uc989\uc2dc \ubc18\ud658\n    final cached = ref.read(productCacheProvider);\n    if (cached.isNotEmpty) {\n      \/\/ \ubc31\uadf8\ub77c\uc6b4\ub4dc\uc5d0\uc11c \uc0c8 \ub370\uc774\ud130 \ub85c\ub4dc\n      _loadInBackground();\n      return cached;\n    }\n\n    return await _loadProducts();\n  }\n\n  Future&lt;void&gt; refresh() async {\n    state = const AsyncLoading();\n    state = await AsyncValue.guard(() =&gt; _loadProducts());\n  }\n\n  Future&lt;List&lt;Product&gt;&gt; _loadProducts() async {\n    final products = await ref.read(productRepositoryProvider).getProducts();\n    ref.read(productCacheProvider.notifier).cache(products);\n    return products;\n  }\n\n  void _loadInBackground() async {\n    try {\n      final products = await _loadProducts();\n      state = AsyncData(products);\n    } catch (e) {\n      \/\/ \ubc31\uadf8\ub77c\uc6b4\ub4dc \ub85c\ub529 \uc2e4\ud328\ub294 \ubb34\uc2dc (\uce90\uc2dc\ub41c \ub370\uc774\ud130 \uc720\uc9c0)\n    }\n  }\n}\n\n\/\/ UI\uc5d0\uc11c \uc0ac\uc6a9\nclass ProductListView extends ConsumerWidget {\n  @override\n  Widget build(BuildContext context, WidgetRef ref) {\n    final productsAsync = ref.watch(productListProvider);\n\n    return Scaffold(\n      appBar: AppBar(\n        title: Text('Products'),\n        actions: &#91;\n          IconButton(\n            icon: Icon(Icons.refresh),\n            onPressed: () =&gt; ref.refresh(productListProvider),\n          ),\n        ],\n      ),\n      body: productsAsync.when(\n        data: (products) =&gt; RefreshIndicator(\n          onRefresh: () =&gt; ref.refresh(productListProvider),\n          child: ListView.builder(\n            itemCount: products.length,\n            itemBuilder: (context, index) =&gt; ProductTile(products&#91;index]),\n          ),\n        ),\n        loading: () =&gt; Center(child: CircularProgressIndicator()),\n        error: (error, stack) =&gt; ErrorWidget(\n          error: error,\n          onRetry: () =&gt; ref.refresh(productListProvider),\n        ),\n      ),\n    );\n  }\n}<\/code><\/pre>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\">\ud14c\uc2a4\ud2b8 \uc804\ub7b5<\/h2>\n\n\n\n<h3 class=\"wp-block-heading\">Riverpod \ud14c\uc2a4\ud2b8<\/h3>\n\n\n\n<pre class=\"wp-block-code\"><code>\/\/ \ud14c\uc2a4\ud2b8\uc6a9 \uac00\uc9dc \ub9ac\ud3ec\uc9c0\ud1a0\ub9ac\nclass FakeUserRepository implements UserRepository {\n  List&lt;User&gt; _users = &#91;\n    User(id: '1', name: 'John', email: 'john@example.com'),\n    User(id: '2', name: 'Jane', email: 'jane@example.com'),\n  ];\n\n  @override\n  Future&lt;List&lt;User&gt;&gt; getUsers() async {\n    await Future.delayed(Duration(milliseconds: 100)); \/\/ \ub124\ud2b8\uc6cc\ud06c \uc9c0\uc5f0 \uc2dc\ubbac\ub808\uc774\uc158\n    return _users;\n  }\n\n  @override\n  Future&lt;void&gt; addUser(User user) async {\n    _users.add(user);\n  }\n\n  @override\n  Future&lt;void&gt; deleteUser(String id) async {\n    _users.removeWhere((user) =&gt; user.id == id);\n  }\n}\n\n\/\/ \uc720\ub2db \ud14c\uc2a4\ud2b8\nvoid main() {\n  group('UserNotifier', () {\n    late ProviderContainer container;\n    late FakeUserRepository fakeRepository;\n\n    setUp(() {\n      fakeRepository = FakeUserRepository();\n      container = ProviderContainer(\n        overrides: &#91;\n          userRepositoryProvider.overrideWithValue(fakeRepository),\n        ],\n      );\n    });\n\n    tearDown(() {\n      container.dispose();\n    });\n\n    test('should load users successfully', () async {\n      final notifier = container.read(userNotifierProvider.notifier);\n      final users = await container.read(userNotifierProvider.future);\n\n      expect(users.length, equals(2));\n      expect(users&#91;0].name, equals('John'));\n    });\n\n    test('should add user successfully', () async {\n      final notifier = container.read(userNotifierProvider.notifier);\n\n      await notifier.addUser(User(\n        id: '3',\n        name: 'Bob',\n        email: 'bob@example.com',\n      ));\n\n      final users = await container.read(userNotifierProvider.future);\n      expect(users.length, equals(3));\n      expect(users.last.name, equals('Bob'));\n    });\n\n    test('should handle repository errors', () async {\n      \/\/ \uc5d0\ub7ec\ub97c \ubc1c\uc0dd\uc2dc\ud0a4\ub294 \ub9ac\ud3ec\uc9c0\ud1a0\ub9ac\ub85c \uad50\uccb4\n      container = ProviderContainer(\n        overrides: &#91;\n          userRepositoryProvider.overrideWithValue(\n            ErrorUserRepository(), \/\/ \ud56d\uc0c1 \uc5d0\ub7ec\ub97c \ubc1c\uc0dd\uc2dc\ud0a4\ub294 \uad6c\ud604\n          ),\n        ],\n      );\n\n      final asyncValue = container.read(userNotifierProvider);\n      expect(asyncValue.hasError, isTrue);\n    });\n  });\n}\n\n\/\/ \uc704\uc82f \ud14c\uc2a4\ud2b8\nvoid main() {\n  group('UserListScreen', () {\n    testWidgets('displays loading indicator initially', (tester) async {\n      await tester.pumpWidget(\n        ProviderScope(\n          overrides: &#91;\n            userRepositoryProvider.overrideWithValue(\n              SlowUserRepository(), \/\/ \ub290\ub9b0 \uc751\ub2f5\uc744 \uc2dc\ubbac\ub808\uc774\uc158\n            ),\n          ],\n          child: MaterialApp(home: UserListScreen()),\n        ),\n      );\n\n      expect(find.byType(CircularProgressIndicator), findsOneWidget);\n    });\n\n    testWidgets('displays users after loading', (tester) async {\n      await tester.pumpWidget(\n        ProviderScope(\n          overrides: &#91;\n            userRepositoryProvider.overrideWithValue(FakeUserRepository()),\n          ],\n          child: MaterialApp(home: UserListScreen()),\n        ),\n      );\n\n      \/\/ \ub85c\ub529 \uc644\ub8cc\uae4c\uc9c0 \ub300\uae30\n      await tester.pumpAndSettle();\n\n      expect(find.text('John'), findsOneWidget);\n      expect(find.text('Jane'), findsOneWidget);\n    });\n\n    testWidgets('can add new user', (tester) async {\n      await tester.pumpWidget(\n        ProviderScope(\n          overrides: &#91;\n            userRepositoryProvider.overrideWithValue(FakeUserRepository()),\n          ],\n          child: MaterialApp(home: UserListScreen()),\n        ),\n      );\n\n      await tester.pumpAndSettle();\n\n      \/\/ \ucd94\uac00 \ubc84\ud2bc \ud0ed\n      await tester.tap(find.byIcon(Icons.add));\n      await tester.pumpAndSettle();\n\n      \/\/ \uc0ac\uc6a9\uc790 \uc815\ubcf4 \uc785\ub825\n      await tester.enterText(find.byKey(Key('name_field')), 'Bob');\n      await tester.enterText(find.byKey(Key('email_field')), 'bob@example.com');\n\n      \/\/ \uc800\uc7a5 \ubc84\ud2bc \ud0ed\n      await tester.tap(find.text('Save'));\n      await tester.pumpAndSettle();\n\n      \/\/ \uc0c8 \uc0ac\uc6a9\uc790\uac00 \ubaa9\ub85d\uc5d0 \ucd94\uac00\ub418\uc5c8\ub294\uc9c0 \ud655\uc778\n      expect(find.text('Bob'), findsOneWidget);\n    });\n  });\n}<\/code><\/pre>\n\n\n\n<h3 class=\"wp-block-heading\">BLoC \ud14c\uc2a4\ud2b8<\/h3>\n\n\n\n<pre class=\"wp-block-code\"><code>\/\/ BLoC \ud14c\uc2a4\ud2b8\nvoid main() {\n  group('UserBloc', () {\n    late UserBloc userBloc;\n    late MockUserRepository mockRepository;\n\n    setUp(() {\n      mockRepository = MockUserRepository();\n      userBloc = UserBloc(userRepository: mockRepository);\n    });\n\n    tearDown(() {\n      userBloc.close();\n    });\n\n    test('initial state is UserInitial', () {\n      expect(userBloc.state, equals(UserInitial()));\n    });\n\n    blocTest&lt;UserBloc, UserState&gt;(\n      'emits &#91;UserLoadInProgress, UserLoadSuccess] when users are loaded successfully',\n      build: () {\n        when(() =&gt; mockRepository.getUsers())\n            .thenAnswer((_) async =&gt; &#91;\n          User(id: '1', name: 'John'),\n          User(id: '2', name: 'Jane'),\n        ]);\n        return userBloc;\n      },\n      act: (bloc) =&gt; bloc.add(UserLoadRequested()),\n      expect: () =&gt; &#91;\n        UserLoadInProgress(),\n        UserLoadSuccess(&#91;\n          User(id: '1', name: 'John'),\n          User(id: '2', name: 'Jane'),\n        ]),\n      ],\n    );\n\n    blocTest&lt;UserBloc, UserState&gt;(\n      'emits &#91;UserLoadInProgress, UserLoadFailure] when loading fails',\n      build: () {\n        when(() =&gt; mockRepository.getUsers())\n            .thenThrow(Exception('Network error'));\n        return userBloc;\n      },\n      act: (bloc) =&gt; bloc.add(UserLoadRequested()),\n      expect: () =&gt; &#91;\n        UserLoadInProgress(),\n        UserLoadFailure('Exception: Network error'),\n      ],\n    );\n  });\n}<\/code><\/pre>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\">\uc0c1\ud0dc \uad00\ub9ac \uc120\ud0dd \uac00\uc774\ub4dc: \ucd5c\uc885 \uacb0\ub860<\/h2>\n\n\n\n<h3 class=\"wp-block-heading\">\ud504\ub85c\uc81d\ud2b8 \ud2b9\uc131\ubcc4 \ucd94\ucc9c<\/h3>\n\n\n\n<figure class=\"wp-block-table\"><table class=\"has-fixed-layout\"><thead><tr><th>\ud504\ub85c\uc81d\ud2b8 \ud2b9\uc131<\/th><th>1\uc21c\uc704<\/th><th>2\uc21c\uc704<\/th><th>\ube44\uace0<\/th><\/tr><\/thead><tbody><tr><td><strong>\ud504\ub85c\ud1a0\ud0c0\uc785\/MVP<\/strong><\/td><td>GetX<\/td><td>setState<\/td><td>\ube60\ub978 \uac1c\ubc1c\uc774 \uc6b0\uc120<\/td><\/tr><tr><td><strong>\uc911\uc18c\uaddc\ubaa8 \uc571<\/strong><\/td><td>Riverpod<\/td><td>Provider<\/td><td>\uc548\uc815\uc131\uacfc \ud655\uc7a5\uc131<\/td><\/tr><tr><td><strong>\ub300\uae30\uc5c5\/\uc5d4\ud130\ud504\ub77c\uc774\uc988<\/strong><\/td><td>BLoC<\/td><td>Riverpod<\/td><td>\uba85\ud655\ud55c \uc544\ud0a4\ud14d\ucc98 \ud544\uc694<\/td><\/tr><tr><td><strong>\ud300 \uac1c\ubc1c (5\uba85 \uc774\uc0c1)<\/strong><\/td><td>BLoC<\/td><td>Riverpod<\/td><td>\ucf54\ub4dc \ucee8\ubca4\uc158 \ud1b5\uc77c<\/td><\/tr><tr><td><strong>\ubcf5\uc7a1\ud55c \ube44\uc988\ub2c8\uc2a4 \ub85c\uc9c1<\/strong><\/td><td>BLoC<\/td><td>Clean Architecture + Riverpod<\/td><td>\ud14c\uc2a4\ud2b8 \uc6a9\uc774\uc131<\/td><\/tr><tr><td><strong>\uc2e4\uc2dc\uac04 \uae30\ub2a5 \uc911\uc2ec<\/strong><\/td><td>GetX<\/td><td>Riverpod<\/td><td>\ubc18\uc751\ud615 \ud504\ub85c\uadf8\ub798\ubc0d<\/td><\/tr><\/tbody><\/table><\/figure>\n\n\n\n<h3 class=\"wp-block-heading\">\ud559\uc2b5 \uc21c\uc11c \ucd94\ucc9c<\/h3>\n\n\n\n<ol class=\"wp-block-list\">\n<li><strong>1\ub2e8\uacc4<\/strong>: setState\uc640 InheritedWidget\uc73c\ub85c \uae30\ubcf8 \uac1c\ub150 \uc774\ud574<\/li>\n\n\n\n<li><strong>2\ub2e8\uacc4<\/strong>: Riverpod\uc73c\ub85c \ud604\ub300\uc801 \uc0c1\ud0dc \uad00\ub9ac \ud328\ud134 \ud559\uc2b5<\/li>\n\n\n\n<li><strong>3\ub2e8\uacc4<\/strong>: \ud504\ub85c\uc81d\ud2b8 \uc694\uad6c\uc0ac\ud56d\uc5d0 \ub530\ub77c GetX \ub610\ub294 BLoC \uc120\ud0dd<\/li>\n<\/ol>\n\n\n\n<h3 class=\"wp-block-heading\">\ub9c8\uc774\uadf8\ub808\uc774\uc158 \uc804\ub7b5<\/h3>\n\n\n\n<pre class=\"wp-block-code\"><code>\/\/ 1\ub2e8\uacc4: setState \u2192 Riverpod\n\/\/ \uae30\uc874 StatefulWidget\uc744 ConsumerStatefulWidget\uc73c\ub85c \ubcc0\uacbd\nclass OldCounterWidget extends StatefulWidget {\n  @override\n  _OldCounterWidgetState createState() =&gt; _OldCounterWidgetState();\n}\n\nclass _OldCounterWidgetState extends State&lt;OldCounterWidget&gt; {\n  int _counter = 0;\n\n  void _increment() {\n    setState(() {\n      _counter++;\n    });\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    return Text('$_counter');\n  }\n}\n\n\/\/ \uc0c8\ub85c\uc6b4 Riverpod \ubc84\uc804\nfinal counterProvider = StateProvider&lt;int&gt;((ref) =&gt; 0);\n\nclass NewCounterWidget extends ConsumerWidget {\n  @override\n  Widget build(BuildContext context, WidgetRef ref) {\n    final counter = ref.watch(counterProvider);\n\n    return GestureDetector(\n      onTap: () =&gt; ref.read(counterProvider.notifier).state++,\n      child: Text('$counter'),\n    );\n  }\n}<\/code><\/pre>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\">\ub9c8\ubb34\ub9ac: \uc0c1\ud0dc \uad00\ub9ac\uc758 \ubbf8\ub798<\/h2>\n\n\n\n<p>Flutter\uc758 \uc0c1\ud0dc \uad00\ub9ac\ub294 \uacc4\uc18d \uc9c4\ud654\ud558\uace0 \uc788\uc2b5\ub2c8\ub2e4. <strong>Riverpod<\/strong>\uc740 \ud604\uc7ac \uac00\uc7a5 \ud604\ub300\uc801\uc774\uace0 \uad8c\uc7a5\ub418\ub294 \uc811\uadfc\ubc95\uc774\uba70, <strong>GetX<\/strong>\ub294 \ube60\ub978 \ud504\ub85c\ud1a0\ud0c0\uc774\ud551\uc5d0, <strong>BLoC<\/strong>\uc740 \ub300\uaddc\ubaa8 \ud504\ub85c\uc81d\ud2b8\uc5d0 \ucd5c\uc801\ud654\ub418\uc5b4 \uc788\uc2b5\ub2c8\ub2e4.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">\ud575\uc2ec \uc6d0\uce59<\/h3>\n\n\n\n<ol class=\"wp-block-list\">\n<li><strong>\ub2e8\uc21c\ud568\uc5d0\uc11c \uc2dc\uc791\ud558\uc138\uc694<\/strong>: setState\ubd80\ud130 \uc2dc\uc791\ud574 \uc810\uc9c4\uc801\uc73c\ub85c \ubc1c\uc804<\/li>\n\n\n\n<li><strong>\ud300\uacfc \ud504\ub85c\uc81d\ud2b8\uc5d0 \ub9de\ub294 \uc120\ud0dd<\/strong>: \uc740\ucd1d\uc54c\uc740 \uc5c6\uc2b5\ub2c8\ub2e4<\/li>\n\n\n\n<li><strong>\ud14c\uc2a4\ud2b8 \uac00\ub2a5\uc131\uc744 \uace0\ub824\ud558\uc138\uc694<\/strong>: \ube44\uc988\ub2c8\uc2a4 \ub85c\uc9c1\uacfc UI\uc758 \ubd84\ub9ac<\/li>\n\n\n\n<li><strong>\uc131\ub2a5\uc744 \ubaa8\ub2c8\ud130\ub9c1\ud558\uc138\uc694<\/strong>: \ubd88\ud544\uc694\ud55c \uc7ac\ube4c\ub4dc \ucd5c\uc18c\ud654<\/li>\n<\/ol>\n\n\n\n<h3 class=\"wp-block-heading\">\ub2e4\uc74c \ud559\uc2b5 \ub2e8\uacc4<\/h3>\n\n\n\n<ul class=\"wp-block-list\">\n<li><strong>Riverpod Generator<\/strong>: \ud0c0\uc785 \uc548\uc804\uc131\uc744 \uadf9\ub300\ud654\ud558\ub294 \ucf54\ub4dc \uc0dd\uc131<\/li>\n\n\n\n<li><strong>State Restoration<\/strong>: \uc571 \uc7ac\uc2dc\uc791 \uc2dc \uc0c1\ud0dc \ubcf5\uc6d0<\/li>\n\n\n\n<li><strong>Hydrated BLoC<\/strong>: \uc0c1\ud0dc \uc9c0\uc18d\uc131\uc744 \uc704\ud55c \uc790\ub3d9 \uc9c1\ub82c\ud654<\/li>\n<\/ul>\n\n\n\n<p><strong>\uae30\uc5b5\ud558\uc138\uc694<\/strong>: \uc644\ubcbd\ud55c \uc0c1\ud0dc \uad00\ub9ac \uc194\ub8e8\uc158\uc740 \uc5c6\uc2b5\ub2c8\ub2e4. \ud504\ub85c\uc81d\ud2b8\uc758 \uc694\uad6c\uc0ac\ud56d, \ud300\uc758 \uacbd\ud5d8, \uc7a5\uae30\uc801\uc778 \uc720\uc9c0\ubcf4\uc218\uc131\uc744 \uace0\ub824\ud558\uc5ec \ud604\uba85\ud558\uac8c \uc120\ud0dd\ud558\uc138\uc694. \uac00\uc7a5 \uc911\uc694\ud55c \uac83\uc740 <strong>\uc77c\uad00\uc131 \uc788\ub294 \ud328\ud134<\/strong>\uc744 \uc720\uc9c0\ud558\ub294 \uac83\uc785\ub2c8\ub2e4.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\">\ucd94\uac00 \ud559\uc2b5 \uc790\ub8cc<\/h2>\n\n\n\n<h3 class=\"wp-block-heading\">\uacf5\uc2dd \ubb38\uc11c<\/h3>\n\n\n\n<ul class=\"wp-block-list\">\n<li><a href=\"https:\/\/docs.flutter.dev\/development\/data-and-backend\/state-mgmt\">Flutter State Management<\/a><\/li>\n\n\n\n<li><a href=\"https:\/\/riverpod.dev\/\">Riverpod Documentation<\/a><\/li>\n\n\n\n<li><a href=\"https:\/\/bloclibrary.dev\/\">BLoC Library<\/a><\/li>\n<\/ul>\n\n\n\n<h3 class=\"wp-block-heading\">\ucee4\ubba4\ub2c8\ud2f0 \ub9ac\uc18c\uc2a4<\/h3>\n\n\n\n<ul class=\"wp-block-list\">\n<li><a href=\"https:\/\/github.com\/fluttercommunity\">Flutter Community<\/a><\/li>\n\n\n\n<li><a href=\"https:\/\/github.com\/rrousselGit\/riverpod\/tree\/master\/examples\">Riverpod Examples<\/a><\/li>\n\n\n\n<li><a href=\"https:\/\/github.com\/felangel\/bloc\/tree\/master\/examples\">BLoC Examples<\/a><\/li>\n<\/ul>\n\n\n\n<p>\uc774\uc81c \uc5ec\ub7ec\ubd84\ub9cc\uc758 Flutter \uc571\uc5d0\uc11c \uc801\uc808\ud55c \uc0c1\ud0dc \uad00\ub9ac\ub97c \uc120\ud0dd\ud558\uace0 \uad6c\ud604\ud574\ubcf4\uc138\uc694!<\/p>\n","protected":false},"excerpt":{"rendered":"<p>Flutter \uc0c1\ud0dc \uad00\ub9ac \uc644\ubcbd \uac00\uc774\ub4dc: \ub124\uc774\ud2f0\ube0c \uac1c\ubc1c\uc790\ub97c \uc704\ud55c \uc2e4\ubb34 \uc120\ud0dd \uc804\ub7b5 \uc548\ub4dc\ub85c\uc774\ub4dc\/iOS \uac1c\ubc1c\uc790\uac00 \uac00\uc7a5 \uace0\ubbfc\ud558\ub294 &#8220;\uc5b4\ub5a4 \uc0c1\ud0dc \uad00\ub9ac\ub97c \uc120\ud0dd\ud574\uc57c \ud560\uae4c?&#8221;\uc5d0 \ub300\ud55c \uacb0\uc815\uc801 \ub2f5\ubcc0 \uc11c\ub860: \uc0c1\ud0dc \uad00\ub9ac, \uc65c \uc774\ub807\uac8c \ubcf5\uc7a1\ud560\uae4c? \uc548\ub4dc\ub85c\uc774\ub4dc \uac1c\ubc1c\uc790\ub77c\uba74 MVVM\uc758 LiveData\ub098 DataBinding\uc744, iOS \uac1c\ubc1c\uc790\ub77c\uba74 MVC\ub098 MVVM\uc758 \uad00\ucc30\uc790 \ud328\ud134\uc744 \uc0ac\uc6a9\ud574\ubd24\uc744 \uac83\uc785\ub2c8\ub2e4. \uc6f9 \ud504\ub860\ud2b8\uc5d4\ub4dc \uac1c\ubc1c\uc790\ub77c\uba74 Redux\ub098 MobX, Vue\uc758 Vuex \uac19\uc740 \uc0c1\ud0dc \uad00\ub9ac \ub77c\uc774\ube0c\ub7ec\ub9ac\uc5d0 \uc775\uc219\ud558\uc2e4 \uac81\ub2c8\ub2e4. [&hellip;]<\/p>\n","protected":false},"author":1,"featured_media":0,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"_jetpack_memberships_contains_paid_content":false,"footnotes":""},"categories":[43],"tags":[114,110,78,113,111,79,112,106],"class_list":["post-85","post","type-post","status-publish","format-standard","hentry","category-flutter","tag-android","tag-dart","tag-flutter","tag-ios","tag-111","tag-79","tag-112","tag-106"],"jetpack_featured_media_url":"","jetpack_sharing_enabled":true,"_links":{"self":[{"href":"https:\/\/hed-g.me\/index.php?rest_route=\/wp\/v2\/posts\/85","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/hed-g.me\/index.php?rest_route=\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/hed-g.me\/index.php?rest_route=\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/hed-g.me\/index.php?rest_route=\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/hed-g.me\/index.php?rest_route=%2Fwp%2Fv2%2Fcomments&post=85"}],"version-history":[{"count":2,"href":"https:\/\/hed-g.me\/index.php?rest_route=\/wp\/v2\/posts\/85\/revisions"}],"predecessor-version":[{"id":108,"href":"https:\/\/hed-g.me\/index.php?rest_route=\/wp\/v2\/posts\/85\/revisions\/108"}],"wp:attachment":[{"href":"https:\/\/hed-g.me\/index.php?rest_route=%2Fwp%2Fv2%2Fmedia&parent=85"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/hed-g.me\/index.php?rest_route=%2Fwp%2Fv2%2Fcategories&post=85"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/hed-g.me\/index.php?rest_route=%2Fwp%2Fv2%2Ftags&post=85"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}