3. [Flutter] 플러터의 심장, Dart 언어! 개발 초보도 쉽게 배우는 기본 문법

Flutter 개발자를 위한 Dart 언어 완벽 가이드

Android/iOS 개발자가 Flutter로 전환할 때 반드시 알아야 할 Dart 언어의 핵심 개념들


들어가며

안녕하세요! 모바일 개발 경험이 있으시거나 웹 프론트엔드에서 모바일로 영역을 확장하려는 개발자분들을 위한 Dart 언어 가이드를 준비했습니다.

Flutter를 배우기 위해서는 먼저 Dart 언어를 이해해야 합니다. 하지만 걱정하지 마세요! Java/Kotlin, Swift, JavaScript를 사용해보신 분이라면 Dart는 매우 친숙하게 느껴질 것입니다.

이 글에서는 실제 Flutter 앱 개발에서 자주 사용되는 Dart의 핵심 개념들을 실무 예제와 함께 설명드리겠습니다.


1. Dart 언어 소개

왜 Dart인가?

Flutter팀이 Dart를 선택한 이유는 명확합니다:

  • AOT(Ahead-of-Time) 컴파일: 네이티브 수준의 성능
  • JIT(Just-in-Time) 컴파일: 개발 중 Hot Reload 지원
  • 가비지 컬렉션: 메모리 관리 자동화
  • 단일 스레드 모델: 복잡한 동시성 문제 해결

다른 언어와의 비교

// Java/Kotlin 스타일과 매우 유사
class User {
  String name;
  int age;

  User(this.name, this.age);  // 생성자 단축 문법
}

// JavaScript처럼 동적 타이핑도 지원
var data = {'name': 'John', 'age': 30};

2. 변수와 타입 시스템

변수 선언의 베스트 프랙티스

Dart는 강타입 언어이지만 타입 추론을 지원하여 개발 편의성을 제공합니다.

// 타입 추론 활용 (권장)
var userName = 'John Doe';  // String으로 추론
var userAge = 25;           // int로 추론
var isActive = true;        // bool로 추론

// 명시적 타입 선언 (API 경계에서 권장)
String getUserName() => userName;
int calculateAge(DateTime birthDate) => 
    DateTime.now().year - birthDate.year;

// 컬렉션 타입 명시
List<String> tags = ['flutter', 'dart', 'mobile'];
Map<String, dynamic> apiResponse = {'status': 'success', 'data': []};

final vs const: 실무에서의 활용

class AppConfig {
  // 컴파일 타임 상수 - 빌드 시점에 결정
  static const String appName = 'MyAwesomeApp';
  static const int maxRetryCount = 3;
  static const Duration requestTimeout = Duration(seconds: 30);

  // 런타임 상수 - 앱 실행 중 한 번만 설정
  final String deviceId;
  final DateTime appStartTime;

  AppConfig() 
    : deviceId = _generateDeviceId(),
      appStartTime = DateTime.now();

  static String _generateDeviceId() {
    // 디바이스 고유 ID 생성 로직
    return DateTime.now().millisecondsSinceEpoch.toString();
  }
}

// 사용 예
void main() {
  final config = AppConfig();
  print('앱 이름: ${AppConfig.appName}');
  print('시작 시간: ${config.appStartTime}');
}

late 키워드: 지연 초기화 패턴

class ApiService {
  late final Dio _dio;
  late final String _baseUrl;

  // 초기화 메서드에서 late 변수들을 설정
  Future<void> initialize() async {
    _baseUrl = await _loadBaseUrlFromConfig();
    _dio = Dio(BaseOptions(
      baseUrl: _baseUrl,
      connectTimeout: Duration(seconds: 5),
      receiveTimeout: Duration(seconds: 10),
    ));
  }

  Future<String> _loadBaseUrlFromConfig() async {
    // 설정 파일이나 환경 변수에서 URL 로드
    return 'https://api.example.com';
  }

  Future<Map<String, dynamic>> get(String endpoint) async {
    final response = await _dio.get(endpoint);
    return response.data;
  }
}

3. 함수와 메서드

함수형 프로그래밍 스타일

Dart는 함수를 일급 객체로 취급하여 함수형 프로그래밍 패턴을 지원합니다.

// 고차 함수 활용
List<T> processData<T>(
  List<T> data,
  bool Function(T) filter,
  T Function(T) transform,
) {
  return data
      .where(filter)
      .map(transform)
      .toList();
}

