2026๋…„ Flutter ์ƒํƒœ ๊ด€๋ฆฌ ์™„๋ฒฝ ๊ฐ€์ด๋“œ: Riverpod vs Bloc vs GetX ๋น„๊ต ๋ถ„์„

Riverpod 3.0, Bloc 9.0, GetX ์„ธ ๊ฐ€์ง€ Flutter ์ƒํƒœ ๊ด€๋ฆฌ ์†”๋ฃจ์…˜์„ ์ฝ”๋“œ ์˜ˆ์ œ, ์„ฑ๋Šฅ ๋ถ„์„, ํ…Œ์ŠคํŠธ ์ „๋žต ๊ด€์ ์—์„œ ๋น„๊ต ๋ถ„์„ํ•ฉ๋‹ˆ๋‹ค.

Flutter ์ƒํƒœ ๊ด€๋ฆฌ 2026: Riverpod, Bloc, GetX ๋น„๊ต ๋‹ค์ด์–ด๊ทธ๋žจ

Flutter ์ƒํƒœ ๊ด€๋ฆฌ๋Š” ์œ„์ ฏ ๊ฐ„ ๋ฐ์ดํ„ฐ ํ๋ฆ„์„ ์ œ์–ดํ•˜๋Š” ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ ์„ค๊ณ„์˜ ํ•ต์‹ฌ ์š”์†Œ์ด๋‹ค. 2026๋…„ ํ˜„์žฌ Flutter ์ƒํƒœ๊ณ„์—์„œ๋Š” ์„ธ ๊ฐ€์ง€ ์†”๋ฃจ์…˜์ด ์ฃผ๋ฅ˜๋ฅผ ์ฐจ์ง€ํ•˜๊ณ  ์žˆ๋‹ค. ์ปดํŒŒ์ผ ํƒ€์ž„ ์•ˆ์ „์„ฑ์„ ์ œ๊ณตํ•˜๋Š” Riverpod 3.0, ์—”ํ„ฐํ”„๋ผ์ด์ฆˆ๊ธ‰ ์ด๋ฒคํŠธ ์ถ”์  ๊ธฐ๋Šฅ์„ ๊ฐ–์ถ˜ Bloc 9.0, ๊ทธ๋ฆฌ๊ณ  ์ฑ„ํƒ๋ฅ ์€ ๊ฐ์†Œํ•˜๊ณ  ์žˆ์ง€๋งŒ ๊ธฐ์กด ํ”„๋กœ์ ํŠธ์— ์—ฌ์ „ํžˆ ๋‚จ์•„์žˆ๋Š” GetX์ด๋‹ค. ์˜ฌ๋ฐ”๋ฅธ ์†”๋ฃจ์…˜์˜ ์„ ํƒ์€ ํ…Œ์ŠคํŠธ ์šฉ์ด์„ฑ, ํ™•์žฅ์„ฑ, ์žฅ๊ธฐ์ ์ธ ์œ ์ง€๋ณด์ˆ˜ ๋น„์šฉ์— ์ง์ ‘์ ์ธ ์˜ํ–ฅ์„ ๋ฏธ์นœ๋‹ค.

์„ ํƒ ๊ธฐ์ค€ ํ”„๋ ˆ์ž„์›Œํฌ

Riverpod 3.0์€ ์ปดํŒŒ์ผ ํƒ€์ž„ ์•ˆ์ „์„ฑ๊ณผ ์ตœ์†Œํ•œ์˜ ๋ณด์ผ๋Ÿฌํ”Œ๋ ˆ์ดํŠธ๋กœ ๋Œ€๋ถ€๋ถ„์˜ ํ”„๋กœ์ ํŠธ์— ์ ํ•ฉํ•ฉ๋‹ˆ๋‹ค. Bloc 9.0์€ ์ด๋ฒคํŠธ ๊ธฐ๋ฐ˜ ๊ฐ์‚ฌ ์ถ”์ ์ด ํ•„์š”ํ•œ ๊ทœ์ œ ์‚ฐ์—…์˜ ํ‘œ์ค€์ž…๋‹ˆ๋‹ค. GetX๋Š” ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ ์˜ˆ์‚ฐ์ด ์—†๋Š” ๊ธฐ์กด ์ฝ”๋“œ๋ฒ ์ด์Šค ์œ ์ง€๋ณด์ˆ˜์—๋งŒ ๊ณ ๋ คํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.

