Flutter: ์ฒซ ๋ฒˆ์งธ ํฌ๋กœ์Šค ํ”Œ๋žซํผ ์•ฑ ๊ตฌ์ถ•ํ•˜๊ธฐ

Flutter์™€ Dart๋ฅผ ์‚ฌ์šฉํ•œ ํฌ๋กœ์Šค ํ”Œ๋žซํผ ๋ชจ๋ฐ”์ผ ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ ๊ตฌ์ถ• ์™„์ „ ๊ฐ€์ด๋“œ. Widget, ์ƒํƒœ ๊ด€๋ฆฌ, ๋‚ด๋น„๊ฒŒ์ด์…˜, ์ดˆ๋ณด์ž๋ฅผ ์œ„ํ•œ ๋ชจ๋ฒ” ์‚ฌ๋ก€๋ฅผ ๋‹ค๋ฃน๋‹ˆ๋‹ค.

Flutter์™€ Dart๋กœ ์ฒซ ๋ฒˆ์งธ ๋ชจ๋ฐ”์ผ ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์„ ๊ตฌ์ถ•ํ•˜๋Š” ๊ฐ€์ด๋“œ

Flutter๋Š” ๋‹จ์ผ ์ฝ”๋“œ๋ฒ ์ด์Šค์—์„œ iOS์™€ Android ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์„ ๊ตฌ์ถ•ํ•  ์ˆ˜ ์žˆ๊ฒŒ ํ•˜์—ฌ ๋ชจ๋ฐ”์ผ ๊ฐœ๋ฐœ์— ํ˜๋ช…์„ ์ผ์œผํ‚ค๊ณ  ์žˆ์Šต๋‹ˆ๋‹ค. Google์ด ๊ฐœ๋ฐœํ•œ ์ด ํ”„๋ ˆ์ž„์›Œํฌ๋Š” Skia ๋ Œ๋”๋ง ์—”์ง„๊ณผ ์„ ์–ธ์  Widget ์‹œ์Šคํ…œ์„ ํ†ตํ•ด ๋„ค์ดํ‹ฐ๋ธŒ ์ˆ˜์ค€์˜ ์„ฑ๋Šฅ๊ณผ ๋›ฐ์–ด๋‚œ ์ƒ์‚ฐ์„ฑ์„ ๊ฒฐํ•ฉํ•ฉ๋‹ˆ๋‹ค. ๋ณธ ๊ฐ€์ด๋“œ์—์„œ๋Š” ์„ค์น˜๋ถ€ํ„ฐ ํ”„๋กœ๋•์…˜ ๋ชจ๋ฒ” ์‚ฌ๋ก€๊นŒ์ง€ ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ ๊ตฌ์ถ•์˜ ์ „์ฒด ๊ณผ์ •์„ ๋‹ค๋ฃน๋‹ˆ๋‹ค.

Flutter 3.27 - 2026๋…„ 1์›”

Flutter 3.27์—๋Š” ์ค‘์š”ํ•œ ๊ฐœ์„  ์‚ฌํ•ญ์ด ๋„์ž…๋˜์—ˆ์Šต๋‹ˆ๋‹ค. Material 3 ๋„ค์ดํ‹ฐ๋ธŒ ์ง€์›์ด ๊ธฐ๋ณธ์œผ๋กœ ํ™œ์„ฑํ™”๋˜๊ณ , ์ตœ์ ํ™”๋œ Impeller ์• ๋‹ˆ๋ฉ”์ด์…˜์ด ์ถ”๊ฐ€๋˜์—ˆ์œผ๋ฉฐ, ์‹คํ—˜์  ๋งคํฌ๋กœ๋ฅผ ํฌํ•จํ•œ Dart 3.6๊ณผ์˜ ํ†ตํ•ฉ์ด ์ด๋ฃจ์–ด์กŒ์Šต๋‹ˆ๋‹ค. ํ”„๋ ˆ์ž„์›Œํฌ๋Š” ์ตœ์ ์˜ ์„ฑ๋Šฅ์„ ์œ„ํ•ด ARM64 ๋„ค์ดํ‹ฐ๋ธŒ๋กœ ์ปดํŒŒ์ผ๋ฉ๋‹ˆ๋‹ค.

ํ™˜๊ฒฝ ์„ค์ • ๋ฐ ์„ค์น˜

Flutter ์„ค์ •์—๋Š” ๋ช‡ ๊ฐ€์ง€ ๊ตฌ์„ฑ ๋‹จ๊ณ„๊ฐ€ ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค. Flutter SDK์—๋Š” ํ”„๋ ˆ์ž„์›Œํฌ, ๋นŒ๋“œ ๋„๊ตฌ, Dart ํŒจํ‚ค์ง€ ๋งค๋‹ˆ์ € ๋“ฑ ํ•„์š”ํ•œ ๋ชจ๋“  ๊ฒƒ์ด ํฌํ•จ๋˜์–ด ์žˆ์Šต๋‹ˆ๋‹ค.

bash
# terminal
# Download Flutter SDK (macOS/Linux)
git clone https://github.com/flutter/flutter.git -b stable
export PATH="$PATH:`pwd`/flutter/bin"

# Verify installation and dependencies
flutter doctor

# Create a new project
flutter create --org com.example my_app
cd my_app

# Run in development mode
flutter run

flutter doctor ๋ช…๋ น์–ด๋Š” Android Studio, Xcode(macOS), ์—๋ฎฌ๋ ˆ์ดํ„ฐ ์„ค์ • ๋“ฑ ๋ชจ๋“  ์ข…์†์„ฑ์ด ์„ค์น˜๋˜์—ˆ๋Š”์ง€ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค.

yaml
# pubspec.yaml
name: my_app
description: Cross-platform Flutter application
publish_to: 'none'
version: 1.0.0+1

environment:
  sdk: '>=3.6.0 <4.0.0'

dependencies:
  flutter:
    sdk: flutter
  # UI and design
  cupertino_icons: ^1.0.8
  google_fonts: ^6.2.1
  # State management
  flutter_riverpod: ^2.6.1
  # Navigation
  go_router: ^14.6.2
  # HTTP and API
  dio: ^5.7.0
  # Local storage
  shared_preferences: ^2.3.4

dev_dependencies:
  flutter_test:
    sdk: flutter
  flutter_lints: ^5.0.0

