2026๋ Flutter ์ํ ๊ด๋ฆฌ ์๋ฒฝ ๊ฐ์ด๋: Riverpod vs Bloc vs GetX ๋น๊ต ๋ถ์
Riverpod 3.0, Bloc 9.0, GetX ์ธ ๊ฐ์ง Flutter ์ํ ๊ด๋ฆฌ ์๋ฃจ์ ์ ์ฝ๋ ์์ , ์ฑ๋ฅ ๋ถ์, ํ ์คํธ ์ ๋ต ๊ด์ ์์ ๋น๊ต ๋ถ์ํฉ๋๋ค.

Flutter ์ํ ๊ด๋ฆฌ๋ ์์ ฏ ๊ฐ ๋ฐ์ดํฐ ํ๋ฆ์ ์ ์ดํ๋ ์ ํ๋ฆฌ์ผ์ด์ ์ค๊ณ์ ํต์ฌ ์์์ด๋ค. 2026๋ ํ์ฌ Flutter ์ํ๊ณ์์๋ ์ธ ๊ฐ์ง ์๋ฃจ์ ์ด ์ฃผ๋ฅ๋ฅผ ์ฐจ์งํ๊ณ ์๋ค. ์ปดํ์ผ ํ์ ์์ ์ฑ์ ์ ๊ณตํ๋ Riverpod 3.0, ์ํฐํ๋ผ์ด์ฆ๊ธ ์ด๋ฒคํธ ์ถ์ ๊ธฐ๋ฅ์ ๊ฐ์ถ Bloc 9.0, ๊ทธ๋ฆฌ๊ณ ์ฑํ๋ฅ ์ ๊ฐ์ํ๊ณ ์์ง๋ง ๊ธฐ์กด ํ๋ก์ ํธ์ ์ฌ์ ํ ๋จ์์๋ GetX์ด๋ค. ์ฌ๋ฐ๋ฅธ ์๋ฃจ์ ์ ์ ํ์ ํ ์คํธ ์ฉ์ด์ฑ, ํ์ฅ์ฑ, ์ฅ๊ธฐ์ ์ธ ์ ์ง๋ณด์ ๋น์ฉ์ ์ง์ ์ ์ธ ์ํฅ์ ๋ฏธ์น๋ค.
Riverpod 3.0์ ์ปดํ์ผ ํ์ ์์ ์ฑ๊ณผ ์ต์ํ์ ๋ณด์ผ๋ฌํ๋ ์ดํธ๋ก ๋๋ถ๋ถ์ ํ๋ก์ ํธ์ ์ ํฉํฉ๋๋ค. Bloc 9.0์ ์ด๋ฒคํธ ๊ธฐ๋ฐ ๊ฐ์ฌ ์ถ์ ์ด ํ์ํ ๊ท์ ์ฐ์ ์ ํ์ค์ ๋๋ค. GetX๋ ๋ง์ด๊ทธ๋ ์ด์ ์์ฐ์ด ์๋ ๊ธฐ์กด ์ฝ๋๋ฒ ์ด์ค ์ ์ง๋ณด์์๋ง ๊ณ ๋ คํด์ผ ํฉ๋๋ค.
Riverpod 3.0: ์ปดํ์ผ ํ์ ์์ ์ฑ๊ณผ ์๋ ์ฌ์๋ ๋ฉ์ปค๋์ฆ
Riverpod 3.0์ Flutter ์ ํ๋ฆฌ์ผ์ด์ ์์ ์ํ๋ฅผ ์ ์ธํ๊ณ ์๋นํ๋ ๋ฐฉ์์ ๊ทผ๋ณธ์ ์ผ๋ก ๋ณํ์์ผฐ๋ค. ์ด๋ ธํ ์ด์ ๊ธฐ๋ฐ ์ฝ๋ ์์ฑ์ ํตํด ์์กด์ฑ ์ค๋ฅ๊ฐ ๋ฐํ์์ด ์๋ ์ปดํ์ผ ํ์์ ๊ฐ์ง๋๋ค. ์ด๋ก์จ ๊ธฐ์กด์๋ ์๋ ํ ์คํธ๋ก๋ง ๋ฐ๊ฒฌํ ์ ์์๋ ์ ์ฒด ๋ฒ๊ทธ ์นดํ ๊ณ ๋ฆฌ๊ฐ ์ ๊ฑฐ๋๋ค.
์๋ ์ฌ์๋ ๋ฉ์ปค๋์ฆ์ ํ๋ก๋ฐ์ด๋ ๊ณ์ฐ์ด ์คํจํ ๊ฒฝ์ฐ ์ผ์์ ์ธ ๋คํธ์ํฌ ์ค๋ฅ๋ฅผ ์๋์ผ๋ก ์ฒ๋ฆฌํ๋ค. ์ค์ ๊ฐ๋ฅํ ์ง์ฐ ์๊ฐ์ ์ ์ฉํ ์ฌ์๋๋ฅผ ํตํด ์๋ฌ ๋ณต๊ตฌ์ฉ ๋ณด์ผ๋ฌํ๋ ์ดํธ ์ฝ๋๊ฐ ๋ํญ ๊ฐ์ํ๋ค.
import 'package:riverpod_annotation/riverpod_annotation.dart';
part 'counter_provider.g.dart';
// Code generation ensures compile-time safety
class Counter extends _$Counter {
int build() => 0; // Initial state
void increment() => state = state + 1;
void decrement() => state = state - 1;
void reset() => state = 0;
}@riverpod ์ด๋
ธํ
์ด์
์ ํตํด ํ๋ก๋ฐ์ด๋์ ๋ชจ๋ ๋ณด์ผ๋ฌํ๋ ์ดํธ๊ฐ ์๋ ์์ฑ๋๋ค. ํ์
๋ถ์ผ์น, ์ค๋ฒ๋ผ์ด๋ ๋๋ฝ, ์ํ ์์กด์ฑ์ ์ปดํ์ผ ๋จ๊ณ์์ ๊ฐ์ง๋๋ค.
import 'package:riverpod_annotation/riverpod_annotation.dart';
part 'user_repository_provider.g.dart';
Future<User> currentUser(Ref ref) async {
final authService = ref.watch(authServiceProvider);
final userId = authService.currentUserId;
// Auto-retry on network failure (Riverpod 3.0)
final response = await ref.watch(
httpClientProvider,
).get('/api/users/$userId');
return User.fromJson(response.data);
}Riverpod 3.0์ ์์ ฏ์ด ํ๋ฉด์์ ๋ฒ์ด๋ฌ์ ๋ ํ๋ก๋ฐ์ด๋ ๋ฆฌ์ค๋๋ฅผ ์๋์ผ๋ก ์ผ์ ์ค์งํ๋ค. ์ด๋ฅผ ํตํด ๋ถํ์ํ ์ฐ์ฐ์ด ์ค์ด๋ค๊ณ ๋ชจ๋ฐ์ผ ๊ธฐ๊ธฐ์ ๋ฐฐํฐ๋ฆฌ ์๋ชจ๊ฐ ๊ฐ์ ๋๋ค.
Bloc 9.0: ์ํฐํ๋ผ์ด์ฆ๋ฅผ ์ํ ์ด๋ฒคํธ ๊ธฐ๋ฐ ์ํคํ ์ฒ
Bloc 9.0์ ์ด๋ฒคํธ, ์ํ, ๋น์ฆ๋์ค ๋ก์ง์ ์๊ฒฉํ ๋ถ๋ฆฌ๋ฅผ ๊ฐ์ ํ๋ค. ๋ชจ๋ ์ํ ๋ณ๊ฒฝ์ด ํน์ ์ด๋ฒคํธ์ ๋งคํ๋๋ฏ๋ก ๊ท์ ์ฐ์ ์์ ์๊ตฌํ๋ ๊ฐ์ฌ ์ถ์ ์ด ์๋์ผ๋ก ์์ฑ๋๋ค. ๋ฒ์ 9.0์์ ๋์ ๋ ๋ง์ดํธ ์์ ์ฑ ๊ฒ์ฌ๋ ํ๊ธฐ๋ ์์ ฏ์์ ์ฝ๋ฐฑ์ด ์คํ๋๋ ๊ฒ์ ๋ฐฉ์งํ๋ค.
sealed class AuthenticationEvent {}
final class LoginRequested extends AuthenticationEvent {
final String email;
final String password;
LoginRequested({required this.email, required this.password});
}
final class LogoutRequested extends AuthenticationEvent {}
final class SessionRestored extends AuthenticationEvent {
final String token;
SessionRestored({required this.token});
}Dart 3์ sealed class๋ฅผ ํตํด ์ด๋ฒคํธ์ ์์ ํ ํจํด ๋งค์นญ์ด ๋ณด์ฅ๋๋ค. ์ปดํ์ผ๋ฌ๊ฐ ๋ชจ๋ ์ด๋ฒคํธ ํ์ ์ ๋ํ ํธ๋ค๋ฌ ์กด์ฌ ์ฌ๋ถ๋ฅผ ๊ฒ์ฆํ๋ค.
import 'package:flutter_bloc/flutter_bloc.dart';
class AuthenticationBloc
extends Bloc<AuthenticationEvent, AuthenticationState> {
final AuthRepository _authRepo;
final TokenStorage _tokenStorage;
AuthenticationBloc({
required AuthRepository authRepo,
required TokenStorage tokenStorage,
}) : _authRepo = authRepo,
_tokenStorage = tokenStorage,
super(AuthenticationInitial()) {
on<LoginRequested>(_onLoginRequested);
on<LogoutRequested>(_onLogoutRequested);
on<SessionRestored>(_onSessionRestored);
}
Future<void> _onLoginRequested(
LoginRequested event,
Emitter<AuthenticationState> emit,
) async {
emit(AuthenticationLoading());
try {
final token = await _authRepo.login(
email: event.email,
password: event.password,
);
await _tokenStorage.save(token);
emit(AuthenticationSuccess(token: token));
} catch (e) {
emit(AuthenticationFailure(message: e.toString()));
}
}
Future<void> _onLogoutRequested(
LogoutRequested event,
Emitter<AuthenticationState> emit,
) async {
await _tokenStorage.clear();
emit(AuthenticationInitial());
}
Future<void> _onSessionRestored(
SessionRestored event,
Emitter<AuthenticationState> emit,
) async {
emit(AuthenticationSuccess(token: event.token));
}
}๊ฐ ์ด๋ฒคํธ ํธ๋ค๋ฌ๋ ๋ช
ํํ ์ํ ์ ํ์ ์์ฑํ๋ค. ๋ก๊น
๋ฏธ๋ค์จ์ด๋ฅผ ํตํด ๋๋ฒ๊น
์ด๋ ์ปดํ๋ผ์ด์ธ์ค ๋ชฉ์ ์ผ๋ก ๋ชจ๋ ์ด๋ฒคํธ๋ฅผ ๊ธฐ๋กํ ์ ์๋ค. Bloc 9.0์ EmittableStateStreamableSource ์ธํฐํ์ด์ค๋ ๊ฒฝ๋ ๋ชจ์ ๊ตฌํ์ ๊ฐ๋ฅํ๊ฒ ํ์ฌ ํ
์คํธ๋ฅผ ๋จ์ํํ๋ค.
Bloc ์ด๋ฒคํธ ํธ๋์คํฌ๋จธ: ๊ณ ๋น๋ ์ ๋ ฅ ์ฒ๋ฆฌ
Bloc์๋ ์ผ๋ฐ์ ์ธ ๋์์ฑ ๋ฌธ์ ๋ฅผ ํด๊ฒฐํ๋ ๋ด์ฅ ์ด๋ฒคํธ ํธ๋์คํฌ๋จธ๊ฐ ์ ๊ณต๋๋ค. ๊ฒ์์ด ์๋์์ฑ, ์ฐ์ ๋ฒํผ ํญ, ์ค์๊ฐ ๋ฐ์ดํฐ ์คํธ๋ฆผ ์ฒ๋ฆฌ๊ฐ ์ ์ธ์ ์ผ๋ก ๊ตฌํ ๊ฐ๋ฅํ๋ค.
import 'package:bloc_concurrency/bloc_concurrency.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
class SearchBloc extends Bloc<SearchEvent, SearchState> {
final SearchRepository _repository;
SearchBloc({required SearchRepository repository})
: _repository = repository,
super(SearchInitial()) {
// restartable() cancels previous search on new input
on<SearchQueryChanged>(
_onQueryChanged,
transformer: restartable(),
);
// droppable() ignores events while processing
on<SearchResultSelected>(
_onResultSelected,
transformer: droppable(),
);
}
Future<void> _onQueryChanged(
SearchQueryChanged event,
Emitter<SearchState> emit,
) async {
if (event.query.length < 3) {
emit(SearchInitial());
return;
}
emit(SearchLoading());
final results = await _repository.search(event.query);
emit(SearchLoaded(results: results));
}
Future<void> _onResultSelected(
SearchResultSelected event,
Emitter<SearchState> emit,
) async {
emit(SearchNavigating(result: event.result));
}
}restartable() ํธ๋์คํฌ๋จธ๋ ์๋ก์ด ์
๋ ฅ์ด ๋์ฐฉํ๋ฉด ์งํ ์ค์ธ ๊ฒ์์ ์ทจ์ํ์ฌ ์ค๋๋ ๊ฒฐ๊ณผ๊ฐ ์ต์ ๊ฒฐ๊ณผ๋ฅผ ๋ฎ์ด์ฐ๋ ๊ฒ์ ๋ฐฉ์งํ๋ค. droppable() ํธ๋์คํฌ๋จธ๋ ๋ค๋น๊ฒ์ด์
์ฒ๋ฆฌ ์ค ์ค๋ณต ํญ์ ๋ฌด์ํ๋ค.
Flutter ๋ฉด์ ์ค๋น๊ฐ ๋์ จ๋์?
์ธํฐ๋ํฐ๋ธ ์๋ฎฌ๋ ์ดํฐ, flashcards, ๊ธฐ์ ํ ์คํธ๋ก ์ฐ์ตํ์ธ์.
GetX: ๊ธฐ์ ๋ถ์ฑ์ ๋ง์ด๊ทธ๋ ์ด์ ํ์ค
GetX๋ ๋น ๋ฅธ ํ๋กํ ํ์ดํ๊ณผ ์ต์ํ์ ๋ณด์ผ๋ฌํ๋ ์ดํธ๋ก ์ธ๊ธฐ๋ฅผ ์ป์๋ค. ๊ทธ๋ฌ๋ 2026๋ ํ์ฌ, ์ด ๋ผ์ด๋ธ๋ฌ๋ฆฌ๋ ์ ์ง๋ณด์ ์๊ธฐ์ ์ง๋ฉดํด ์๋ค. ์ฐ๋ฐ์ ์ธ ์ ๋ฐ์ดํธ, ๋จ์ผ ๋ฉ์ธํ ์ด๋ ์์กด์ฑ, ์ต์ Flutter SDK์์ ํธํ์ฑ ๋ฌธ์ ๊ฐ ์ฆ๊ฐํ๊ณ ์๋ค. ํ๋ก๋์ ํ๊ฒฝ์ GetX ์ ํ๋ฆฌ์ผ์ด์ ์์๋ ์ปจํธ๋กค๋ฌ ๋ผ์ดํ์ฌ์ดํด ๋ฌธ์ ์ ์๋ฌต์ ๊ธ๋ก๋ฒ ์ฑ๊ธํค์ผ๋ก ์ธํ ๋ฉ๋ชจ๋ฆฌ ๋์๊ฐ ๋ณด๊ณ ๋๊ณ ์๋ค.
import 'package:get/get.dart';
// Global singleton - difficult to test and scope
class CounterController extends GetxController {
final count = 0.obs; // Reactive observable
void increment() => count.value++;
void decrement() => count.value--;
// Lifecycle hooks - disposal timing is unpredictable
void onClose() {
// Cleanup may not execute reliably
super.onClose();
}
}
// Usage in widget
class CounterPage extends StatelessWidget {
Widget build(BuildContext context) {
// Get.put creates a global singleton
final controller = Get.put(CounterController());
return Obx(() => Text('${controller.count}'));
}
}Get.put()์ ์ปจํธ๋กค๋ฌ๋ฅผ ๊ธ๋ก๋ฒ ์ฑ๊ธํค์ผ๋ก ๋ฑ๋กํ๋ค. ๋ณต์กํ ๋ค๋น๊ฒ์ด์
ํ๋ก์ฐ์์๋ ์ปจํธ๋กค๋ฌ๊ฐ ์๋ํ ์ค์ฝํ๋ฅผ ๋์ด ์กด์ํ๋ฉฐ ๋ฉ๋ชจ๋ฆฌ๋ฅผ ์๋นํ๋ค. .obs ๋ฐ์ํ ๋ณ์๋ Flutter์ ํ์ค ์ํ ์๋ฆผ ์์คํ
์ ์ฐํํ๋ฏ๋ก ๋ค๋ฅธ ํจํค์ง์์ ํตํฉ์ด ๋ถ์์ ํด์ง๋ค.
GetX์์ Riverpod์ผ๋ก์ ๋จ๊ณ๋ณ ๋ง์ด๊ทธ๋ ์ด์
GetX ์ฝ๋๋ฒ ์ด์ค๋ฅผ ์ ์ง๋ณด์ํ๋ ํ์๊ฒ Riverpod์ผ๋ก์ ๋ง์ด๊ทธ๋ ์ด์ ์ ๋จ๊ณ์ ์ผ๋ก ์งํํ ์ ์๋ค. ๋ ๋ผ์ด๋ธ๋ฌ๋ฆฌ๋ ๋์ผํ ํ๋ก์ ํธ ๋ด์์ ๊ณต์กดํ ์ ์์ผ๋ฏ๋ก ์ ๋ฉด์ ์ธ ์ฌ์์ฑ ์์ด ํ๋ฉด ๋จ์๋ก ์ ํ์ ์งํํ ์ ์๋ค.
// Step 1: Replace GetX controller with Riverpod notifier
// Before (GetX)
class ProductController extends GetxController {
final products = <Product>[].obs;
final isLoading = false.obs;
Future<void> loadProducts() async {
isLoading.value = true;
products.value = await ProductApi.fetchAll();
isLoading.value = false;
}
}
// After (Riverpod 3.0)
class ProductList extends _$ProductList {
Future<List<Product>> build() async {
// Auto-retry on failure, auto-pause when off-screen
return ProductApi.fetchAll();
}
Future<void> refresh() async {
ref.invalidateSelf();
}
}// Step 2: Replace widget bindings
// Before (GetX)
class ProductPage extends StatelessWidget {
Widget build(BuildContext context) {
final ctrl = Get.put(ProductController());
return Obx(() {
if (ctrl.isLoading.value) return CircularProgressIndicator();
return ListView.builder(
itemCount: ctrl.products.length,
itemBuilder: (_, i) => ProductTile(ctrl.products[i]),
);
});
}
}
// After (Riverpod 3.0)
class ProductPage extends ConsumerWidget {
Widget build(BuildContext context, WidgetRef ref) {
final productsAsync = ref.watch(productListProvider);
return productsAsync.when(
loading: () => const CircularProgressIndicator(),
error: (err, stack) => ErrorDisplay(error: err),
data: (products) => ListView.builder(
itemCount: products.length,
itemBuilder: (_, i) => ProductTile(products[i]),
),
);
}
}Riverpod ๋ฒ์ ์์๋ AsyncValue.when()์ ํตํด ๋ก๋ฉ, ์๋ฌ, ๋ฐ์ดํฐ ์ํ๋ฅผ ๋ช
์์ ์ผ๋ก ์ฒ๋ฆฌํ๋ค. ๊ธ๋ก๋ฒ ์ฑ๊ธํค์ด ํ์ ์๊ณ , ์๋ ๋ผ์ดํ์ฌ์ดํด ๊ด๋ฆฌ๋ ๋ถํ์ํ๋ฉฐ, ์์ ฏ ์ธ๋ง์ดํธ ์ ์๋์ผ๋ก ํด์ ๋๋ค.
์ฑ๋ฅ ๋น๊ต: ๋ฆฌ๋น๋ ํจ์จ์ฑ
๋ฆฌ๋น๋ ํจ์จ์ฑ์ ํ๋ ์ ๋ ์ดํธ์ ์ง์ ์ ์ธ ์ํฅ์ ๋ฏธ์น๋ค. ๊ฐ ์๋ฃจ์ ์ ์์ ฏ ๋ฆฌ๋น๋๋ฅผ ๋ค๋ฅด๊ฒ ์ฒ๋ฆฌํ๋ฉฐ, ๊ทธ ์ฐจ์ด๋ ์๋ฐฑ ๊ฐ์ ์์ดํ ์ด ํฌํจ๋ ๋ฆฌ์คํธ์์ ์ธก์ ๊ฐ๋ฅํ๊ฒ ๋ํ๋๋ค.
| ์งํ | Riverpod 3.0 | Bloc 9.0 | GetX |
|------|-------------|----------|------|
| ์ ํ์ ๋ฆฌ๋น๋ | select() ํํฐ | BlocSelector | ํ๋๋ณ .obs |
| ์ปดํ์ผ ํ์ ์์ ์ฑ | ์์ (์ฝ๋ ์์ฑ) | ๋ถ๋ถ์ (sealed class) | ์์ |
| ์๋ ํด์ | ๋ด์ฅ | close()๋ฅผ ํตํ ์๋ | ๋ถ์์ |
| ํ๋ฉด ๋ฐ ์ผ์ ์ค์ง | ์๋ (3.0) | ์๋ | ๋ฏธ์ง์ |
| ์ด๋ฒคํธ ์ถ์ | Provider Observer | ์ ์ฒด ์ด๋ฒคํธ ๋ก๊ทธ | ์์ |
| ํ
์คํธ ๊ฒฉ๋ฆฌ | ProviderContainer.test() | EmittableStateStreamableSource | Get.testMode ํ์ |
| ๋ฒ๋ค ํฌ๊ธฐ | ์ฝ 45KB | ์ฝ 38KB | ์ฝ 120KB (๋ผ์ฐํ
, DI, HTTP ํฌํจ) |
Riverpod์ select() ๋ฉ์๋์ Bloc์ BlocSelector๋ ๋ณ๊ฒฝ๋ ๋ฐ์ดํฐ์ ์์กดํ๋ ์์ ฏ ์๋ธํธ๋ฆฌ๋ง ์
๋ฐ์ดํธํ๋ ์ ๋ฐํ ๋ฆฌ๋น๋๋ฅผ ๊ตฌํํ๋ค. GetX์ .obs๋ ํ๋ ๋จ์๋ก ์ ์ฌํ ์ธ๋ถ์ฑ์ ๋ฌ์ฑํ์ง๋ง, ์์กด์ฑ ๊ทธ๋ํ์ ์ปดํ์ผ ํ์ ๊ฒ์ฆ์ด ๋ถ์ฌํ๋ค.
GetX๋ ๋ผ์ฐํ , ์์กด์ฑ ์ฃผ์ , HTTP ํด๋ผ์ด์ธํธ, ์ํ ๊ด๋ฆฌ๋ฅผ ๋จ์ผ ํจํค์ง์ ๋ฒ๋ค๋งํฉ๋๋ค. ์ํ ๊ด๋ฆฌ๋ง ์ฌ์ฉํ๋ ์ ํ๋ฆฌ์ผ์ด์ ์์๋ 120KB ์ ์ฒด ๋ผ์ด๋ธ๋ฌ๋ฆฌ๋ฅผ ์ํฌํธํด์ผ ํฉ๋๋ค. Riverpod๊ณผ Bloc์ ๋จ์ผ ์ฑ ์์ ํนํ๋ ๊ฒฝ๋ ํจํค์ง์ ๋๋ค.
๊ฐ ์๋ฃจ์ ์ ํ ์คํธ ์ ๋ต
ํ ์คํธ ์ฉ์ด์ฑ์ ํ ๊ท๋ชจ๊ฐ ํ๋๋ ๋ ์ด๋ค ์๋ฃจ์ ์ด ํ์ฅ ๊ฐ๋ฅํ์ง๋ฅผ ๊ฒฐ์ ํ๋ ํต์ฌ ์์์ด๋ค. ๊ฐ ๋ผ์ด๋ธ๋ฌ๋ฆฌ๋ ํ ์คํธ์ ๋ํ ์ ๊ทผ ๋ฐฉ์์ด ๋ค๋ฅด๋ค.
// Riverpod test - isolated container
import 'package:flutter_test/flutter_test.dart';
import 'package:riverpod/riverpod.dart';
void main() {
test('Counter increments', () {
final container = ProviderContainer.test();
// Override dependencies for isolation
final counter = container.read(counterProvider.notifier);
expect(container.read(counterProvider), 0);
counter.increment();
expect(container.read(counterProvider), 1);
});
}// Bloc test - event-driven verification
import 'package:bloc_test/bloc_test.dart';
import 'package:flutter_test/flutter_test.dart';
void main() {
blocTest<AuthenticationBloc, AuthenticationState>(
'emits [loading, success] on valid login',
build: () => AuthenticationBloc(
authRepo: MockAuthRepo(),
tokenStorage: MockTokenStorage(),
),
act: (bloc) => bloc.add(
LoginRequested(email: 'dev@test.com', password: 'secure123'),
),
expect: () => [
isA<AuthenticationLoading>(),
isA<AuthenticationSuccess>(),
],
);
}Riverpod์ ProviderContainer.test()๋ ํ
์คํธ๋ณ๋ก ๊ฒฉ๋ฆฌ๋ ์์กด์ฑ ๊ทธ๋ํ๋ฅผ ์์ฑํ๋ค. Bloc์ blocTest ํฌํผ๋ ์ด๋ฒคํธ ๊ธฐ๋ฐ ์ํคํ
์ฒ์ ๋ง์ถฐ ์ ํํ ์ํ ์ ํ ์ํ์ค๋ฅผ ๊ฒ์ฆํ๋ค. GetX ํ
์คํธ์์๋ Get.testMode = true ์ค์ ๊ณผ ์ปจํธ๋กค๋ฌ ๋ผ์ดํ์ฌ์ดํด์ ์๋ ๊ด๋ฆฌ๊ฐ ํ์ํ๋ฉฐ, CI ํ๊ฒฝ์์ ๋ถ์์ ํ ํ
์คํธ ๊ฒฐ๊ณผ๋ฅผ ์ด๋ํ๊ธฐ ์ฝ๋ค.
Flutter ์ํ ๊ด๋ฆฌ๋ ๋ชจ๋ฐ์ผ ๊ฐ๋ฐ์ ๋ฉด์ ์์ ๊ฐ์ฅ ์์ฃผ ์ถ์ ๋๋ ์ฃผ์ ์ค ํ๋์ ๋๋ค. Riverpod, Bloc, GetX ๊ฐ๊ฐ์ ํธ๋ ์ด๋์คํ๋ฅผ ์ดํดํ๊ณ , ๊ฐ ์๋ฃจ์ ์ด ์ ํฉํ ๊ฒฝ์ฐ์ ์ ํฉํ์ง ์์ ๊ฒฝ์ฐ๋ฅผ ์ค๋ช ํ ์ ์๋ ๊ฒ์ด ์ํคํ ์ฒ์ ์ฑ์๋๋ฅผ ๋ณด์ฌ์ค๋๋ค.
ํ๋จ ๋งคํธ๋ฆญ์ค: ์ต์ ์ ์๋ฃจ์ ์ ํ
ํ๋ก์ ํธ์ ์ ์ฝ ์กฐ๊ฑด์ด ์ต์ ์ ์ ํ์ ๊ฒฐ์ ํ๋ค. ํ ๊ท๋ชจ, ๊ท์ ์๊ฑด, ๊ธฐ์กด ์ฝ๋๋ฒ ์ด์ค๊ฐ ๋ชจ๋ ํ๋จ์ ์ํฅ์ ๋ฏธ์น๋ค.
Riverpod 3.0์ ์ปดํ์ผ ํ์ ์์ ์ฑ์ ์ค์ํ๋ ํ, ์๋ ์๋ฌ ๋ณต๊ตฌ๊ฐ ํฌํจ๋ ๋น๋๊ธฐ ๋ฐ์ดํฐ ํ์นญ์ด ํ์ํ ํ๋ก์ ํธ, ๋๋ ์ฒ์๋ถํฐ ๊ตฌ์ถํ๋ ์ฝ๋๋ฒ ์ด์ค์ ์ ํฉํ๋ค. ํ์ต ๊ณก์ ์ ์ค๊ฐ ์์ค์ด๋ฉฐ, Provider์ ์ต์ํ ๊ฐ๋ฐ์๋ ์์ฐ์ค๋ฝ๊ฒ ์ ํํ ์ ์๋ค.
Bloc 9.0์ ๊ท์ ์ฐ์ (ํํ ํฌ, ํฌ์ค์ผ์ด)์์ ์ด์๋๋ ํ๋ก์ ํธ, ๊ฐ์ฌ๋ฅผ ์ํ ์์ ํ ์ด๋ฒคํธ ์ถ์ ์ด ํ์ํ ํ, ๋๋ ๊ฒฐ์ ์ฒ๋ฆฌ์ ๊ฐ์ ๋ณต์กํ ๋์์ฑ ์ํฌํ๋ก์ฐ๋ฅผ ๋ค๋ฃจ๋ ์ ํ๋ฆฌ์ผ์ด์ ์ ์ ํฉํ๋ค. ๋ณด์ผ๋ฌํ๋ ์ดํธ ๋น์ฉ์ ๋๊ท๋ชจ ์ ์ง๋ณด์์ฑ์ผ๋ก ํ์๋๋ค.
GetX๋ ๋ง์ด๊ทธ๋ ์ด์ ๋น์ฉ์ด ๊ฐ์ฉ ์์ฐ์ ์ด๊ณผํ๋ ๊ธฐ์กด GetX ์ฝ๋๋ฒ ์ด์ค์ ์ ์ง๋ณด์์๋ง ์ ํฉํ๋ค. 2026๋ ์ GetX๋ก ์ ๊ท ํ๋ก์ ํธ๋ฅผ ์์ํ๋ ๊ฒ์ ์ฒซ๋ ๋ถํฐ ๊ธฐ์ ๋ถ์ฑ๋ฅผ ์์ฑํ๋ ๊ฒ์ด๋ค. Flutter ๊ณต์ ๋ฌธ์๋ ๊ถ์ฅ ์๋ฃจ์ ๋ชฉ๋ก์ GetX๋ฅผ ํฌํจํ์ง ์๊ณ ์๋ค.
Flutter ์ํ ๊ด๋ฆฌ ํจํด์ ๋ํ ์ฌํ ํ์ต์ ์ํ ๊ด๋ฆฌ ๊ธฐ์ด ๋ชจ๋์์ ๋ฉด์ ์์ ์ถ์ ๋๋ ๊ธฐ๋ณธ ๊ฐ๋ ์ ๋ค๋ฃจ๊ณ ์๋ค. ํ๋ก๋ฐ์ด๋ ํจํด ๋ชจ๋์์๋ ์ธ ๊ฐ์ง ์๋ฃจ์ ๋ชจ๋์ ์ ์ฉ๋๋ ์์กด์ฑ ์ฃผ์ ์ ๋ต์ ํด์คํ๋ค.
์ฐ์ต์ ์์ํ์ธ์!
๋ฉด์ ์๋ฎฌ๋ ์ดํฐ์ ๊ธฐ์ ํ ์คํธ๋ก ์ง์์ ํ ์คํธํ์ธ์.
๊ฒฐ๋ก
- Riverpod 3.0์ ์ฝ๋ ์์ฑ์ ํตํ ์ปดํ์ผ ํ์ ์์ ์ฑ, ์คํจํ ํ๋ก๋ฐ์ด๋์ ์๋ ์ฌ์๋, ๋ชจ๋ฐ์ผ ๊ธฐ๊ธฐ์ ๋ฐฐํฐ๋ฆฌ ์๋ชจ๋ฅผ ์ค์ด๋ ์ผ์ ์ค์ง/์ฌ๊ฐ ๊ธฐ๋ฅ์ ์ ๊ณตํ๋ค
- Bloc 9.0์ ์์ ํ ๊ฐ์ฌ ๊ธฐ๋ฅ์ ๊ฐ์ถ ์ด๋ฒคํธ ๊ธฐ๋ฐ ์ํ ์ ํ์ ๊ฐ์ ํ๋ฉฐ, ๊ท์ ์ฐ์ ์ ์ํฐํ๋ผ์ด์ฆ ์ ํ๋ฆฌ์ผ์ด์ ํ์ค์ด ๋๊ณ ์๋ค
- GetX๋ ์ฐ๋ฐ์ ์ธ ์ ๋ฐ์ดํธ์ SDK ํธํ์ฑ ๋ฌธ์ ๋ก 2026๋ ์ ์ ์ง๋ณด์ ์๊ธฐ์ ์ง๋ฉดํด ์์ผ๋ฉฐ, ๊ธฐ์กด GetX ํ๋ก์ ํธ๋ Riverpod์ผ๋ก์ ๋จ๊ณ์ ๋ง์ด๊ทธ๋ ์ด์ ์ ๊ณํํด์ผ ํ๋ค
- GetX์์ Riverpod์ผ๋ก์ ๋ง์ด๊ทธ๋ ์ด์ ์ ๋ ๋ผ์ด๋ธ๋ฌ๋ฆฌ๊ฐ ๋์ผ ํ๋ก์ ํธ ๋ด์์ ๊ณต์กดํ ์ ์์ผ๋ฏ๋ก ํ๋ฉด ๋จ์๋ก ์ ๋ฉด ์ฌ์์ฑ ์์ด ์งํํ ์ ์๋ค
- ํ
์คํธ ๊ฒฉ๋ฆฌ์๋ ํฐ ์ฐจ์ด๊ฐ ์๋ค: Riverpod์
ProviderContainer.test(), Bloc์blocTest๋ฅผ ํตํ ์ด๋ฒคํธ ์ํ์ค ๊ฒ์ฆ, GetX๋ ์ทจ์ฝํ ๊ธ๋ก๋ฒ ํ ์คํธ ๋ชจ๋ ์ค์ ์ด ํ์ํ๋ค - ๋ฒ๋ค ํฌ๊ธฐ๋ ๋ชจ๋ฐ์ผ์์ ์ค์ํ๋ค: Riverpod(์ฝ 45KB)๊ณผ Bloc(์ฝ 38KB)์ ํนํ๋ ํจํค์ง๋ฅผ ์ ๊ณตํ๋ ๋ฐ๋ฉด, GetX(์ฝ 120KB)๋ ๋ฏธ์ฌ์ฉ ๊ธฐ๋ฅ๊น์ง ๋ฒ๋ค๋งํ๋ค
์ฐ์ต์ ์์ํ์ธ์!
๋ฉด์ ์๋ฎฌ๋ ์ดํฐ์ ๊ธฐ์ ํ ์คํธ๋ก ์ง์์ ํ ์คํธํ์ธ์.
ํ๊ทธ
๊ณต์
๊ด๋ จ ๊ธฐ์ฌ