// 실사용 예제
class UserService {
  List<User> filterActiveUsers(List<User> users) {
    return processData(
      users,
      (user) => user.isActive && user.lastLoginDays < 30,
      (user) => user.copyWith(displayName: user.name.toUpperCase()),
    );
  }
}

// 익명 함수와 클로저
class EventHandler {
  final List<VoidCallback> _listeners = [];

  void addListener(VoidCallback callback) => _listeners.add(callback);

  void fireEvent() {
    for (final listener in _listeners) {
      listener();
    }
  }

  // 클로저를 활용한 이벤트 리스너
  VoidCallback createLoggingListener(String tag) {
    return () => print('[$tag] Event fired at ${DateTime.now()}');
  }
}

선택적 매개변수 패턴

// Named Parameters (권장 패턴)
class HttpClient {
  Future<Response> request(
    String url, {
    Map<String, String>? headers,
    Duration? timeout,
    int maxRetries = 3,
    bool followRedirects = true,
  }) async {
    // 구현...
  }
}

// 사용 시 가독성이 매우 높음
final response = await httpClient.request(
  'https://api.example.com/users',
  headers: {'Authorization': 'Bearer $token'},
  timeout: Duration(seconds: 30),
  maxRetries: 5,
);

// 빌더 패턴과 결합
class ApiRequestBuilder {
  String? _url;
  Map<String, String> _headers = {};
  Duration _timeout = Duration(seconds: 30);

  ApiRequestBuilder url(String url) {
    _url = url;
    return this;
  }

  ApiRequestBuilder header(String key, String value) {
    _headers[key] = value;
    return this;
  }

  ApiRequestBuilder timeout(Duration timeout) {
    _timeout = timeout;
    return this;
  }

  Future<Response> execute() async {
    if (_url == null) throw ArgumentError('URL is required');
    // HTTP 요청 실행
  }
}

// 사용 예
final response = await ApiRequestBuilder()
    .url('https://api.example.com/users')
    .header('Authorization', 'Bearer $token')
    .header('Content-Type', 'application/json')
    .timeout(Duration(seconds: 45))
    .execute();

4. 클래스와 객체지향 프로그래밍

모던 클래스 설계 패턴

// 불변 클래스 패턴 (권장)
class User {
  final String id;
  final String name;
  final String email;
  final DateTime createdAt;
  final bool isActive;

  const User({
    required this.id,
    required this.name,
    required this.email,
    required this.createdAt,
    this.isActive = true,
  });

  // copyWith 패턴으로 불변성 유지하면서 수정
  User copyWith({
    String? id,
    String? name,
    String? email,
    DateTime? createdAt,
    bool? isActive,
  }) {
    return User(
      id: id ?? this.id,
      name: name ?? this.name,
      email: email ?? this.email,
      createdAt: createdAt ?? this.createdAt,
      isActive: isActive ?? this.isActive,
    );
  }

  // 팩토리 생성자 패턴
  factory User.fromJson(Map<String, dynamic> json) {
    return User(
      id: json['id'] as String,
      name: json['name'] as String,
      email: json['email'] as String,
      createdAt: DateTime.parse(json['created_at'] as String),
      isActive: json['is_active'] as bool? ?? true,
    );
  }

  Map<String, dynamic> toJson() {
    return {
      'id': id,
      'name': name,
      'email': email,
      'created_at': createdAt.toIso8601String(),
      'is_active': isActive,
    };
  }

  @override
  bool operator ==(Object other) {
    if (identical(this, other)) return true;
    return other is User &&
        other.id == id &&
        other.name == name &&
        other.email == email &&
        other.createdAt == createdAt &&
        other.isActive == isActive;
  }

  @override
  int get hashCode {
    return Object.hash(id, name, email, createdAt, isActive);
  }

  @override
  String toString() {
    return 'User(id: $id, name: $name, email: $email, '
           'createdAt: $createdAt, isActive: $isActive)';
  }
}

추상 클래스와 인터페이스 패턴

// 데이터 레이어 추상화
abstract class UserRepository {
  Future<List<User>> getAllUsers();
  Future<User?> getUserById(String id);
  Future<User> createUser(User user);
  Future<User> updateUser(User user);
  Future<void> deleteUser(String id);
}