flutter:
  uses-material-design: true

์ด pubspec.yaml ํŒŒ์ผ์€ ์ตœ์‹  Flutter ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์— ํ•„์š”ํ•œ ์ข…์†์„ฑ์„ ๊ตฌ์„ฑํ•ฉ๋‹ˆ๋‹ค.

Flutter ํ”„๋กœ์ ํŠธ ์•„ํ‚คํ…์ฒ˜

๋ช…ํ™•ํ•œ ํ”„๋กœ์ ํŠธ ๊ตฌ์กฐ๋Š” ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์˜ ์œ ์ง€๋ณด์ˆ˜์™€ ํ™•์žฅ์„ ์šฉ์ดํ•˜๊ฒŒ ํ•ฉ๋‹ˆ๋‹ค. ์ด ๊ตฌ์„ฑ์€ ์ฑ…์ž„์„ ๋ช…ํ™•ํ•˜๊ฒŒ ๋ถ„๋ฆฌํ•ฉ๋‹ˆ๋‹ค.

text
lib/
โ”œโ”€โ”€ main.dart                 # Entry point
โ”œโ”€โ”€ app.dart                  # App configuration
โ”œโ”€โ”€ core/
โ”‚   โ”œโ”€โ”€ constants/           # Colors, dimensions, strings
โ”‚   โ”œโ”€โ”€ theme/               # Material 3 theme
โ”‚   โ””โ”€โ”€ utils/               # Utility functions
โ”œโ”€โ”€ features/
โ”‚   โ”œโ”€โ”€ auth/                # Authentication feature
โ”‚   โ”‚   โ”œโ”€โ”€ data/           # Repositories, data sources
โ”‚   โ”‚   โ”œโ”€โ”€ domain/         # Models, use cases
โ”‚   โ”‚   โ””โ”€โ”€ presentation/   # Screens, widgets, providers
โ”‚   โ””โ”€โ”€ home/               # Home feature
โ”‚       โ”œโ”€โ”€ data/
โ”‚       โ”œโ”€โ”€ domain/
โ”‚       โ””โ”€โ”€ presentation/
โ”œโ”€โ”€ shared/
โ”‚   โ”œโ”€โ”€ widgets/            # Reusable widgets
โ”‚   โ””โ”€โ”€ providers/          # Shared providers
โ””โ”€โ”€ routing/
    โ””โ”€โ”€ app_router.dart     # Route configuration

"Feature-First" ์•„ํ‚คํ…์ฒ˜๋Š” ํ•˜๋‚˜์˜ ๊ธฐ๋Šฅ๊ณผ ๊ด€๋ จ๋œ ๋ชจ๋“  ์ฝ”๋“œ๋ฅผ ๊ฐ™์€ ํด๋”์— ๊ทธ๋ฃนํ™”ํ•˜์—ฌ ํƒ์ƒ‰๊ณผ ๋ฆฌํŒฉํ† ๋ง์„ ์šฉ์ดํ•˜๊ฒŒ ํ•ฉ๋‹ˆ๋‹ค.

Feature-First ์•„ํ‚คํ…์ฒ˜

Feature-First ์ ‘๊ทผ ๋ฐฉ์‹์€ ๊ธฐ์ˆ ์  ์œ ํ˜•์ด ์•„๋‹Œ ๋น„์ฆˆ๋‹ˆ์Šค ๊ธฐ๋Šฅ๋ณ„๋กœ ์ฝ”๋“œ๋ฅผ ๊ตฌ์„ฑํ•ฉ๋‹ˆ๋‹ค. ๊ฐ Feature๋Š” ์ž์ฒด ๋ชจ๋ธ, Widget, ๋กœ์ง์„ ํฌํ•จํ•˜์—ฌ ์ฝ”๋“œ์˜ ๋ชจ๋“ˆ์„ฑ๊ณผ ํ…Œ์ŠคํŠธ ์šฉ์ด์„ฑ์„ ๋†’์ž…๋‹ˆ๋‹ค.

Flutter Widget ์ดํ•ดํ•˜๊ธฐ

Widget์€ Flutter์˜ ๊ธฐ๋ณธ ๊ตฌ์„ฑ ์š”์†Œ์ž…๋‹ˆ๋‹ค. ๋ฒ„ํŠผ, ํ…์ŠคํŠธ, ๋ ˆ์ด์•„์›ƒ, ์‹ฌ์ง€์–ด ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ ์ž์ฒด๊นŒ์ง€ ๋ชจ๋“  ๊ฒƒ์ด Widget์ž…๋‹ˆ๋‹ค. Flutter๋Š” ๋‘ ๊ฐ€์ง€ ์œ ํ˜•์˜ Widget์„ ์ œ๊ณตํ•ฉ๋‹ˆ๋‹ค: StatelessWidget(์ƒํƒœ ์—†์Œ)๊ณผ StatefulWidget(๋กœ์ปฌ ์ƒํƒœ ๋ณด์œ ).

lib/shared/widgets/custom_button.dartdart
import 'package:flutter/material.dart';

/// Custom reusable button widget throughout the application.
/// Automatically handles loading and disabled states.
class CustomButton extends StatelessWidget {
  // Required and optional widget parameters
  final String label;
  final VoidCallback? onPressed;
  final bool isLoading;
  final bool isOutlined;

  // Constructor with named parameters for clarity
  const CustomButton({
    super.key,
    required this.label,
    this.onPressed,
    this.isLoading = false,
    this.isOutlined = false,
  });

  
  Widget build(BuildContext context) {
    // Access theme for consistent styles
    final theme = Theme.of(context);

    // Conditional build based on button type
    if (isOutlined) {
      return OutlinedButton(
        // Disable button during loading
        onPressed: isLoading ? null : onPressed,
        style: OutlinedButton.styleFrom(
          padding: const EdgeInsets.symmetric(
            horizontal: 24,
            vertical: 16,
          ),
          shape: RoundedRectangleBorder(
            borderRadius: BorderRadius.circular(12),
          ),
        ),
        child: _buildChild(theme),
      );
    }

    return FilledButton(
      onPressed: isLoading ? null : onPressed,
      style: FilledButton.styleFrom(
        padding: const EdgeInsets.symmetric(
          horizontal: 24,
          vertical: 16,
        ),
        shape: RoundedRectangleBorder(
          borderRadius: BorderRadius.circular(12),
        ),
      ),
      child: _buildChild(theme),
    );
  }