Riverpod 3.0: ์ปดํŒŒ์ผ ํƒ€์ž„ ์•ˆ์ „์„ฑ๊ณผ ์ž๋™ ์žฌ์‹œ๋„ ๋ฉ”์ปค๋‹ˆ์ฆ˜

Riverpod 3.0์€ Flutter ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์—์„œ ์ƒํƒœ๋ฅผ ์„ ์–ธํ•˜๊ณ  ์†Œ๋น„ํ•˜๋Š” ๋ฐฉ์‹์„ ๊ทผ๋ณธ์ ์œผ๋กœ ๋ณ€ํ™”์‹œ์ผฐ๋‹ค. ์–ด๋…ธํ…Œ์ด์…˜ ๊ธฐ๋ฐ˜ ์ฝ”๋“œ ์ƒ์„ฑ์„ ํ†ตํ•ด ์˜์กด์„ฑ ์˜ค๋ฅ˜๊ฐ€ ๋Ÿฐํƒ€์ž„์ด ์•„๋‹Œ ์ปดํŒŒ์ผ ํƒ€์ž„์— ๊ฐ์ง€๋œ๋‹ค. ์ด๋กœ์จ ๊ธฐ์กด์—๋Š” ์ˆ˜๋™ ํ…Œ์ŠคํŠธ๋กœ๋งŒ ๋ฐœ๊ฒฌํ•  ์ˆ˜ ์žˆ์—ˆ๋˜ ์ „์ฒด ๋ฒ„๊ทธ ์นดํ…Œ๊ณ ๋ฆฌ๊ฐ€ ์ œ๊ฑฐ๋œ๋‹ค.

์ž๋™ ์žฌ์‹œ๋„ ๋ฉ”์ปค๋‹ˆ์ฆ˜์€ ํ”„๋กœ๋ฐ”์ด๋” ๊ณ„์‚ฐ์ด ์‹คํŒจํ•  ๊ฒฝ์šฐ ์ผ์‹œ์ ์ธ ๋„คํŠธ์›Œํฌ ์˜ค๋ฅ˜๋ฅผ ์ž๋™์œผ๋กœ ์ฒ˜๋ฆฌํ•œ๋‹ค. ์„ค์ • ๊ฐ€๋Šฅํ•œ ์ง€์—ฐ ์‹œ๊ฐ„์„ ์ ์šฉํ•œ ์žฌ์‹œ๋„๋ฅผ ํ†ตํ•ด ์—๋Ÿฌ ๋ณต๊ตฌ์šฉ ๋ณด์ผ๋Ÿฌํ”Œ๋ ˆ์ดํŠธ ์ฝ”๋“œ๊ฐ€ ๋Œ€ํญ ๊ฐ์†Œํ•œ๋‹ค.

counter_provider.dartdart
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 ์–ด๋…ธํ…Œ์ด์…˜์„ ํ†ตํ•ด ํ”„๋กœ๋ฐ”์ด๋”์˜ ๋ชจ๋“  ๋ณด์ผ๋Ÿฌํ”Œ๋ ˆ์ดํŠธ๊ฐ€ ์ž๋™ ์ƒ์„ฑ๋œ๋‹ค. ํƒ€์ž… ๋ถˆ์ผ์น˜, ์˜ค๋ฒ„๋ผ์ด๋“œ ๋ˆ„๋ฝ, ์ˆœํ™˜ ์˜์กด์„ฑ์€ ์ปดํŒŒ์ผ ๋‹จ๊ณ„์—์„œ ๊ฐ์ง€๋œ๋‹ค.

user_repository_provider.dartdart
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์—์„œ ๋„์ž…๋œ ๋งˆ์šดํŠธ ์•ˆ์ „์„ฑ ๊ฒ€์‚ฌ๋Š” ํ๊ธฐ๋œ ์œ„์ ฏ์—์„œ ์ฝœ๋ฐฑ์ด ์‹คํ–‰๋˜๋Š” ๊ฒƒ์„ ๋ฐฉ์ง€ํ•œ๋‹ค.

authentication_event.dartdart
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๋ฅผ ํ†ตํ•ด ์ด๋ฒคํŠธ์˜ ์™„์ „ํ•œ ํŒจํ„ด ๋งค์นญ์ด ๋ณด์žฅ๋œ๋‹ค. ์ปดํŒŒ์ผ๋Ÿฌ๊ฐ€ ๋ชจ๋“  ์ด๋ฒคํŠธ ํƒ€์ž…์— ๋Œ€ํ•œ ํ•ธ๋“ค๋Ÿฌ ์กด์žฌ ์—ฌ๋ถ€๋ฅผ ๊ฒ€์ฆํ•œ๋‹ค.