// 로컬 저장소 구현
class LocalUserRepository implements UserRepository {
  final Box<Map<dynamic, dynamic>> _userBox;

  LocalUserRepository(this._userBox);

  @override
  Future<List<User>> getAllUsers() async {
    return _userBox.values
        .map((json) => User.fromJson(Map<String, dynamic>.from(json)))
        .toList();
  }

  @override
  Future<User?> getUserById(String id) async {
    final json = _userBox.get(id);
    return json != null 
        ? User.fromJson(Map<String, dynamic>.from(json))
        : null;
  }

  @override
  Future<User> createUser(User user) async {
    await _userBox.put(user.id, user.toJson());
    return user;
  }

  // ... 나머지 메서드 구현
}

// API 기반 구현
class ApiUserRepository implements UserRepository {
  final Dio _dio;

  ApiUserRepository(this._dio);

  @override
  Future<List<User>> getAllUsers() async {
    final response = await _dio.get('/users');
    final List<dynamic> usersJson = response.data['users'];
    return usersJson
        .map((json) => User.fromJson(json as Map<String, dynamic>))
        .toList();
  }

  // ... 나머지 메서드 구현
}

Mixin을 활용한 코드 재사용

// 로깅 기능을 mixin으로 분리
mixin LoggerMixin {
  String get logTag => runtimeType.toString();

  void logInfo(String message) {
    print('[$logTag] INFO: $message');
  }

  void logError(String message, [Object? error, StackTrace? stackTrace]) {
    print('[$logTag] ERROR: $message');
    if (error != null) print('Error: $error');
    if (stackTrace != null) print('StackTrace: $stackTrace');
  }

  void logDebug(String message) {
    if (kDebugMode) {
      print('[$logTag] DEBUG: $message');
    }
  }
}

// 네트워크 상태 확인 mixin
mixin NetworkAwareMixin {
  Future<bool> get isConnected async {
    final connectivityResult = await Connectivity().checkConnectivity();
    return connectivityResult != ConnectivityResult.none;
  }

  Future<T> executeWithNetworkCheck<T>(Future<T> Function() operation) async {
    if (!await isConnected) {
      throw NetworkException('No internet connection');
    }
    return await operation();
  }
}

// mixin들을 활용한 서비스 클래스
class UserService with LoggerMixin, NetworkAwareMixin {
  final UserRepository _repository;

  UserService(this._repository);

  Future<List<User>> getUsers() async {
    logInfo('Fetching users...');

    try {
      final users = await executeWithNetworkCheck(
        () => _repository.getAllUsers(),
      );

      logInfo('Successfully fetched ${users.length} users');
      return users;
    } catch (e, stackTrace) {
      logError('Failed to fetch users', e, stackTrace);
      rethrow;
    }
  }
}

5. 컬렉션과 제네릭

실무에서 자주 사용하는 컬렉션 패턴

class DataProcessor {
  // List 변환과 필터링
  List<String> extractUserNames(List<User> users) {
    return users
        .where((user) => user.isActive)
        .map((user) => user.name)
        .toList();
  }

  // Map을 활용한 그룹핑
  Map<String, List<User>> groupUsersByDomain(List<User> users) {
    final groupedUsers = <String, List<User>>{};

    for (final user in users) {
      final domain = user.email.split('@').last;
      groupedUsers.putIfAbsent(domain, () => []).add(user);
    }

    return groupedUsers;
  }

  // Set을 활용한 중복 제거
  Set<String> getUniqueEmails(List<User> users) {
    return users.map((user) => user.email).toSet();
  }

  // 함수형 스타일 체이닝
  List<User> processUsers(List<User> users) {
    return users
        .where((user) => user.isActive)
        .where((user) => user.email.contains('@company.com'))
        .map((user) => user.copyWith(
              name: user.name.trim().toUpperCase(),
            ))
        .toList()
      ..sort((a, b) => a.name.compareTo(b.name));
  }
}

제네릭을 활용한 타입 안전성

// API 응답 래퍼 클래스
class ApiResponse<T> {
  final bool success;
  final T? data;
  final String? error;
  final int statusCode;

  const ApiResponse({
    required this.success,
    this.data,
    this.error,
    required this.statusCode,
  });

  factory ApiResponse.success(T data, {int statusCode = 200}) {
    return ApiResponse(
      success: true,
      data: data,
      statusCode: statusCode,
    );
  }

