📚 학습 기록/Dart & Flutter 기초

Flutter 데이터 그룹핑과 동적 위젯 생성: Map 자료구조를 활용한 확장 가능한 아키텍처 설계

zenjoydev 2025. 7. 5. 14:25

개요

현대 모바일 애플리케이션에서 데이터의 효과적인 시각화와 사용자 경험 개선을 위해서는 단순한 평면적 리스트 구조를 넘어선 계층적 데이터 표현이 필수적입니다.

이 글에서는 Flutter에서 Map<K, List<T>> 자료구조를 활용한 제네릭 데이터 그룹핑 패턴과 동적 위젯 생성 기법을 통해 확장 가능하고 성능 최적화된 리스트 UI 아키텍처를 구현하는 방법을 상세히 다루겠습니다.

목차

  1. 기존 ListView 아키텍처의 한계점 분석
  2. Map 자료구조를 활용한 함수형 데이터 그룹핑
  3. 제네릭 동적 위젯 생성 패턴
  4. 렌더링 아키텍처 전략: ListView vs Column
  5. 성능 최적화 및 메모리 관리
  6. 실무 적용 사례 및 확장성 고려사항

기존 ListView 아키텍처의 한계점 분석

일반적인 Flutter 리스트 구현은 ListView.builder를 사용하여 다음과 같이 작성됩니다:

class SimpleListView<T> extends StatelessWidget {
  final List<T> items;
  final Widget Function(T item, int index) itemBuilder;
  
  @override
  Widget build(BuildContext context) {
    return ListView.builder(
      itemCount: items.length,
      itemBuilder: (context, index) {
        return itemBuilder(items[index], index);
      },
    );
  }
}

아키텍처적 한계점

이러한 접근법은 다음과 같은 기술적 제약사항을 가집니다:

  1. 단일 차원 데이터 모델: 리스트의 각 요소가 개별적으로만 처리되어 데이터 간의 관계성을 표현하기 어려움
  2. 정적 위젯 구조: 헤더, 구분선, 그룹별 특수 위젯 등 다양한 UI 요소를 조합하는데 제약
  3. 확장성 부족: 새로운 그룹핑 조건이나 정렬 기준 추가 시 전체 구조 재설계 필요
  4. 성능 병목: 대용량 데이터셋에서 효율적인 가상화 및 렌더링 최적화 구현의 복잡성

데이터와 프레젠테이션 레이어의 강결합

// 문제가 있는 구조: 데이터 로직과 UI 로직이 혼재
class ProblematicListView extends StatelessWidget {
  final List<DataItem> items;
  
  @override
  Widget build(BuildContext context) {
    return ListView.builder(
      itemCount: items.length,
      itemBuilder: (context, index) {
        final item = items[index];
        
        // 데이터 처리 로직이 UI 레이어에 포함됨
        final showHeader = index == 0 || 
            item.category != items[index - 1].category;
        
        return Column(
          children: [
            if (showHeader) CategoryHeader(item.category),
            ItemWidget(item),
          ],
        );
      },
    );
  }
}

이러한 구조는 관심사 분리 원칙을 위배하며, 테스트 작성과 유지보수를 어렵게 만듭니다.

Map 자료구조를 활용한 함수형 데이터 그룹핑

제네릭 그룹핑 함수 설계

데이터 그룹핑의 핵심은 타입 안전성과 재사용성을 보장하는 제네릭 함수를 설계하는 것입니다:

/// 제네릭 그룹핑 함수
/// T: 원본 데이터 타입, K: 그룹핑 키 타입
Map<K, List<T>> groupBy<T, K>(
  Iterable<T> items,
  K Function(T) keySelector,
) {
  return items.fold<Map<K, List<T>>>(
    <K, List<T>>{},
    (Map<K, List<T>> accumulated, T item) {
      final K key = keySelector(item);
      accumulated.putIfAbsent(key, () => <T>[]).add(item);
      return accumulated;
    },
  );
}

함수형 프로그래밍 접근법

fold 메서드를 사용한 함수형 접근법의 장점:

  1. 불변성 보장: 원본 데이터의 변경 없이 새로운 구조 생성
  2. 순수 함수: 부작용 없는 예측 가능한 동작
  3. 조합 가능성: 다른 함수형 연산과 쉽게 체이닝 가능
  4. 성능 최적화: 단일 순회로 그룹핑 완료

실무 활용 패턴 구현