authentication_bloc.dartdart
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์—๋Š” ์ผ๋ฐ˜์ ์ธ ๋™์‹œ์„ฑ ๋ฌธ์ œ๋ฅผ ํ•ด๊ฒฐํ•˜๋Š” ๋‚ด์žฅ ์ด๋ฒคํŠธ ํŠธ๋žœ์Šคํฌ๋จธ๊ฐ€ ์ œ๊ณต๋œ๋‹ค. ๊ฒ€์ƒ‰์–ด ์ž๋™์™„์„ฑ, ์—ฐ์† ๋ฒ„ํŠผ ํƒญ, ์‹ค์‹œ๊ฐ„ ๋ฐ์ดํ„ฐ ์ŠคํŠธ๋ฆผ ์ฒ˜๋ฆฌ๊ฐ€ ์„ ์–ธ์ ์œผ๋กœ ๊ตฌํ˜„ ๊ฐ€๋Šฅํ•˜๋‹ค.

search_bloc.dartdart
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 ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์—์„œ๋Š” ์ปจํŠธ๋กค๋Ÿฌ ๋ผ์ดํ”„์‚ฌ์ดํด ๋ฌธ์ œ์™€ ์•”๋ฌต์  ๊ธ€๋กœ๋ฒŒ ์‹ฑ๊ธ€ํ†ค์œผ๋กœ ์ธํ•œ ๋ฉ”๋ชจ๋ฆฌ ๋ˆ„์ˆ˜๊ฐ€ ๋ณด๊ณ ๋˜๊ณ  ์žˆ๋‹ค.

counter_controller.dart (GetX pattern)dart
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์œผ๋กœ์˜ ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜์€ ๋‹จ๊ณ„์ ์œผ๋กœ ์ง„ํ–‰ํ•  ์ˆ˜ ์žˆ๋‹ค. ๋‘ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋Š” ๋™์ผํ•œ ํ”„๋กœ์ ํŠธ ๋‚ด์—์„œ ๊ณต์กดํ•  ์ˆ˜ ์žˆ์œผ๋ฏ€๋กœ ์ „๋ฉด์ ์ธ ์žฌ์ž‘์„ฑ ์—†์ด ํ™”๋ฉด ๋‹จ์œ„๋กœ ์ „ํ™˜์„ ์ง„ํ–‰ํ•  ์ˆ˜ ์žˆ๋‹ค.

dart
// 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();
  }
}
dart
// 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 ๋ฒˆ๋“ค ํฌ๊ธฐ ์ฃผ์˜์‚ฌํ•ญ

GetX๋Š” ๋ผ์šฐํŒ…, ์˜์กด์„ฑ ์ฃผ์ž…, HTTP ํด๋ผ์ด์–ธํŠธ, ์ƒํƒœ ๊ด€๋ฆฌ๋ฅผ ๋‹จ์ผ ํŒจํ‚ค์ง€์— ๋ฒˆ๋“ค๋งํ•ฉ๋‹ˆ๋‹ค. ์ƒํƒœ ๊ด€๋ฆฌ๋งŒ ์‚ฌ์šฉํ•˜๋Š” ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์—์„œ๋„ 120KB ์ „์ฒด ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋ฅผ ์ž„ํฌํŠธํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค. Riverpod๊ณผ Bloc์€ ๋‹จ์ผ ์ฑ…์ž„์— ํŠนํ™”๋œ ๊ฒฝ๋Ÿ‰ ํŒจํ‚ค์ง€์ž…๋‹ˆ๋‹ค.

๊ฐ ์†”๋ฃจ์…˜์˜ ํ…Œ์ŠคํŠธ ์ „๋žต

ํ…Œ์ŠคํŠธ ์šฉ์ด์„ฑ์€ ํŒ€ ๊ทœ๋ชจ๊ฐ€ ํ™•๋Œ€๋  ๋•Œ ์–ด๋–ค ์†”๋ฃจ์…˜์ด ํ™•์žฅ ๊ฐ€๋Šฅํ•œ์ง€๋ฅผ ๊ฒฐ์ •ํ•˜๋Š” ํ•ต์‹ฌ ์š”์†Œ์ด๋‹ค. ๊ฐ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋Š” ํ…Œ์ŠคํŠธ์— ๋Œ€ํ•œ ์ ‘๊ทผ ๋ฐฉ์‹์ด ๋‹ค๋ฅด๋‹ค.