  factory ApiResponse.error(String error, {int statusCode = 500}) {
    return ApiResponse(
      success: false,
      error: error,
      statusCode: statusCode,
    );
  }

  // 결과 처리를 위한 패턴 매칭 스타일 메서드
  R when<R>({
    required R Function(T data) success,
    required R Function(String error) error,
  }) {
    if (this.success && data != null) {
      return success(data as T);
    } else {
      return error(this.error ?? 'Unknown error');
    }
  }
}

// 제네릭 저장소 패턴
abstract class CacheRepository<T> {
  Future<void> save(String key, T item);
  Future<T?> get(String key);
  Future<void> delete(String key);
  Future<void> clear();
  Future<List<T>> getAll();
}

class HiveCacheRepository<T> implements CacheRepository<T> {
  final Box<String> _box;
  final T Function(Map<String, dynamic>) _fromJson;
  final Map<String, dynamic> Function(T) _toJson;

  HiveCacheRepository(
    this._box,
    this._fromJson,
    this._toJson,
  );

  @override
  Future<void> save(String key, T item) async {
    final jsonString = jsonEncode(_toJson(item));
    await _box.put(key, jsonString);
  }

  @override
  Future<T?> get(String key) async {
    final jsonString = _box.get(key);
    if (jsonString == null) return null;

    final json = jsonDecode(jsonString) as Map<String, dynamic>;
    return _fromJson(json);
  }

  // ... 나머지 메서드 구현
}

// 사용 예
final userCache = HiveCacheRepository<User>(
  userBox,
  (json) => User.fromJson(json),
  (user) => user.toJson(),
);

6. 비동기 프로그래밍

Future와 async/await 실무 패턴

class NetworkService {
  final Dio _dio;
  final Duration defaultTimeout;

  NetworkService(this._dio, {this.defaultTimeout = const Duration(seconds: 30)});

  // 기본적인 HTTP 요청 패턴
  Future<ApiResponse<T>> request<T>(
    String method,
    String path, {
    dynamic data,
    Map<String, dynamic>? queryParameters,
    Duration? timeout,
    T Function(Map<String, dynamic>)? parser,
  }) async {
    try {
      final response = await _dio.request(
        path,
        data: data,
        queryParameters: queryParameters,
        options: Options(
          method: method,
          receiveTimeout: timeout ?? defaultTimeout,
        ),
      );

      if (response.statusCode == 200) {
        final responseData = parser != null 
            ? parser(response.data)
            : response.data as T;
        return ApiResponse.success(responseData);
      } else {
        return ApiResponse.error(
          'Request failed with status: ${response.statusCode}',
          statusCode: response.statusCode!,
        );
      }
    } on DioException catch (e) {
      return _handleDioException(e);
    } catch (e) {
      return ApiResponse.error('Unexpected error: $e');
    }
  }

  // 에러 처리 패턴
  ApiResponse<T> _handleDioException<T>(DioException e) {
    switch (e.type) {
      case DioExceptionType.connectionTimeout:
      case DioExceptionType.sendTimeout:
      case DioExceptionType.receiveTimeout:
        return ApiResponse.error(
          'Request timeout',
          statusCode: 408,
        );
      case DioExceptionType.badResponse:
        return ApiResponse.error(
          'Server error: ${e.response?.statusCode}',
          statusCode: e.response?.statusCode ?? 500,
        );
      case DioExceptionType.cancel:
        return ApiResponse.error('Request cancelled');
      default:
        return ApiResponse.error('Network error: ${e.message}');
    }
  }

  // 병렬 요청 처리
  Future<(List<User>, List<Post>)> fetchUserDataParallel(String userId) async {
    final futures = await Future.wait([
      getUserProfile(userId),
      getUserPosts(userId),
    ]);

    return (
      futures[0] as List<User>,
      futures[1] as List<Post>,
    );
  }
}

Stream을 활용한 실시간 데이터 처리

class ChatService {
  final WebSocketChannel _channel;
  late final StreamController<ChatMessage> _messageController;

  ChatService(this._channel) {
    _messageController = StreamController<ChatMessage>.broadcast();
    _listenToMessages();
  }

  Stream<ChatMessage> get messages => _messageController.stream;