class DataGroupingService {
  /// 날짜별 이벤트 그룹핑
  Map<DateTime, List<EventLog>> groupEventsByDate(List<EventLog> events) {
    return groupBy(events, (event) => DateTime(
      event.timestamp.year,
      event.timestamp.month,
      event.timestamp.day,
    ));
  }
  
  /// 카테고리별 제품 그룹핑
  Map<ProductCategory, List<Product>> groupProductsByCategory(List<Product> products) {
    return groupBy(products, (product) => product.category);
  }
  
  /// 우선순위별 작업 그룹핑
  Map<TaskPriority, List<Task>> groupTasksByPriority(List<Task> tasks) {
    return groupBy(tasks, (task) => task.priority);
  }
  
  /// 상태별 주문 그룹핑
  Map<OrderStatus, List<Order>> groupOrdersByStatus(List<Order> orders) {
    return groupBy(orders, (order) => order.status);
  }
}

고급 그룹핑 패턴

다단계 계층적 그룹핑

class HierarchicalGrouping {
  /// 년-월-일 계층 구조 생성
  Map<int, Map<int, Map<int, List<Event>>>> buildDateHierarchy(List<Event> events) {
    final Map<int, Map<int, Map<int, List<Event>>>> hierarchy = {};
    
    for (final event in events) {
      final year = event.date.year;
      final month = event.date.month;
      final day = event.date.day;
      
      hierarchy
          .putIfAbsent(year, () => {})
          .putIfAbsent(month, () => {})
          .putIfAbsent(day, () => [])
          .add(event);
    }
    
    return hierarchy;
  }
  
  /// 복합 키를 이용한 그룹핑
  Map<String, List<T>> groupByCompositeKey<T>(
    List<T> items,
    List<String Function(T)> keyExtractors,
    String keySeparator,
  ) {
    return groupBy(items, (item) {
      return keyExtractors
          .map((extractor) => extractor(item))
          .join(keySeparator);
    });
  }
}

조건부 그룹핑

class ConditionalGrouping {
  /// 여러 조건에 따른 동적 그룹핑
  Map<String, List<T>> conditionalGrouping<T>(
    List<T> items,
    Map<String, bool Function(T)> groupConditions,
  ) {
    final Map<String, List<T>> groups = {};
    
    for (final item in items) {
      for (final entry in groupConditions.entries) {
        if (entry.value(item)) {
          groups.putIfAbsent(entry.key, () => []).add(item);
        }
      }
    }
    
    return groups;
  }
  
  /// 범위 기반 그룹핑
  Map<String, List<T>> groupByRange<T>(
    List<T> items,
    num Function(T) valueExtractor,
    List<(String label, num min, num max)> ranges,
  ) {
    return groupBy(items, (item) {
      final value = valueExtractor(item);
      for (final range in ranges) {
        if (value >= range.$2 && value < range.$3) {
          return range.$1;
        }
      }
      return 'Unknown';
    });
  }
}

성능 고려사항

지연 평가를 통한 최적화

class LazyGrouping<T, K> {
  final List<T> _sourceData;
  final K Function(T) _keySelector;
  Map<K, List<T>>? _cachedResult;
  int? _lastHashCode;
  
  LazyGrouping(this._sourceData, this._keySelector);
  
  Map<K, List<T>> get groupedData {
    final currentHash = _calculateDataHash();
    
    if (_cachedResult == null || _lastHashCode != currentHash) {
      _cachedResult = groupBy(_sourceData, _keySelector);
      _lastHashCode = currentHash;
    }
    
    return _cachedResult!;
  }
  
  int _calculateDataHash() {
    return _sourceData.fold(0, (hash, item) => hash ^ item.hashCode);
  }
  
  void invalidateCache() {
    _cachedResult = null;
    _lastHashCode = null;
  }
}

동적 위젯 생성 패턴

위젯 리스트 생성 로직

그룹핑된 데이터를 바탕으로 UI 위젯들을 동적으로 생성합니다:

List<Widget> buildTimelineView(Map<String, List<Message>> grouped) {
  List<Widget> widgets = [];
  
  grouped.forEach((timeSection, messages) {
    // 1. 시간대 헤더 위젯 추가
    widgets.add(_buildSectionHeader(timeSection, messages.length));
    
    // 2. 메시지 위젯들 추가 (중복 시간 제거 로직 포함)
    widgets.addAll(_buildMessageWidgets(messages));
  });
  
  return widgets;
}