dart
// 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);
  });
}
dart
// 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
#state-management
#riverpod
#bloc
#getx

๊ณต์œ 

๊ด€๋ จ ๊ธฐ์‚ฌ

Flutter ์ƒํƒœ ๊ด€๋ฆฌ๋ฅผ ์œ„ํ•œ Riverpod์™€ BLoC ๋น„๊ต

Flutter ์ƒํƒœ ๊ด€๋ฆฌ: Riverpod vs BLoC - ์™„์ „ ๋น„๊ต ๊ฐ€์ด๋“œ

Flutter ์ƒํƒœ ๊ด€๋ฆฌ๋ฅผ ์œ„ํ•œ Riverpod์™€ BLoC์˜ ์‹ฌ์ธต ๋น„๊ต์ž…๋‹ˆ๋‹ค. ์•„ํ‚คํ…์ฒ˜, ์„ฑ๋Šฅ, ํ…Œ์ŠคํŠธ ์šฉ์ด์„ฑ, ์‚ฌ์šฉ ์‚ฌ๋ก€๋ฅผ ํ†ตํ•ด ์ตœ์ ์˜ ์†”๋ฃจ์…˜์„ ์„ ํƒํ•ฉ๋‹ˆ๋‹ค.

Flutter ํ…Œ์ŠคํŠธ: ์œ„์ ฏ ํ…Œ์ŠคํŠธ, ํ†ตํ•ฉ ํ…Œ์ŠคํŠธ ๋ฐ ๋ฉด์ ‘ ๋ฒ ์ŠคํŠธ ํ”„๋ž™ํ‹ฐ์Šค 2026

Flutter ํ…Œ์ŠคํŠธ ์™„๋ฒฝ ๊ฐ€์ด๋“œ 2026: ์œ„์ ฏ ํ…Œ์ŠคํŠธ, ํ†ตํ•ฉ ํ…Œ์ŠคํŠธ ๋ฐ ๋ฉด์ ‘ ๋ฒ ์ŠคํŠธ ํ”„๋ž™ํ‹ฐ์Šค

Flutter์˜ ์œ„์ ฏ ํ…Œ์ŠคํŠธ, ํ†ตํ•ฉ ํ…Œ์ŠคํŠธ, ๊ณจ๋“  ํ…Œ์ŠคํŠธ, ๋ชจํ‚น ์ „๋žต์„ ์‹ค์ „ ์ฝ”๋“œ์™€ ํ•จ๊ป˜ ํ•ด์„คํ•ฉ๋‹ˆ๋‹ค. 2026๋…„ ๊ธฐ์ˆ  ๋ฉด์ ‘์—์„œ ์ž์ฃผ ์ถœ์ œ๋˜๋Š” ํ…Œ์ŠคํŠธ ํŒจํ„ด๊ณผ ๋ชจ๋ฒ” ๋‹ต์•ˆ์„ ์ œ๊ณตํ•ฉ๋‹ˆ๋‹ค.

๋ชจ๋ฐ”์ผ ๊ฐœ๋ฐœ์ž๋ฅผ ์œ„ํ•œ Flutter ๋ฉด์ ‘ ์งˆ๋ฌธ ๊ฐ€์ด๋“œ

๋ชจ๋ฐ”์ผ ๊ฐœ๋ฐœ์ž๋ฅผ ์œ„ํ•œ Flutter ๋ฉด์ ‘ ์งˆ๋ฌธ 20์„ 

Flutter ๋ฉด์ ‘์—์„œ ๊ฐ€์žฅ ์ž์ฃผ ์ถœ์ œ๋˜๋Š” 20๊ฐ€์ง€ ์งˆ๋ฌธ์„ ์ค€๋น„ํ•˜์‹ญ์‹œ์˜ค. Widget, ์ƒํƒœ ๊ด€๋ฆฌ, Dart, ์•„ํ‚คํ…์ฒ˜, ๋ชจ๋ฒ” ์‚ฌ๋ก€๋ฅผ ์ƒ์„ธํ•œ ์ฝ”๋“œ ์˜ˆ์ œ์™€ ํ•จ๊ป˜ ์„ค๋ช…ํ•ฉ๋‹ˆ๋‹ค.