  void _listenToMessages() {
    _channel.stream.listen(
      (data) {
        try {
          final json = jsonDecode(data) as Map<String, dynamic>;
          final message = ChatMessage.fromJson(json);
          _messageController.add(message);
        } catch (e) {
          print('Failed to parse message: $e');
        }
      },
      onError: (error) {
        _messageController.addError(error);
      },
      onDone: () {
        _messageController.close();
      },
    );
  }

  void sendMessage(String text) {
    final message = {
      'type': 'message',
      'text': text,
      'timestamp': DateTime.now().toIso8601String(),
    };
    _channel.sink.add(jsonEncode(message));
  }

  void dispose() {
    _channel.sink.close();
    _messageController.close();
  }
}

// Stream 변환과 필터링
class DataStreamProcessor {
  Stream<List<User>> processUserStream(Stream<List<User>> userStream) {
    return userStream
        .where((users) => users.isNotEmpty)
        .map((users) => users.where((user) => user.isActive).toList())
        .distinct((previous, current) => 
            const ListEquality().equals(previous, current))
        .debounceTime(Duration(milliseconds: 300));
  }

  // 여러 스트림 결합
  Stream<SearchResult> combineSearchStreams(
    Stream<String> queryStream,
    Stream<List<String>> suggestionsStream,
  ) {
    return Rx.combineLatest2(
      queryStream.debounceTime(Duration(milliseconds: 500)),
      suggestionsStream,
      (query, suggestions) => SearchResult(
        query: query,
        suggestions: suggestions
            .where((s) => s.toLowerCase().contains(query.toLowerCase()))
            .toList(),
      ),
    );
  }
}

7. 널 안전성 (Null Safety)

실무에서의 널 안전성 패턴

class UserValidator {
  // 널 안전 메서드 체이닝
  String? validateEmail(String? email) {
    return email
        ?.trim()
        .toLowerCase()
        .let((email) => email.contains('@') ? email : null);
  }

  // 널 병합 연산자 활용
  String getDisplayName(User? user) {
    return user?.name ?? 
           user?.email?.split('@').first ?? 
           'Unknown User';
  }

  // 안전한 타입 캐스팅
  int? parseAge(dynamic value) {
    if (value is int) return value;
    if (value is String) return int.tryParse(value);
    return null;
  }

  // 조건부 멤버 접근
  void updateUserIfActive(User? user, String newName) {
    user?.let((user) {
      if (user.isActive) {
        // 사용자 업데이트 로직
        updateUser(user.copyWith(name: newName));
      }
    });
  }
}

// Extension을 활용한 널 안전성 향상
extension NullableExtensions<T> on T? {
  R? let<R>(R Function(T) transform) {
    final value = this;
    return value != null ? transform(value) : null;
  }

  T orElse(T defaultValue) {
    return this ?? defaultValue;
  }

  T orElseGet(T Function() defaultValueProvider) {
    return this ?? defaultValueProvider();
  }
}

// 사용 예
final user = await userService.getUser(userId);
final avatar = user
    ?.profileImageUrl
    ?.let((url) => CachedNetworkImage(imageUrl: url))
    ?? const CircleAvatar(child: Icon(Icons.person));

안전한 JSON 파싱 패턴

class SafeJsonParser {
  static T? tryParse<T>(
    Map<String, dynamic>? json,
    String key,
    T Function(dynamic) parser,
  ) {
    try {
      final value = json?[key];
      return value != null ? parser(value) : null;
    } catch (e) {
      print('Failed to parse $key: $e');
      return null;
    }
  }

  static List<T> parseList<T>(
    Map<String, dynamic>? json,
    String key,
    T Function(Map<String, dynamic>) itemParser,
  ) {
    try {
      final list = json?[key] as List<dynamic>?;
      if (list == null) return [];

      return list
          .whereType<Map<String, dynamic>>()
          .map(itemParser)
          .toList();
    } catch (e) {
      print('Failed to parse list $key: $e');
      return [];
    }
  }
}

// 안전한 모델 클래스
class SafeUser {
  final String id;
  final String name;
  final String? email;  // 선택적 필드
  final int age;
  final List<String> tags;
  final DateTime? lastLogin;  // 선택적 필드

  SafeUser({
    required this.id,
    required this.name,
    this.email,
    required this.age,
    this.tags = const [],
    this.lastLogin,
  });