Widget _buildSectionHeader(String timeSection, int messageCount) {
  return Container(
    width: double.infinity,
    padding: EdgeInsets.symmetric(vertical: 12, horizontal: 16),
    margin: EdgeInsets.symmetric(vertical: 8),
    decoration: BoxDecoration(
      color: Colors.grey[100],
      borderRadius: BorderRadius.circular(8),
    ),
    child: Text(
      '$timeSection ($messageCount개)',
      style: TextStyle(
        fontSize: 14,
        fontWeight: FontWeight.w600,
        color: Colors.grey[700],
      ),
      textAlign: TextAlign.center,
    ),
  );
}

List<Widget> _buildMessageWidgets(List<Message> messages) {
  return messages.asMap().entries.map((entry) {
    int index = entry.key;
    Message message = entry.value;
    
    // 중복 시간 표시 방지 로직
    bool shouldShowTime = index == 0 || 
        !_isSameMinute(message.timeStamp, messages[index - 1].timeStamp);
    
    return MessageBubble(
      message: message.text,
      timestamp: shouldShowTime ? _formatTime(message.timeStamp) : null,
      isUser: message.isUser,
    );
  }).toList();
}

bool _isSameMinute(DateTime time1, DateTime time2) {
  return time1.hour == time2.hour && time1.minute == time2.minute;
}

String _formatTime(DateTime timestamp) {
  return DateFormat('HH:mm').format(timestamp);
}

주요 특징

  • 조건부 위젯 생성: 중복 시간 제거를 위한 조건부 로직
  • 함수형 프로그래밍: map(), toList() 체이닝 활용
  • 관심사 분리: 헤더 생성과 메시지 생성 로직을 별도 함수로 분리

렌더링 아키텍처 전략: ListView vs Column

렌더링 성능과 메모리 효율성을 고려할 때, 데이터 크기와 UI 복잡성에 따라 적절한 렌더링 전략을 선택해야 합니다.

전략 1: 가상화된 ListView (대용량 데이터 적합)

/// 평면화된 데이터 구조를 위한 아이템 래퍼
abstract class ListItem {
  const ListItem();
}

class GroupHeaderItem extends ListItem {
  final String title;
  final int itemCount;
  final Map<String, dynamic> metadata;
  
  const GroupHeaderItem({
    required this.title,
    required this.itemCount,
    this.metadata = const {},
  });
}

class DataItem<T> extends ListItem {
  final T data;
  final int indexInGroup;
  final List<T> groupSiblings;
  
  const DataItem({
    required this.data,
    required this.indexInGroup,
    required this.groupSiblings,
  });
}

class GroupFooterItem extends ListItem {
  final String groupKey;
  final Map<String, dynamic> summary;
  
  const GroupFooterItem({
    required this.groupKey,
    required this.summary,
  });
}

/// 가상화된 그룹 리스트 위젯
class VirtualizedGroupedListView<T> extends StatelessWidget {
  final Map<String, List<T>> groupedData;
  final GroupedWidgetBuilder<T> widgetBuilder;
  final ScrollController? scrollController;
  final EdgeInsets? padding;
  
  const VirtualizedGroupedListView({
    Key? key,
    required this.groupedData,
    required this.widgetBuilder,
    this.scrollController,
    this.padding,
  }) : super(key: key);
  
  @override
  Widget build(BuildContext context) {
    final flattenedItems = _flattenGroupedData(groupedData);
    
    return ListView.builder(
      controller: scrollController,
      padding: padding,
      itemCount: flattenedItems.length,
      itemBuilder: (context, index) {
        return _buildItemWidget(flattenedItems[index]);
      },
    );
  }
  
  /// 그룹핑된 데이터를 평면화
  List<ListItem> _flattenGroupedData(Map<String, List<T>> grouped) {
    final List<ListItem> items = [];
    
    grouped.forEach((groupKey, groupItems) {
      // 그룹 헤더 추가
      items.add(GroupHeaderItem(
        title: groupKey,
        itemCount: groupItems.length,
      ));
      
      // 그룹 아이템들 추가
      for (int i = 0; i < groupItems.length; i++) {
        items.add(DataItem<T>(
          data: groupItems[i],
          indexInGroup: i,
          groupSiblings: groupItems,
        ));
      }
      
      // 그룹 푸터 추가 (필요한 경우)
      final footerSummary = _calculateGroupSummary(groupKey, groupItems);
      if (footerSummary.isNotEmpty) {
        items.add(GroupFooterItem(
          groupKey: groupKey,
          summary: footerSummary,
        ));
      }
    });
    
    return items;
  }
  