  /// Builds button content with loading state handling.
  Widget _buildChild(ThemeData theme) {
    if (isLoading) {
      return const SizedBox(
        height: 20,
        width: 20,
        child: CircularProgressIndicator(
          strokeWidth: 2,
          color: Colors.white,
        ),
      );
    }
    return Text(label);
  }
}

์ด Widget์€ ๋กœ๋”ฉ ์ƒํƒœ์˜ ์ž๋™ ์ฒ˜๋ฆฌ๋ฅผ ํฌํ•จํ•œ ๋ฒ„ํŠผ ํ‘œ์‹œ ๋กœ์ง์„ ์บก์Аํ™”ํ•ฉ๋‹ˆ๋‹ค.

lib/shared/widgets/user_card.dartdart
import 'package:flutter/material.dart';
import '../../features/auth/domain/models/user.dart';

/// Card displaying user information.
/// Uses Material 3 with consistent elevation and shape.
class UserCard extends StatelessWidget {
  final User user;
  final VoidCallback? onTap;

  const UserCard({
    super.key,
    required this.user,
    this.onTap,
  });

  
  Widget build(BuildContext context) {
    final theme = Theme.of(context);

    // Material 3 Card with InkWell for ripple effect
    return Card(
      // Adaptive elevation based on theme
      elevation: 2,
      // Consistent rounded shape
      shape: RoundedRectangleBorder(
        borderRadius: BorderRadius.circular(16),
      ),
      // Clipping so ripple respects borders
      clipBehavior: Clip.antiAlias,
      child: InkWell(
        onTap: onTap,
        child: Padding(
          padding: const EdgeInsets.all(16),
          child: Row(
            children: [
              // Avatar with image or initials
              CircleAvatar(
                radius: 28,
                backgroundImage: user.avatarUrl != null
                    ? NetworkImage(user.avatarUrl!)
                    : null,
                child: user.avatarUrl == null
                    ? Text(user.initials)
                    : null,
              ),
              const SizedBox(width: 16),
              // User information
              Expanded(
                child: Column(
                  crossAxisAlignment: CrossAxisAlignment.start,
                  children: [
                    Text(
                      user.displayName,
                      style: theme.textTheme.titleMedium?.copyWith(
                        fontWeight: FontWeight.w600,
                      ),
                    ),
                    const SizedBox(height: 4),
                    Text(
                      user.email,
                      style: theme.textTheme.bodyMedium?.copyWith(
                        color: theme.colorScheme.onSurfaceVariant,
                      ),
                    ),
                  ],
                ),
              ),
              // Navigation icon
              Icon(
                Icons.chevron_right,
                color: theme.colorScheme.onSurfaceVariant,
              ),
            ],
          ),
        ),
      ),
    );
  }
}

Widget ํ•ฉ์„ฑ์„ ํ†ตํ•ด ๋‹จ์ˆœํ•˜๊ณ  ์žฌ์‚ฌ์šฉ ๊ฐ€๋Šฅํ•œ ๋ธ”๋ก์œผ๋กœ๋ถ€ํ„ฐ ๋ณต์žกํ•œ ์ธํ„ฐํŽ˜์ด์Šค๋ฅผ ๊ตฌ์ถ•ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

Riverpod๋ฅผ ํ™œ์šฉํ•œ ์ƒํƒœ ๊ด€๋ฆฌ

Riverpod๋Š” Flutter์—์„œ ์ƒํƒœ ๊ด€๋ฆฌ๋ฅผ ์œ„ํ•œ ํ˜„๋Œ€์ ์ธ ์†”๋ฃจ์…˜์ž…๋‹ˆ๋‹ค. ์ด ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋Š” Widget ๊ฐ„์— ์ƒํƒœ๋ฅผ ๊ณต์œ ํ•˜๊ธฐ ์œ„ํ•œ ์„ ์–ธ์ ์ด๊ณ  ํƒ€์ž… ์•ˆ์ „ํ•˜๋ฉฐ ํ…Œ์ŠคํŠธ ๊ฐ€๋Šฅํ•œ ์ ‘๊ทผ ๋ฐฉ์‹์„ ์ œ๊ณตํ•ฉ๋‹ˆ๋‹ค.

lib/features/auth/domain/models/user.dartdart
/// Immutable user model with fromJson factory.
class User {
  final String id;
  final String email;
  final String displayName;
  final String? avatarUrl;
  final DateTime createdAt;

  const User({
    required this.id,
    required this.email,
    required this.displayName,
    this.avatarUrl,
    required this.createdAt,
  });

  /// Generates initials from display name.
  String get initials {
    final parts = displayName.split(' ');
    if (parts.length >= 2) {
      return '${parts[0][0]}${parts[1][0]}'.toUpperCase();
    }
    return displayName.substring(0, 2).toUpperCase();
  }

  /// Creates instance from JSON (API response).
  factory User.fromJson(Map<String, dynamic> json) {
    return User(
      id: json['id'] as String,
      email: json['email'] as String,
      displayName: json['display_name'] as String,
      avatarUrl: json['avatar_url'] as String?,
      createdAt: DateTime.parse(json['created_at'] as String),
    );
  }

  /// Converts to JSON for API submission.
  Map<String, dynamic> toJson() {
    return {
      'id': id,
      'email': email,
      'display_name': displayName,
      'avatar_url': avatarUrl,
      'created_at': createdAt.toIso8601String(),
    };
  }
}
lib/features/auth/presentation/providers/auth_provider.dartdart
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../../data/repositories/auth_repository.dart';
import '../../domain/models/user.dart';

/// Authentication state representing different possible cases.
sealed class AuthState {
  const AuthState();
}

class AuthInitial extends AuthState {
  const AuthInitial();
}

class AuthLoading extends AuthState {
  const AuthLoading();
}

class AuthAuthenticated extends AuthState {
  final User user;
  const AuthAuthenticated(this.user);
}

class AuthUnauthenticated extends AuthState {
  const AuthUnauthenticated();
}

class AuthError extends AuthState {
  final String message;
  const AuthError(this.message);
}

/// Provider for authentication repository.
final authRepositoryProvider = Provider<AuthRepository>((ref) {
  return AuthRepository();
});