  factory SafeUser.fromJson(Map<String, dynamic> json) {
    return SafeUser(
      id: json['id'] as String? ?? '',
      name: json['name'] as String? ?? 'Unknown',
      email: json['email'] as String?,
      age: SafeJsonParser.tryParse(json, 'age', (v) => v as int) ?? 0,
      tags: SafeJsonParser.parseList(
        json, 
        'tags', 
        (item) => item['name'] as String,
      ),
      lastLogin: SafeJsonParser.tryParse(
        json, 
        'last_login', 
        (v) => DateTime.parse(v as String),
      ),
    );
  }
}

8. 함수형 프로그래밍 패턴

불변성과 순수 함수

// 순수 함수 예제
class MathUtils {
  // 입력에만 의존하고 부작용이 없는 순수 함수
  static double calculateDistance(Point a, Point b) {
    final dx = a.x - b.x;
    final dy = a.y - b.y;
    return sqrt(dx * dx + dy * dy);
  }

  // 리스트를 변경하지 않고 새로운 리스트 반환
  static List<int> addToAll(List<int> numbers, int value) {
    return numbers.map((n) => n + value).toList();
  }
}

// 함수 조합 패턴
class StringProcessor {
  static String Function(String) compose(
    List<String Function(String)> functions,
  ) {
    return (input) => functions.fold(input, (acc, fn) => fn(acc));
  }

  static final processors = {
    'trim': (String s) => s.trim(),
    'lower': (String s) => s.toLowerCase(),
    'removeSpaces': (String s) => s.replaceAll(' ', ''),
    'capitalize': (String s) => s.isEmpty 
        ? s 
        : s[0].toUpperCase() + s.substring(1),
  };

  // 사용 예
  static final nameProcessor = compose([
    processors['trim']!,
    processors['lower']!,
    processors['capitalize']!,
  ]);
}

// 함수형 에러 처리
class Result<T, E> {
  final T? _value;
  final E? _error;
  final bool _isSuccess;

  const Result._(this._value, this._error, this._isSuccess);

  factory Result.success(T value) => Result._(value, null, true);
  factory Result.error(E error) => Result._(null, error, false);

  bool get isSuccess => _isSuccess;
  bool get isError => !_isSuccess;

  T get value {
    if (_isSuccess) return _value as T;
    throw StateError('Cannot get value from error result');
  }

  E get error {
    if (!_isSuccess) return _error as E;
    throw StateError('Cannot get error from success result');
  }

  // 함수형 변환
  Result<R, E> map<R>(R Function(T) transform) {
    return _isSuccess 
        ? Result.success(transform(_value as T))
        : Result.error(_error as E);
  }

  Result<R, E> flatMap<R>(Result<R, E> Function(T) transform) {
    return _isSuccess 
        ? transform(_value as T)
        : Result.error(_error as E);
  }

  T orElse(T defaultValue) {
    return _isSuccess ? _value as T : defaultValue;
  }

  // 패턴 매칭 스타일
  R when<R>({
    required R Function(T) success,
    required R Function(E) error,
  }) {
    return _isSuccess 
        ? success(_value as T)
        : error(_error as E);
  }
}

// 실사용 예제
class UserService {
  Future<Result<User, String>> getUser(String id) async {
    try {
      final user = await _apiService.getUser(id);
      return Result.success(user);
    } catch (e) {
      return Result.error('Failed to fetch user: $e');
    }
  }

  Future<Result<String, String>> processUserData(String userId) async {
    final userResult = await getUser(userId);

    return userResult
        .map((user) => user.name.toUpperCase())
        .map((name) => 'Processed: $name');
  }
}

9. 성능 최적화를 위한 Dart 패턴

메모리 효율적인 코드 작성