  /// 아이템 타입에 따른 위젯 빌드
  Widget _buildItemWidget(ListItem item) {
    if (item is GroupHeaderItem) {
      return widgetBuilder.buildGroupHeader(item.title, []);
    } else if (item is DataItem<T>) {
      return widgetBuilder.buildItem(
        item.data,
        item.indexInGroup,
        item.groupSiblings,
      );
    } else if (item is GroupFooterItem) {
      return widgetBuilder.buildGroupFooter(item.groupKey, []) ?? 
             const SizedBox.shrink();
    }
    
    return const SizedBox.shrink();
  }
  
  Map<String, dynamic> _calculateGroupSummary(String groupKey, List<T> items) {
    // 그룹별 요약 정보 계산 로직
    return {};
  }
}

전략 2: 유연한 Column 구조 (복잡한 레이아웃 적합)

/// 스크롤 가능한 컬럼 기반 그룹 리스트
class FlexibleGroupedListView<T> extends StatelessWidget {
  final Map<String, List<T>> groupedData;
  final DynamicGroupedListBuilder<T> listBuilder;
  final ScrollController? scrollController;
  final EdgeInsets? padding;
  final Axis scrollDirection;
  
  const FlexibleGroupedListView({
    Key? key,
    required this.groupedData,
    required this.listBuilder,
    this.scrollController,
    this.padding,
    this.scrollDirection = Axis.vertical,
  }) : super(key: key);
  
  @override
  Widget build(BuildContext context) {
    final widgets = listBuilder.buildWidgetList(groupedData);
    
    return SingleChildScrollView(
      controller: scrollController,
      scrollDirection: scrollDirection,
      padding: padding,
      child: scrollDirection == Axis.vertical
          ? Column(
              crossAxisAlignment: CrossAxisAlignment.stretch,
              children: widgets,
            )
          : Row(
              crossAxisAlignment: CrossAxisAlignment.start,
              children: widgets,
            ),
    );
  }
}

하이브리드 접근법: 적응형 렌더링

/// 데이터 크기에 따라 렌더링 전략을 동적으로 선택
class AdaptiveGroupedListView<T> extends StatelessWidget {
  final Map<String, List<T>> groupedData;
  final GroupedWidgetBuilder<T> widgetBuilder;
  final int virtualizationThreshold;
  final ScrollController? scrollController;
  
  const AdaptiveGroupedListView({
    Key? key,
    required this.groupedData,
    required this.widgetBuilder,
    this.virtualizationThreshold = 100,
    this.scrollController,
  }) : super(key: key);
  
  @override
  Widget build(BuildContext context) {
    final totalItemCount = groupedData.values
        .fold(0, (sum, items) => sum + items.length);
    
    if (totalItemCount > virtualizationThreshold) {
      // 대용량 데이터: 가상화된 ListView 사용
      return VirtualizedGroupedListView<T>(
        groupedData: groupedData,
        widgetBuilder: widgetBuilder,
        scrollController: scrollController,
      );
    } else {
      // 소용량 데이터: 유연한 Column 사용
      final listBuilder = DynamicGroupedListBuilder<T>(
        widgetBuilder: widgetBuilder,
      );
      
      return FlexibleGroupedListView<T>(
        groupedData: groupedData,
        listBuilder: listBuilder,
        scrollController: scrollController,
      );
    }
  }
}

성능 모니터링 및 디버깅

/// 렌더링 성능 메트릭 수집
class PerformanceAwareGroupedList<T> extends StatefulWidget {
  final Map<String, List<T>> groupedData;
  final GroupedWidgetBuilder<T> widgetBuilder;
  final Function(RenderingMetrics)? onMetricsUpdate;
  
  @override
  _PerformanceAwareGroupedListState<T> createState() => 
      _PerformanceAwareGroupedListState<T>();
}

class _PerformanceAwareGroupedListState<T> 
    extends State<PerformanceAwareGroupedList<T>> 
    with TickerProviderStateMixin {
  
  late AnimationController _animationController;
  Stopwatch? _buildStopwatch;
  int _frameCount = 0;
  
  @override
  void initState() {
    super.initState();
    _animationController = AnimationController(
      duration: const Duration(milliseconds: 16),
      vsync: this,
    );
    
    _animationController.addListener(_onFrame);
    _animationController.repeat();
  }
  
  void _onFrame() {
    _frameCount++;
    
    if (_frameCount % 60 == 0) {
      // 60프레임마다 메트릭 업데이트
      final metrics = RenderingMetrics(
        frameRate: 60.0 / (_buildStopwatch?.elapsedMilliseconds ?? 1) * 1000,
        itemCount: widget.groupedData.values.fold(0, (sum, items) => sum + items.length),
        memoryUsage: _estimateMemoryUsage(),
      );
      
      widget.onMetricsUpdate?.call(metrics);
    }
  }
  
  @override
  Widget build(BuildContext context) {
    _buildStopwatch = Stopwatch()..start();
    
    final result = AdaptiveGroupedListView<T>(
      groupedData: widget.groupedData,
      widgetBuilder: widget.widgetBuilder,
    );
    
    _buildStopwatch?.stop();
    
    return result;
  }
  
  double _estimateMemoryUsage() {
    // 메모리 사용량 추정 로직
    return 0.0;
  }
  
  @override
  void dispose() {
    _animationController.dispose();
    super.dispose();
  }
}