/// Main provider managing authentication state.
final authProvider = StateNotifierProvider<AuthNotifier, AuthState>((ref) {
  final repository = ref.watch(authRepositoryProvider);
  return AuthNotifier(repository);
});

/// Notifier handling authentication logic.
class AuthNotifier extends StateNotifier<AuthState> {
  final AuthRepository _repository;

  AuthNotifier(this._repository) : super(const AuthInitial()) {
    // Check initial state on startup
    checkAuthStatus();
  }

  /// Checks if a user is already logged in.
  Future<void> checkAuthStatus() async {
    state = const AuthLoading();
    try {
      final user = await _repository.getCurrentUser();
      if (user != null) {
        state = AuthAuthenticated(user);
      } else {
        state = const AuthUnauthenticated();
      }
    } catch (e) {
      state = const AuthUnauthenticated();
    }
  }

  /// Signs in user with email and password.
  Future<void> signIn(String email, String password) async {
    state = const AuthLoading();
    try {
      final user = await _repository.signIn(email, password);
      state = AuthAuthenticated(user);
    } catch (e) {
      state = AuthError(e.toString());
    }
  }

  /// Creates a new user account.
  Future<void> signUp(String email, String password, String displayName) async {
    state = const AuthLoading();
    try {
      final user = await _repository.signUp(email, password, displayName);
      state = AuthAuthenticated(user);
    } catch (e) {
      state = AuthError(e.toString());
    }
  }

  /// Signs out the user.
  Future<void> signOut() async {
    state = const AuthLoading();
    await _repository.signOut();
    state = const AuthUnauthenticated();
  }
}

Sealed Class ํŒจํ„ด(Dart 3.0 ์ด์ƒ)์€ Switch ํ‘œํ˜„์‹์„ ์‚ฌ์šฉํ•  ๋•Œ ๋ชจ๋“  ์ƒํƒœ ์ผ€์ด์Šค๊ฐ€ ์ฒ˜๋ฆฌ๋˜๋„๋ก ๋ณด์žฅํ•ฉ๋‹ˆ๋‹ค.

Flutter ๋ฉด์ ‘ ์ค€๋น„๊ฐ€ ๋˜์…จ๋‚˜์š”?

์ธํ„ฐ๋ž™ํ‹ฐ๋ธŒ ์‹œ๋ฎฌ๋ ˆ์ดํ„ฐ, flashcards, ๊ธฐ์ˆ  ํ…Œ์ŠคํŠธ๋กœ ์—ฐ์Šตํ•˜์„ธ์š”.

GoRouter๋ฅผ ํ™œ์šฉํ•œ ๋‚ด๋น„๊ฒŒ์ด์…˜

GoRouter๋Š” Flutter์—์„œ์˜ ์„ ์–ธ์  ๋‚ด๋น„๊ฒŒ์ด์…˜์„ ๊ฐ„์†Œํ™”ํ•ฉ๋‹ˆ๋‹ค. ์ด ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋Š” ๋ผ์šฐํŠธ, ๋งค๊ฐœ๋ณ€์ˆ˜, ๋ฆฌ๋‹ค์ด๋ ‰ํŠธ, ์ค‘์ฒฉ ๋‚ด๋น„๊ฒŒ์ด์…˜์„ ๊ด€๋ฆฌํ•ฉ๋‹ˆ๋‹ค.

lib/routing/app_router.dartdart
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:go_router/go_router.dart';
import '../features/auth/presentation/providers/auth_provider.dart';
import '../features/auth/presentation/screens/login_screen.dart';
import '../features/auth/presentation/screens/register_screen.dart';
import '../features/home/presentation/screens/home_screen.dart';
import '../features/home/presentation/screens/profile_screen.dart';
import '../shared/widgets/shell_scaffold.dart';

/// Provider for router with authentication handling.
final routerProvider = Provider<GoRouter>((ref) {
  // Listen to authentication state changes
  final authState = ref.watch(authProvider);

  return GoRouter(
    // Initial route
    initialLocation: '/',
    // Refresh router when auth changes
    refreshListenable: GoRouterRefreshStream(ref, authProvider),
    // Handle redirects based on authentication
    redirect: (context, state) {
      final isAuthenticated = authState is AuthAuthenticated;
      final isAuthRoute = state.matchedLocation.startsWith('/auth');

      // Not authenticated on protected route โ†’ login
      if (!isAuthenticated && !isAuthRoute) {
        return '/auth/login';
      }

      // Authenticated on auth route โ†’ home
      if (isAuthenticated && isAuthRoute) {
        return '/';
      }

      return null; // No redirect
    },
    routes: [
      // Auth routes (without shell)
      GoRoute(
        path: '/auth/login',
        name: 'login',
        builder: (context, state) => const LoginScreen(),
      ),
      GoRoute(
        path: '/auth/register',
        name: 'register',
        builder: (context, state) => const RegisterScreen(),
      ),
      // Protected routes with shell (bottom navigation)
      ShellRoute(
        builder: (context, state, child) {
          return ShellScaffold(child: child);
        },
        routes: [
          GoRoute(
            path: '/',
            name: 'home',
            builder: (context, state) => const HomeScreen(),
          ),
          GoRoute(
            path: '/profile',
            name: 'profile',
            builder: (context, state) => const ProfileScreen(),
          ),
          GoRoute(
            path: '/profile/:userId',
            name: 'userProfile',
            builder: (context, state) {
              // Extract route parameter
              final userId = state.pathParameters['userId']!;
              return ProfileScreen(userId: userId);
            },
          ),
        ],
      ),
    ],
    // Custom error page
    errorBuilder: (context, state) => Scaffold(
      body: Center(
        child: Text('Page not found: ${state.error}'),
      ),
    ),
  );
});

/// Stream to trigger router refresh.
class GoRouterRefreshStream extends ChangeNotifier {
  GoRouterRefreshStream(Ref ref, StateNotifierProvider provider) {
    ref.listen(provider, (previous, next) {
      notifyListeners();
    });
  }
}