Flutter ์ํ ๊ด๋ฆฌ: Riverpod vs BLoC - ์์ ๋น๊ต ๊ฐ์ด๋
Flutter ์ํ ๊ด๋ฆฌ๋ฅผ ์ํ Riverpod์ BLoC์ ์ฌ์ธต ๋น๊ต์ ๋๋ค. ์ํคํ ์ฒ, ์ฑ๋ฅ, ํ ์คํธ ์ฉ์ด์ฑ, ์ฌ์ฉ ์ฌ๋ก๋ฅผ ํตํด ์ต์ ์ ์๋ฃจ์ ์ ์ ํํฉ๋๋ค.

Flutter ํ ์คํธ ์๋ฒฝ ๊ฐ์ด๋ 2026: ์์ ฏ ํ ์คํธ, ํตํฉ ํ ์คํธ ๋ฐ ๋ฉด์ ๋ฒ ์คํธ ํ๋ํฐ์ค
Flutter์ ์์ ฏ ํ ์คํธ, ํตํฉ ํ ์คํธ, ๊ณจ๋ ํ ์คํธ, ๋ชจํน ์ ๋ต์ ์ค์ ์ฝ๋์ ํจ๊ป ํด์คํฉ๋๋ค. 2026๋ ๊ธฐ์ ๋ฉด์ ์์ ์์ฃผ ์ถ์ ๋๋ ํ ์คํธ ํจํด๊ณผ ๋ชจ๋ฒ ๋ต์์ ์ ๊ณตํฉ๋๋ค.

๋ชจ๋ฐ์ผ ๊ฐ๋ฐ์๋ฅผ ์ํ Flutter ๋ฉด์ ์ง๋ฌธ 20์
Flutter ๋ฉด์ ์์ ๊ฐ์ฅ ์์ฃผ ์ถ์ ๋๋ 20๊ฐ์ง ์ง๋ฌธ์ ์ค๋นํ์ญ์์ค. Widget, ์ํ ๊ด๋ฆฌ, Dart, ์ํคํ ์ฒ, ๋ชจ๋ฒ ์ฌ๋ก๋ฅผ ์์ธํ ์ฝ๋ ์์ ์ ํจ๊ป ์ค๋ช ํฉ๋๋ค.