class RenderingMetrics {
  final double frameRate;
  final int itemCount;
  final double memoryUsage;
  
  const RenderingMetrics({
    required this.frameRate,
    required this.itemCount,
    required this.memoryUsage,
  });
}

스크롤 위치 관리 및 복원

/// 스크롤 위치를 자동으로 관리하는 그룹 리스트
class ScrollPositionAwareGroupedList<T> extends StatefulWidget {
  final Map<String, List<T>> groupedData;
  final GroupedWidgetBuilder<T> widgetBuilder;
  final String? scrollToGroupKey;
  final String? scrollToItemId;
  
  @override
  _ScrollPositionAwareGroupedListState<T> createState() => 
      _ScrollPositionAwareGroupedListState<T>();
}

class _ScrollPositionAwareGroupedListState<T> 
    extends State<ScrollPositionAwareGroupedList<T>> {
  
  late ScrollController _scrollController;
  final Map<String, GlobalKey> _groupKeys = {};
  final Map<String, GlobalKey> _itemKeys = {};
  
  @override
  void initState() {
    super.initState();
    _scrollController = ScrollController();
    _initializeKeys();
  }
  
  void _initializeKeys() {
    widget.groupedData.forEach((groupKey, items) {
      _groupKeys[groupKey] = GlobalKey();
      
      for (final item in items) {
        final itemId = _getItemId(item);
        if (itemId != null) {
          _itemKeys[itemId] = GlobalKey();
        }
      }
    });
  }
  
  @override
  void didUpdateWidget(ScrollPositionAwareGroupedList<T> oldWidget) {
    super.didUpdateWidget(oldWidget);
    
    if (widget.scrollToGroupKey != null) {
      WidgetsBinding.instance.addPostFrameCallback((_) {
        _scrollToGroup(widget.scrollToGroupKey!);
      });
    } else if (widget.scrollToItemId != null) {
      WidgetsBinding.instance.addPostFrameCallback((_) {
        _scrollToItem(widget.scrollToItemId!);
      });
    }
  }
  
  void _scrollToGroup(String groupKey) {
    final key = _groupKeys[groupKey];
    if (key?.currentContext != null) {
      Scrollable.ensureVisible(
        key!.currentContext!,
        duration: const Duration(milliseconds: 300),
        curve: Curves.easeInOut,
      );
    }
  }
  
  void _scrollToItem(String itemId) {
    final key = _itemKeys[itemId];
    if (key?.currentContext != null) {
      Scrollable.ensureVisible(
        key!.currentContext!,
        duration: const Duration(milliseconds: 300),
        curve: Curves.easeInOut,
      );
    }
  }
  
  String? _getItemId(T item) {
    // 아이템에서 고유 ID 추출 로직
    return null;
  }
  
  @override
  Widget build(BuildContext context) {
    return AdaptiveGroupedListView<T>(
      groupedData: widget.groupedData,
      widgetBuilder: _createKeyAwareWidgetBuilder(),
      scrollController: _scrollController,
    );
  }
  
  GroupedWidgetBuilder<T> _createKeyAwareWidgetBuilder() {
    return KeyAwareWidgetBuilder<T>(
      originalBuilder: widget.widgetBuilder,
      groupKeys: _groupKeys,
      itemKeys: _itemKeys,
      getItemId: _getItemId,
    );
  }
  
  @override
  void dispose() {
    _scrollController.dispose();
    super.dispose();
  }
}

성능 최적화 고려사항

대용량 데이터 처리

메시지가 수천 개 이상인 경우를 위한 최적화 방안:

// 1. 가상화된 리스트 사용
class VirtualizedChatView extends StatelessWidget {
  final Map<String, List<Message>> groupedMessages;
  