์ธ์ฆ ์ƒํƒœ์— ๊ธฐ๋ฐ˜ํ•œ ์ž๋™ ๋ฆฌ๋‹ค์ด๋ ‰ํŠธ๋ฅผ ํ†ตํ•ด ๋ณดํ˜ธ๋œ ๋ผ์šฐํŠธ๊ฐ€ ์ธ์ฆ๋˜์ง€ ์•Š์€ ์‚ฌ์šฉ์ž๋กœ๋ถ€ํ„ฐ ์ ‘๊ทผ ๋ถˆ๊ฐ€๋Šฅํ•˜๋„๋ก ๋ณด์žฅํ•ฉ๋‹ˆ๋‹ค.

lib/shared/widgets/shell_scaffold.dartdart
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';

/// Scaffold with bottom navigation bar for protected routes.
class ShellScaffold extends StatelessWidget {
  final Widget child;

  const ShellScaffold({
    super.key,
    required this.child,
  });

  
  Widget build(BuildContext context) {
    return Scaffold(
      body: child,
      bottomNavigationBar: NavigationBar(
        // Determine active index based on route
        selectedIndex: _calculateSelectedIndex(context),
        onDestinationSelected: (index) => _onItemTapped(index, context),
        destinations: const [
          NavigationDestination(
            icon: Icon(Icons.home_outlined),
            selectedIcon: Icon(Icons.home),
            label: 'Home',
          ),
          NavigationDestination(
            icon: Icon(Icons.person_outline),
            selectedIcon: Icon(Icons.person),
            label: 'Profile',
          ),
        ],
      ),
    );
  }

  /// Calculates navigation index based on current route.
  int _calculateSelectedIndex(BuildContext context) {
    final location = GoRouterState.of(context).matchedLocation;
    if (location.startsWith('/profile')) return 1;
    return 0;
  }

  /// Navigates to route corresponding to index.
  void _onItemTapped(int index, BuildContext context) {
    switch (index) {
      case 0:
        context.goNamed('home');
      case 1:
        context.goNamed('profile');
    }
  }
}

Material 3์˜ NavigationBar๋Š” ๋ผ์ดํŠธ ํ…Œ๋งˆ์™€ ๋‹คํฌ ํ…Œ๋งˆ์— ์ž๋™์œผ๋กœ ์ ์‘ํ•ฉ๋‹ˆ๋‹ค.

ํ™”๋ฉด๊ณผ ํผ

์ธํ„ฐ๋ž™ํ‹ฐ๋ธŒ ํ™”๋ฉด ์ƒ์„ฑ์€ Widget, ์ƒํƒœ ๊ด€๋ฆฌ, ํผ ์œ ํšจ์„ฑ ๊ฒ€์‚ฌ๋ฅผ ๊ฒฐํ•ฉํ•˜์—ฌ ์ด๋ฃจ์–ด์ง‘๋‹ˆ๋‹ค.

lib/features/auth/presentation/screens/login_screen.dartdart
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:go_router/go_router.dart';
import '../providers/auth_provider.dart';
import '../../../../shared/widgets/custom_button.dart';

/// Login screen with validated form.
class LoginScreen extends ConsumerStatefulWidget {
  const LoginScreen({super.key});

  
  ConsumerState<LoginScreen> createState() => _LoginScreenState();
}

class _LoginScreenState extends ConsumerState<LoginScreen> {
  // Global key for the form
  final _formKey = GlobalKey<FormState>();
  // Controllers for text fields
  final _emailController = TextEditingController();
  final _passwordController = TextEditingController();
  // Local state for password visibility
  bool _obscurePassword = true;

  
  void dispose() {
    // Resource cleanup
    _emailController.dispose();
    _passwordController.dispose();
    super.dispose();
  }

  /// Submits the login form.
  Future<void> _submit() async {
    // Validate all fields
    if (_formKey.currentState!.validate()) {
      await ref.read(authProvider.notifier).signIn(
            _emailController.text.trim(),
            _passwordController.text,
          );
    }
  }

  
  Widget build(BuildContext context) {
    final theme = Theme.of(context);
    final authState = ref.watch(authProvider);
    final isLoading = authState is AuthLoading;

    // Listen for errors to display snackbar
    ref.listen<AuthState>(authProvider, (previous, next) {
      if (next is AuthError) {
        ScaffoldMessenger.of(context).showSnackBar(
          SnackBar(
            content: Text(next.message),
            backgroundColor: theme.colorScheme.error,
          ),
        );
      }
    });

    return Scaffold(
      body: SafeArea(
        child: Center(
          child: SingleChildScrollView(
            padding: const EdgeInsets.all(24),
            child: Form(
              key: _formKey,
              child: Column(
                mainAxisAlignment: MainAxisAlignment.center,
                crossAxisAlignment: CrossAxisAlignment.stretch,
                children: [
                  // Logo or title
                  Icon(
                    Icons.flutter_dash,
                    size: 80,
                    color: theme.colorScheme.primary,
                  ),
                  const SizedBox(height: 16),
                  Text(
                    'Sign In',
                    style: theme.textTheme.headlineMedium?.copyWith(
                      fontWeight: FontWeight.bold,
                    ),
                    textAlign: TextAlign.center,
                  ),
                  const SizedBox(height: 32),
                  // Email field with validation
                  TextFormField(
                    controller: _emailController,
                    keyboardType: TextInputType.emailAddress,
                    textInputAction: TextInputAction.next,
                    decoration: const InputDecoration(
                      labelText: 'Email',
                      hintText: 'example@email.com',
                      prefixIcon: Icon(Icons.email_outlined),
                    ),
                    validator: (value) {
                      if (value == null || value.isEmpty) {
                        return 'Email is required';
                      }
                      // Basic email format validation
                      if (!RegExp(r'^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$')
                          .hasMatch(value)) {
                        return 'Invalid email format';
                      }
                      return null;
                    },
                  ),
                  const SizedBox(height: 16),
                  // Password field with visibility toggle
                  TextFormField(
                    controller: _passwordController,
                    obscureText: _obscurePassword,
                    textInputAction: TextInputAction.done,
                    onFieldSubmitted: (_) => _submit(),
                    decoration: InputDecoration(
                      labelText: 'Password',
                      prefixIcon: const Icon(Icons.lock_outlined),
                      suffixIcon: IconButton(
                        icon: Icon(
                          _obscurePassword
                              ? Icons.visibility_outlined
                              : Icons.visibility_off_outlined,
                        ),
                        onPressed: () {
                          setState(() {
                            _obscurePassword = !_obscurePassword;
                          });
                        },
                      ),
                    ),
                    validator: (value) {
                      if (value == null || value.isEmpty) {
                        return 'Password is required';
                      }
                      if (value.length < 8) {
                        return 'Minimum 8 characters';
                      }
                      return null;
                    },
                  ),
                  const SizedBox(height: 24),
                  // Sign in button
                  CustomButton(
                    label: 'Sign In',
                    isLoading: isLoading,
                    onPressed: _submit,
                  ),
                  const SizedBox(height: 16),
                  // Link to registration
                  TextButton(
                    onPressed: () => context.goNamed('register'),
                    child: const Text('No account? Create one'),
                  ),
                ],
              ),
            ),
          ),
        ),
      ),
    );
  }
}