class PerformanceOptimizedService {
  // 지연 초기화를 통한 메모리 절약
  late final RegExp _emailRegex = RegExp(r'^[w-.]+@([w-]+.)+[w-]{2,4}

비동기 코드 최적화

class AsyncOptimizer {
  // 병렬 처리로 성능 향상
  Future<UserProfile> loadUserProfile(String userId) async {
    // 순차 실행 (느림)
    // final user = await userService.getUser(userId);
    // final posts = await postService.getUserPosts(userId);
    // final followers = await followService.getFollowers(userId);

    // 병렬 실행 (빠름)
    final futures = await Future.wait([
      userService.getUser(userId),
      postService.getUserPosts(userId),
      followService.getFollowers(userId),
    ]);

    return UserProfile(
      user: futures[0] as User,
      posts: futures[1] as List<Post>,
      followers: futures[2] as List<User>,
    );
  }

  // 조건부 비동기 실행
  Future<List<Post>> getPostsConditionally(String userId) async {
    final cachedPosts = await cacheService.getPosts(userId);

    if (cachedPosts.isNotEmpty) {
      return cachedPosts;  // 캐시된 데이터 사용
    }

    // 네트워크에서 가져오기
    final posts = await apiService.getPosts(userId);

    // 백그라운드에서 캐시 저장 (await 없이)
    unawaited(cacheService.savePosts(userId, posts));

    return posts;
  }

  // 타임아웃과 재시도 패턴
  Future<T> executeWithRetry<T>(
    Future<T> Function() operation,
    int maxRetries,
    Duration timeout,
  ) async {
    for (int attempt = 0; attempt <= maxRetries; attempt++) {
      try {
        return await operation().timeout(timeout);
      } catch (e) {
        if (attempt == maxRetries) rethrow;

        // 지수 백오프
        await Future.delayed(Duration(seconds: math.pow(2, attempt).toInt()));
      }
    }

    throw StateError('This should never be reached');
  }
}

마무리

이제 Dart 언어의 핵심 개념들을 실무 중심으로 살펴보았습니다. 각 개념은 Flutter 앱 개발에서 실제로 자주 사용되는 패턴들입니다.

다음 단계

  1. Flutter 위젯 시스템 학습
  2. 상태 관리 (Riverpod, Bloc) 적용
  3. 실제 프로젝트로 연습

추천 학습 자료

궁금한 점이 있으시면 언제든 댓글로 남겨주세요! 함께 성장하는 Flutter 개발자가 되어봅시다! 🚀


관련 글

); late final DateFormat _dateFormatter = DateFormat('yyyy-MM-dd'); // 싱글톤 패턴으로 인스턴스 재사용 static PerformanceOptimizedService? _instance; static PerformanceOptimizedService get instance { return _instance ??= PerformanceOptimizedService._(); } PerformanceOptimizedService._(); // 캐시를 통한 중복 계산 방지 final Map<String, bool> _validationCache = {}; bool isValidEmail(String email) { return _validationCache.putIfAbsent( email, () => _emailRegex.hasMatch(email), ); } // 스트림 구독 관리 final List<StreamSubscription> _subscriptions = []; void addSubscription(StreamSubscription subscription) { _subscriptions.add(subscription); } void dispose() { for (final subscription in _subscriptions) { subscription.cancel(); } _subscriptions.clear(); _validationCache.clear(); } } // 효율적인 컬렉션 사용 class CollectionOptimizer { // Set을 사용한 중복 제거와 빠른 조회 static List<String> removeDuplicatesEfficient(List<String> items) { return items.toSet().toList(); } // Map을 사용한 빠른 조회 static User? findUserByIdEfficient(List<User> users, String id) { // O(n) 매번 순회하는 대신 // final user = users.firstWhere((u) => u.id == id); // O(1) 조회를 위한 Map 사용 final userMap = {for (final user in users) user.id: user}; return userMap[id]; } // 대용량 데이터 청크 단위 처리 static Future<List<T>> processInChunks<T>( List<T> items, Future<T> Function(T) processor, int chunkSize, ) async { final results = <T>[]; for (int i = 0; i < items.length; i += chunkSize) { final chunk = items.sublist( i, math.min(i + chunkSize, items.length), ); final chunkResults = await Future.wait( chunk.map(processor), ); results.addAll(chunkResults); } return results; } }

비동기 코드 최적화






마무리

이제 Dart 언어의 핵심 개념들을 실무 중심으로 살펴보았습니다. 각 개념은 Flutter 앱 개발에서 실제로 자주 사용되는 패턴들입니다.

다음 단계

  1. Flutter 위젯 시스템 학습
  2. 상태 관리 (Riverpod, Bloc) 적용
  3. 실제 프로젝트로 연습

추천 학습 자료

궁금한 점이 있으시면 언제든 댓글로 남겨주세요! 함께 성장하는 Flutter 개발자가 되어봅시다! 🚀


관련 글

코멘트

답글 남기기

이메일 주소는 공개되지 않습니다. 필수 필드는 *로 표시됩니다