  @override
  Widget build(BuildContext context) {
    List<dynamic> flattenedItems = _flattenGroupedMessages(groupedMessages);
    
    return ListView.builder(
      itemCount: flattenedItems.length,
      itemBuilder: (context, index) {
        final item = flattenedItems[index];
        
        if (item is String) {
          // 헤더 아이템
          return SectionHeader(timeSection: item);
        } else if (item is Message) {
          // 메시지 아이템
          return MessageBubble(message: item);
        }
        
        return SizedBox.shrink();
      },
    );
  }
  
  List<dynamic> _flattenGroupedMessages(Map<String, List<Message>> grouped) {
    List<dynamic> items = [];
    grouped.forEach((timeSection, messages) {
      items.add(timeSection); // 헤더 추가
      items.addAll(messages); // 메시지들 추가
    });
    return items;
  }
}

// 2. 메모이제이션 활용
class OptimizedChatScreen extends StatefulWidget {
  @override
  _OptimizedChatScreenState createState() => _OptimizedChatScreenState();
}

class _OptimizedChatScreenState extends State<ChatScreen> 
    with AutomaticKeepAliveClientMixin {
  
  Map<String, List<Message>>? _cachedGroupedMessages;
  int _lastMessageCount = 0;
  
  Map<String, List<Message>> get groupedMessages {
    // 메시지 개수가 변경되었을 때만 재계산
    if (_cachedGroupedMessages == null || 
        messageList.length != _lastMessageCount) {
      _cachedGroupedMessages = buildGroupedMessages();
      _lastMessageCount = messageList.length;
    }
    return _cachedGroupedMessages!;
  }
  
  @override
  bool get wantKeepAlive => true;
}

메모리 최적화

// 메시지 페이지네이션
class PaginatedChatView extends StatefulWidget {
  @override
  _PaginatedChatViewState createState() => _PaginatedChatViewState();
}

class _PaginatedChatViewState extends State<PaginatedChatView> {
  static const int PAGE_SIZE = 50;
  int _currentPage = 0;
  List<Message> _displayedMessages = [];
  
  @override
  void initState() {
    super.initState();
    _loadMoreMessages();
  }
  
  void _loadMoreMessages() {
    int startIndex = _currentPage * PAGE_SIZE;
    int endIndex = math.min(startIndex + PAGE_SIZE, allMessages.length);
    
    setState(() {
      _displayedMessages.addAll(allMessages.sublist(startIndex, endIndex));
      _currentPage++;
    });
  }
}

실무 적용 사례

1. 이커머스 주문 히스토리

Map<String, List<Order>> groupOrdersByMonth(List<Order> orders) {
  return orders.fold<Map<String, List<Order>>>({}, (grouped, order) {
    String monthKey = DateFormat('yyyy년 MM월').format(order.orderDate);
    grouped.putIfAbsent(monthKey, () => []).add(order);
    return grouped;
  });
}

2. 뉴스 피드 카테고리 분류

Map<String, List<Article>> groupArticlesByCategory(List<Article> articles) {
  return articles.fold<Map<String, List<Article>>>({}, (grouped, article) {
    grouped.putIfAbsent(article.category, () => []).add(article);
    return grouped;
  });
}

3. 할일 관리 우선순위별 분류

Map<Priority, List<Task>> groupTasksByPriority(List<Task> tasks) {
  return tasks.fold<Map<Priority, List<Task>>>({}, (grouped, task) {
    grouped.putIfAbsent(task.priority, () => []).add(task);
    return grouped;
  });
}

결론

이 글에서 다룬 패턴들은 단순한 채팅 UI를 넘어서 다양한 리스트 기반 애플리케이션에서 활용할 수 있습니다. 핵심은 데이터 그룹핑, 동적 위젯 생성, 유연한 레이아웃 구조입니다.

주요 이점

  1. 사용자 경험 향상: 시간대별 그룹핑으로 직관적인 UI 제공
  2. 코드 재사용성: 그룹핑 로직을 다른 화면에서도 활용 가능
  3. 확장성: 새로운 요구사항에 대한 유연한 대응
  4. 유지보수성: 관심사가 분리된 클린한 코드 구조

다음 단계

  • 상태 관리 패턴: Provider, Riverpod을 활용한 상태 관리 개선
  • 실시간 통신: WebSocket, Firebase를 통한 실시간 채팅 구현
  • 오프라인 지원: SQLite를 활용한 로컬 데이터 저장
  • 푸시 알림: FCM을 통한 백그라운드 메시지 수신

참고 자료