ConsumerStatefulWidget์˜ ์‚ฌ์šฉ์œผ๋กœ ๋กœ์ปฌ ์ƒํƒœ(ํผ ์ปจํŠธ๋กค๋Ÿฌ)์™€ ๊ธ€๋กœ๋ฒŒ ์ƒํƒœ(Riverpod)๊ฐ€ ํ†ตํ•ฉ๋ฉ๋‹ˆ๋‹ค.

Controller ๊ด€๋ฆฌ

TextEditingController ์ธ์Šคํ„ด์Šค๋Š” ๋ฉ”๋ชจ๋ฆฌ ๋ˆ„์ˆ˜๋ฅผ ๋ฐฉ์ง€ํ•˜๊ธฐ ์œ„ํ•ด dispose() ๋ฉ”์„œ๋“œ์—์„œ ํ•ด์ œํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค. ์ด ๊ทœ์น™์€ ์ˆ˜๋™์œผ๋กœ ์ƒ์„ฑ๋œ ๋ชจ๋“  Controller์™€ Listener์— ์ ์šฉ๋ฉ๋‹ˆ๋‹ค.

Dio๋ฅผ ํ™œ์šฉํ•œ HTTP ํ†ต์‹ 

Dio๋Š” ์ธํ„ฐ์…‰ํ„ฐ, ์—๋Ÿฌ ์ฒ˜๋ฆฌ, ์‘๋‹ต ์ž๋™ ๋ณ€ํ™˜์„ ๊ฐ–์ถ˜ ๊ฐ•๋ ฅํ•œ HTTP API๋ฅผ ์ œ๊ณตํ•ฉ๋‹ˆ๋‹ค.

lib/core/network/api_client.dartdart
import 'package:dio/dio.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../constants/api_constants.dart';

/// Provider for Dio HTTP client.
final apiClientProvider = Provider<ApiClient>((ref) {
  return ApiClient();
});

/// HTTP client configured with interceptors.
class ApiClient {
  late final Dio _dio;

  ApiClient() {
    _dio = Dio(
      BaseOptions(
        baseUrl: ApiConstants.baseUrl,
        connectTimeout: const Duration(seconds: 10),
        receiveTimeout: const Duration(seconds: 10),
        headers: {
          'Content-Type': 'application/json',
          'Accept': 'application/json',
        },
      ),
    );

    // Add interceptors
    _dio.interceptors.addAll([
      _AuthInterceptor(),
      _LoggingInterceptor(),
    ]);
  }

  /// Generic GET request.
  Future<T> get<T>(
    String path, {
    Map<String, dynamic>? queryParameters,
    T Function(dynamic)? fromJson,
  }) async {
    try {
      final response = await _dio.get(
        path,
        queryParameters: queryParameters,
      );
      if (fromJson != null) {
        return fromJson(response.data);
      }
      return response.data as T;
    } on DioException catch (e) {
      throw _handleError(e);
    }
  }

  /// Generic POST request.
  Future<T> post<T>(
    String path, {
    dynamic data,
    T Function(dynamic)? fromJson,
  }) async {
    try {
      final response = await _dio.post(path, data: data);
      if (fromJson != null) {
        return fromJson(response.data);
      }
      return response.data as T;
    } on DioException catch (e) {
      throw _handleError(e);
    }
  }

  /// Sets the authentication token.
  void setAuthToken(String token) {
    _dio.options.headers['Authorization'] = 'Bearer $token';
  }

  /// Clears the authentication token.
  void clearAuthToken() {
    _dio.options.headers.remove('Authorization');
  }

  /// Converts Dio errors to readable exceptions.
  Exception _handleError(DioException error) {
    switch (error.type) {
      case DioExceptionType.connectionTimeout:
      case DioExceptionType.sendTimeout:
      case DioExceptionType.receiveTimeout:
        return Exception('Connection timeout. Check your connection.');
      case DioExceptionType.badResponse:
        final statusCode = error.response?.statusCode;
        final message = error.response?.data['message'] ?? 'Server error';
        return Exception('Error $statusCode: $message');
      case DioExceptionType.cancel:
        return Exception('Request cancelled');
      default:
        return Exception('Network error: ${error.message}');
    }
  }
}

/// Interceptor to automatically add token.
class _AuthInterceptor extends Interceptor {
  
  void onRequest(
    RequestOptions options,
    RequestInterceptorHandler handler,
  ) {
    // Token is added via setAuthToken()
    handler.next(options);
  }

  
  void onError(
    DioException err,
    ErrorInterceptorHandler handler,
  ) {
    // Handle 401 for token refresh
    if (err.response?.statusCode == 401) {
      // TODO: Implement token refresh
    }
    handler.next(err);
  }
}

/// Logging interceptor for development.
class _LoggingInterceptor extends Interceptor {
  
  void onRequest(
    RequestOptions options,
    RequestInterceptorHandler handler,
  ) {
    print('โ†’ ${options.method} ${options.path}');
    handler.next(options);
  }

  
  void onResponse(
    Response response,
    ResponseInterceptorHandler handler,
  ) {
    print('โ† ${response.statusCode} ${response.requestOptions.path}');
    handler.next(response);
  }

  
  void onError(
    DioException err,
    ErrorInterceptorHandler handler,
  ) {
    print('โœ— ${err.response?.statusCode} ${err.requestOptions.path}');
    handler.next(err);
  }
}

์ธํ„ฐ์…‰ํ„ฐ๋ฅผ ํ†ตํ•ด ๊ฐ ์š”์ฒญ์„ ์˜ค์—ผ์‹œํ‚ค์ง€ ์•Š์œผ๋ฉด์„œ ์ธ์ฆ๊ณผ ๋กœ๊น… ๋กœ์ง์„ ์ค‘์•™ ์ง‘์ค‘ํ™”ํ•ฉ๋‹ˆ๋‹ค.

Material 3 ์ปค์Šคํ…€ ํ…Œ๋งˆ

์ผ๊ด€๋œ ํ…Œ๋งˆ๋Š” ํ†ต์ผ๋œ ์‚ฌ์šฉ์ž ๊ฒฝํ—˜์„ ๋ณด์žฅํ•ฉ๋‹ˆ๋‹ค. Material 3(Material You)๋Š” ์‹œ์Šคํ…œ ์„ค์ •์— ์ ์‘ํ•ฉ๋‹ˆ๋‹ค.

lib/core/theme/app_theme.dartdart
import 'package:flutter/material.dart';
import 'package:google_fonts/google_fonts.dart';

/// Application theme configuration.
class AppTheme {
  // Brand primary color
  static const _primaryColor = Color(0xFF6750A4);

  /// Material 3 light theme.
  static ThemeData get lightTheme {
    // Generate color scheme from primary color
    final colorScheme = ColorScheme.fromSeed(
      seedColor: _primaryColor,
      brightness: Brightness.light,
    );

    return ThemeData(
      useMaterial3: true,
      colorScheme: colorScheme,
      // Custom typography
      textTheme: GoogleFonts.interTextTheme(),
      // AppBar configuration
      appBarTheme: AppBarTheme(
        centerTitle: true,
        elevation: 0,
        backgroundColor: colorScheme.surface,
        foregroundColor: colorScheme.onSurface,
      ),
      // Form field configuration
      inputDecorationTheme: InputDecorationTheme(
        filled: true,
        fillColor: colorScheme.surfaceContainerHighest.withOpacity(0.5),
        border: OutlineInputBorder(
          borderRadius: BorderRadius.circular(12),
          borderSide: BorderSide.none,
        ),
        enabledBorder: OutlineInputBorder(
          borderRadius: BorderRadius.circular(12),
          borderSide: BorderSide.none,
        ),
        focusedBorder: OutlineInputBorder(
          borderRadius: BorderRadius.circular(12),
          borderSide: BorderSide(color: colorScheme.primary, width: 2),
        ),
        errorBorder: OutlineInputBorder(
          borderRadius: BorderRadius.circular(12),
          borderSide: BorderSide(color: colorScheme.error),
        ),
        contentPadding: const EdgeInsets.symmetric(
          horizontal: 16,
          vertical: 16,
        ),
      ),
      // Button configuration
      filledButtonTheme: FilledButtonThemeData(
        style: FilledButton.styleFrom(
          minimumSize: const Size(double.infinity, 52),
          shape: RoundedRectangleBorder(
            borderRadius: BorderRadius.circular(12),
          ),
        ),
      ),
      outlinedButtonTheme: OutlinedButtonThemeData(
        style: OutlinedButton.styleFrom(
          minimumSize: const Size(double.infinity, 52),
          shape: RoundedRectangleBorder(
            borderRadius: BorderRadius.circular(12),
          ),
        ),
      ),
      // Card configuration
      cardTheme: CardTheme(
        elevation: 2,
        shape: RoundedRectangleBorder(
          borderRadius: BorderRadius.circular(16),
        ),
      ),
    );
  }

  /// Material 3 dark theme.
  static ThemeData get darkTheme {
    final colorScheme = ColorScheme.fromSeed(
      seedColor: _primaryColor,
      brightness: Brightness.dark,
    );

    return ThemeData(
      useMaterial3: true,
      colorScheme: colorScheme,
      textTheme: GoogleFonts.interTextTheme(
        ThemeData.dark().textTheme,
      ),
      appBarTheme: AppBarTheme(
        centerTitle: true,
        elevation: 0,
        backgroundColor: colorScheme.surface,
        foregroundColor: colorScheme.onSurface,
      ),
      inputDecorationTheme: InputDecorationTheme(
        filled: true,
        fillColor: colorScheme.surfaceContainerHighest.withOpacity(0.5),
        border: OutlineInputBorder(
          borderRadius: BorderRadius.circular(12),
          borderSide: BorderSide.none,
        ),
        enabledBorder: OutlineInputBorder(
          borderRadius: BorderRadius.circular(12),
          borderSide: BorderSide.none,
        ),
        focusedBorder: OutlineInputBorder(
          borderRadius: BorderRadius.circular(12),
          borderSide: BorderSide(color: colorScheme.primary, width: 2),
        ),
        errorBorder: OutlineInputBorder(
          borderRadius: BorderRadius.circular(12),
          borderSide: BorderSide(color: colorScheme.error),
        ),
        contentPadding: const EdgeInsets.symmetric(
          horizontal: 16,
          vertical: 16,
        ),
      ),
      filledButtonTheme: FilledButtonThemeData(
        style: FilledButton.styleFrom(
          minimumSize: const Size(double.infinity, 52),
          shape: RoundedRectangleBorder(
            borderRadius: BorderRadius.circular(12),
          ),
        ),
      ),
      outlinedButtonTheme: OutlinedButtonThemeData(
        style: OutlinedButton.styleFrom(
          minimumSize: const Size(double.infinity, 52),
          shape: RoundedRectangleBorder(
            borderRadius: BorderRadius.circular(12),
          ),
        ),
      ),
      cardTheme: CardTheme(
        elevation: 2,
        shape: RoundedRectangleBorder(
          borderRadius: BorderRadius.circular(16),
        ),
      ),
    );
  }
}

ColorScheme.fromSeed๋Š” ๋‹จ์ผ ์ƒ‰์ƒ์œผ๋กœ๋ถ€ํ„ฐ ์™„์ „ํ•˜๊ณ  ์ ‘๊ทผ์„ฑ ์žˆ๋Š” ์ƒ‰์ƒ ํŒ”๋ ˆํŠธ๋ฅผ ์ž๋™์œผ๋กœ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค.

์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ ๊ตฌ์„ฑ

์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์˜ ์ง„์ž…์ ์€ Riverpod๋ฅผ ์ดˆ๊ธฐํ™”ํ•˜๊ณ  ํ…Œ๋งˆ๋ฅผ ๊ตฌ์„ฑํ•ฉ๋‹ˆ๋‹ค.

lib/main.dartdart
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'app.dart';

void main() {
  // Ensure Flutter bindings are initialized
  WidgetsFlutterBinding.ensureInitialized();

  // Launch application with Riverpod scope
  runApp(
    const ProviderScope(
      child: App(),
    ),
  );
}
lib/app.dartdart
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'core/theme/app_theme.dart';
import 'routing/app_router.dart';

/// Root application widget.
class App extends ConsumerWidget {
  const App({super.key});

  
  Widget build(BuildContext context, WidgetRef ref) {
    // Get configured router
    final router = ref.watch(routerProvider);

    return MaterialApp.router(
      title: 'My Flutter App',
      debugShowCheckedModeBanner: false,
      // Theme configuration
      theme: AppTheme.lightTheme,
      darkTheme: AppTheme.darkTheme,
      themeMode: ThemeMode.system,
      // Router configuration
      routerConfig: router,
    );
  }
}

ThemeMode.system์€ ์‚ฌ์šฉ์ž ์„ค์ •์— ๋งž์ถฐ ํ…Œ๋งˆ๋ฅผ ์ž๋™์œผ๋กœ ์ ์‘์‹œํ‚ต๋‹ˆ๋‹ค.

๊ฒฐ๋ก 

Flutter๋Š” ๊ณ ์„ฑ๋Šฅ ํฌ๋กœ์Šค ํ”Œ๋žซํผ ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์„ ๊ตฌ์ถ•ํ•˜๊ธฐ ์œ„ํ•œ ์™„์ „ํ•œ ์ƒํƒœ๊ณ„๋ฅผ ์ œ๊ณตํ•ฉ๋‹ˆ๋‹ค. ์„ ์–ธ์  Widget, ์ƒํƒœ ๊ด€๋ฆฌ๋ฅผ ์œ„ํ•œ Riverpod, ๋‚ด๋น„๊ฒŒ์ด์…˜์„ ์œ„ํ•œ GoRouter์˜ ์กฐํ•ฉ์œผ๋กœ ์œ ์ง€๋ณด์ˆ˜์„ฑ๊ณผ ํ™•์žฅ์„ฑ์ด ๋›ฐ์–ด๋‚œ ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์„ ๊ตฌ์ถ•ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

Flutter ์‹œ์ž‘์„ ์œ„ํ•œ ์ฒดํฌ๋ฆฌ์ŠคํŠธ

  • โœ… Flutter SDK๋ฅผ ์„ค์น˜ํ•˜๊ณ  flutter doctor๋กœ ํ™˜๊ฒฝ์„ ์„ค์ •ํ•ฉ๋‹ˆ๋‹ค
  • โœ… Feature ๋‹จ์œ„๋กœ ํ”„๋กœ์ ํŠธ๋ฅผ ๊ตฌ์„ฑํ•˜์—ฌ ์ •๋ฆฌํ•ฉ๋‹ˆ๋‹ค
  • โœ… ํƒ€์ž… ์•ˆ์ „ํ•œ ์ƒํƒœ ๊ด€๋ฆฌ๋ฅผ ์œ„ํ•ด Riverpod๋ฅผ ์‚ฌ์šฉํ•ฉ๋‹ˆ๋‹ค
  • โœ… ์ธ์ฆ ๋ฆฌ๋‹ค์ด๋ ‰ํŠธ๊ฐ€ ํฌํ•จ๋œ GoRouter๋ฅผ ๊ตฌ์„ฑํ•ฉ๋‹ˆ๋‹ค
  • โœ… ์žฌ์‚ฌ์šฉ ๊ฐ€๋Šฅํ•˜๊ณ  ํ•ฉ์„ฑ ๊ฐ€๋Šฅํ•œ Widget์„ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค
  • โœ… ์ผ๊ด€๋œ Material 3 ํ…Œ๋งˆ๋ฅผ ๊ตฌํ˜„ํ•ฉ๋‹ˆ๋‹ค
  • โœ… Dio์™€ ์ธํ„ฐ์…‰ํ„ฐ๋กœ HTTP ํ˜ธ์ถœ์„ ์ค‘์•™ ์ง‘์ค‘ํ™”ํ•ฉ๋‹ˆ๋‹ค

์—ฐ์Šต์„ ์‹œ์ž‘ํ•˜์„ธ์š”!

๋ฉด์ ‘ ์‹œ๋ฎฌ๋ ˆ์ดํ„ฐ์™€ ๊ธฐ์ˆ  ํ…Œ์ŠคํŠธ๋กœ ์ง€์‹์„ ํ…Œ์ŠคํŠธํ•˜์„ธ์š”.

Flutter์˜ ์„ ์–ธ์  ์ ‘๊ทผ ๋ฐฉ์‹์€ ํ•ฉ์„ฑ๊ณผ ์žฌ์‚ฌ์šฉ์„ ์ด‰์ง„ํ•ฉ๋‹ˆ๋‹ค. ๊ฐ Widget์€ ๋…๋ฆฝ์ ์œผ๋กœ ํ…Œ์ŠคํŠธํ•  ์ˆ˜ ์žˆ์œผ๋ฉฐ, Hot Reload๋Š” ๊ฐœ๋ฐœ ์ฃผ๊ธฐ๋ฅผ ํฌ๊ฒŒ ๊ฐ€์†ํ™”ํ•ฉ๋‹ˆ๋‹ค. ์ด ๊ฒฌ๊ณ ํ•œ ๊ธฐ๋ฐ˜์„ ํ†ตํ•ด ๋ณต์žกํ•œ ์• ๋‹ˆ๋ฉ”์ด์…˜, ์ž๋™ํ™”๋œ ํ…Œ์ŠคํŠธ, ์Šคํ† ์–ด ๋ฐฐํฌ ๋“ฑ ๊ณ ๊ธ‰ ๊ธฐ๋Šฅ์œผ๋กœ ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์„ ํ™•์žฅํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

ํƒœ๊ทธ

#flutter
#dart
#mobile development
#cross-platform
#android ios

๊ณต์œ 

๊ด€๋ จ ๊ธฐ์‚ฌ

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

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

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

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

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

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

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

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

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