Tuesday, December 27, 2022

Flutter, List와 Map을 200% 더 똑똑하게 쓰는 법: collection 패키지 심층 분석

Flutter와 Dart로 애플리케이션을 개발하다 보면 우리는 수많은 데이터를 다루게 됩니다. 사용자의 프로필 목록, 상품 리스트, 채팅 메시지, 복잡한 JSON 응답 등 거의 모든 기능은 List, Map, Set과 같은 컬렉션(Collection) 데이터 구조를 기반으로 합니다. Dart가 제공하는 기본 컬렉션 기능도 훌륭하지만, 조금만 더 복잡한 시나리오에 직면하면 코드가 금세 길고 지저분해지는 경험을 누구나 한 번쯤은 해보셨을 겁니다.

예를 들어, 두 개의 리스트가 내용물은 같지만 메모리 주소가 달라서 == 비교가 false로 나오는 상황, 특정 기준에 따라 리스트를 그룹화해야 하는 상황, 혹은 여러 개의 정렬 기준을 적용해야 하는 경우를 떠올려 보세요. 이런 문제들을 해결하기 위해 매번 직접 유틸리티 함수를 만들거나, 복잡한 반복문과 조건문을 작성하고 계신가요? 바로 이런 고민을 해결하기 위해 Dart 팀이 직접 만들고 관리하는 공식 패키지, 'collection'이 존재합니다.

collection 패키지는 단순히 몇 가지 편의 기능을 추가하는 수준을 넘어, 데이터 처리의 패러다임을 바꿀 수 있는 강력한 도구들을 제공합니다. 이 패키지를 사용하면 다음과 같은 이점을 얻을 수 있습니다.

  • 코드 가독성 향상: 복잡한 로직을 선언적이고 직관적인 함수 호출로 대체하여 코드의 의도를 명확하게 드러냅니다.
  • 보일러플레이트 코드 감소: 반복적으로 작성하던 데이터 처리 코드를 줄여 개발 생산성을 크게 높입니다.
  • 성능 최적화: Dart 전문가들이 만든 고도로 최적화된 알고리즘을 사용하여 직접 구현하는 것보다 뛰어난 성능을 보장합니다.
  • 안정성 및 신뢰성: Dart 팀이 직접 관리하는 만큼, 언어의 업데이트와 호환성을 유지하며 최고 수준의 안정성을 제공합니다.

이 글에서는 collection 패키지가 제공하는 핵심 기능들을 깊이 있게 파고들어, 실제 Flutter/Dart 개발 현장에서 어떻게 코드를 혁신할 수 있는지 구체적인 예제와 함께 살펴보겠습니다. 이 글을 끝까지 읽으시면 여러분의 컬렉션 처리 코드가 얼마나 더 간결하고, 효율적이며, 우아해질 수 있는지 깨닫게 될 것입니다.

프로젝트에 collection 패키지 추가하기

가장 먼저 할 일은 pubspec.yaml 파일에 collection 패키지를 추가하는 것입니다. 터미널을 열고 프로젝트 루트 디렉토리에서 아래 명령어를 실행하세요.

flutter pub add collection

또는 pubspec.yaml 파일의 dependencies 섹션에 직접 추가할 수도 있습니다.

dependencies:
  flutter:
    sdk: flutter
  collection: ^1.18.0 # 최신 버전을 확인하고 사용하세요.

설정이 완료되었다면, 이제 패키지를 사용하려는 Dart 파일 상단에 다음 코드를 추가하여 라이브러리를 가져옵니다.

import 'package:collection/collection.dart';

이제 모든 준비가 끝났습니다. collection 패키지의 마법 같은 세계로 떠나봅시다.

1. 동등성 비교의 혁신: Equality 클래스

Dart에서 두 객체를 비교할 때 사용하는 == 연산자는 기본적으로 '참조 동등성(reference equality)'을 확인합니다. 즉, 두 변수가 메모리 상의 동일한 객체를 가리키고 있는지를 비교합니다. 이는 원시 타입(int, String, bool 등)에서는 문제가 없지만, ListMap과 같은 컬렉션 객체에서는 우리의 의도와 다르게 동작할 때가 많습니다.

문제점: 내용이 같아도 다르다고?

다음 코드를 보세요. 두 리스트는 내용이 완전히 동일하지만, == 연산의 결과는 false입니다.

void main() {
  var list1 = [1, 2, 3];
  var list2 = [1, 2, 3];

  print(list1 == list2); // 출력: false

  var map1 = {'a': 1, 'b': 2};
  var map2 = {'a': 1, 'b': 2};

  print(map1 == map2); // 출력: false
}

두 변수는 각각 새로운 리스트와 맵 객체를 생성하여 가리키고 있으므로, 메모리 주소가 달라 false가 반환됩니다. 이런 문제를 해결하기 위해 컬렉션의 내용물을 하나하나 비교하는 함수를 직접 만들어야 할까요? collection 패키지가 이 문제를 우아하게 해결해 줍니다.

해결책: CollectionEquality

collection 패키지는 컬렉션의 '내용 동등성(deep equality)'을 비교할 수 있는 다양한 Equality 클래스를 제공합니다.

  • ListEquality: 리스트의 원소들을 순서대로 비교합니다.
  • SetEquality: 셋(Set)의 원소들을 비교합니다. 순서는 무관합니다.
  • MapEquality: 맵(Map)의 키와 값을 비교합니다.
  • DeepCollectionEquality: 중첩된 컬렉션까지 재귀적으로 들어가 모든 내용을 비교합니다. 가장 강력하고 일반적으로 사용됩니다.

이 클래스들의 equals() 메서드를 사용하면 우리가 원했던 내용 비교가 가능해집니다.

import 'package:collection/collection.dart';

void main() {
  var list1 = [1, 2, [3, 4]];
  var list2 = [1, 2, [3, 4]];

  // 기본 비교
  print(list1 == list2); // 출력: false

  // DeepCollectionEquality를 사용한 내용 비교
  var deepEquality = DeepCollectionEquality();
  print(deepEquality.equals(list1, list2)); // 출력: true

  var map1 = {'a': 1, 'b': {'c': 2}};
  var map2 = {'a': 1, 'b': {'c': 2}};

  print(deepEquality.equals(map1, map2)); // 출력: true
}

DeepCollectionEquality는 리스트 안의 리스트, 맵 안의 맵처럼 복잡하게 중첩된 구조라도 끝까지 파고들어 각 원소의 동등성을 확인해 줍니다. 이는 서버로부터 받은 복잡한 JSON 데이터를 비교하거나, 상태 관리에서 이전 상태와 새로운 상태의 변화를 감지할 때 매우 유용합니다.

심화: 순서에 상관없는 비교 (UnorderedEquality)

때로는 리스트의 내용물이 중요하지만 순서는 상관없는 경우가 있습니다. 예를 들어, 사용자가 선택한 태그 목록이 ['flutter', 'dart']이든 ['dart', 'flutter']이든 동일하게 취급하고 싶을 때입니다. 이럴 때는 ListEqualityUnorderedElementsAre와 조합하거나, 더 간단하게는 `UnorderedCollectionEquality`를 사용할 수 있습니다.

import 'package:collection/collection.dart';

void main() {
  var list1 = ['flutter', 'dart'];
  var list2 = ['dart', 'flutter'];

  // 순서를 고려하는 ListEquality
  print(ListEquality().equals(list1, list2)); // 출력: false

  // 순서를 무시하는 UnorderedCollectionEquality
  print(UnorderedCollectionEquality().equals(list1, list2)); // 출력: true
}

hash() 메서드의 중요성

Equality 클래스들은 equals() 메서드뿐만 아니라 hash() 메서드도 제공합니다. 객체의 해시 코드를 생성하는 이 메서드는 Set이나 Map의 키처럼 객체를 해싱 기반의 컬렉션에 저장할 때 필수적입니다. equals()true를 반환하는 두 객체는 hash()도 반드시 동일한 값을 반환해야 한다는 규칙을 기억하세요. collection의 Equality 클래스들은 이 규칙을 완벽하게 지켜줍니다.

예를 들어, ListSet의 원소로 사용하고 싶을 때 유용합니다. 기본적으로 List는 해시 코드가 일정하지 않아 Set에 넣을 수 없지만, Equality를 활용하면 가능합니다.

import 'package:collection/collection.dart';

void main() {
  final equality = ListEquality<int>();
  
  // ListEquality를 기반으로 하는 Set 생성
  var listSet = SetEquality(equality);
  var mySet = <List<int>>{}; // 일반 Set

  var listA = [1, 2];
  var listB = [3, 4];
  var listC = [1, 2]; // listA와 내용이 같음

  mySet.add(listA);
  mySet.add(listB);
  mySet.add(listC);

  // 일반 Set에서는 listA와 listC가 다른 객체로 취급됨
  print(mySet); // 출력: {[1, 2], [3, 4], [1, 2]} - 중복 허용!

  // Equality를 사용하는 Set을 만들려면 `HashSet`을 사용해야 합니다.
  var specializedSet = HashSet<List<int>>(
    equals: equality.equals,
    hashCode: equality.hash,
  );
  
  specializedSet.add(listA);
  specializedSet.add(listB);
  specializedSet.add(listC);

  // specializedSet에서는 listA와 listC가 같은 것으로 간주됨
  print(specializedSet); // 출력: {[1, 2], [3, 4]} - 중복 제거!
}

이처럼 Equality 기능은 단순한 비교를 넘어 Dart의 컬렉션 시스템을 더 깊이 있고 올바르게 활용할 수 있도록 돕는 핵심적인 역할을 합니다.

2. 데이터 그룹화의 마법: groupBy 함수

애플리케이션을 만들다 보면 플랫한(flat) 리스트를 특정 기준에 따라 여러 그룹으로 묶어야 하는 경우가 매우 흔합니다. 예를 들면 다음과 같습니다.

  • 거래 내역 리스트를 날짜별로 그룹화하기
  • 직원 목록을 부서별로 그룹화하기
  • 상품 리스트를 카테고리별로 그룹화하기

이런 기능을 구현하기 위해 Map을 하나 만들고, for 루프를 돌면서 if 문으로 맵에 키가 있는지 확인하고, 없으면 새로운 리스트를 생성해서 넣고, 있으면 기존 리스트에 추가하는 복잡한 코드를 작성해 본 경험이 있으실 겁니다.

문제점: 지루하고 반복적인 그룹화 코드

class Product {
  final String name;
  final String category;
  Product(this.name, this.category);
}

void main() {
  var products = [
    Product('Laptop', 'Electronics'),
    Product('Keyboard', 'Electronics'),
    Product('Apple', 'Fruits'),
    Product('Banana', 'Fruits'),
    Product('T-shirt', 'Fashion'),
  ];

  // 직접 그룹화하는 로직
  var groupedProducts = <String, List<Product>>{};
  for (var product in products) {
    if (!groupedProducts.containsKey(product.category)) {
      groupedProducts[product.category] = [];
    }
    groupedProducts[product.category]!.add(product);
  }

  print(groupedProducts);
  // 출력: {Electronics: [Instance of 'Product', Instance of 'Product'], Fruits: [Instance of 'Product', Instance of 'Product'], Fashion: [Instance of 'Product']}
}

이 코드는 동작은 하지만, 로직이 장황하고 어떤 데이터를 다루든 비슷한 구조의 코드를 반복해서 작성해야 합니다. 가독성도 떨어집니다.

해결책: 단 한 줄로 끝내는 groupBy

collection 패키지의 groupBy 함수는 이 모든 과정을 단 한 줄의 코드로 압축해 줍니다. groupBy는 이터러블(Iterable) 객체와 그룹화할 키를 추출하는 함수를 인자로 받아, Map<Key, List<Element>> 형태로 결과를 반환합니다.

import 'package:collection/collection.dart';

// Product 클래스는 위와 동일

void main() {
  var products = [
    Product('Laptop', 'Electronics'),
    Product('Keyboard', 'Electronics'),
    Product('Apple', 'Fruits'),
    Product('Banana', 'Fruits'),
    Product('T-shirt', 'Fashion'),
  ];

  // groupBy 사용
  final groupedProducts = groupBy(products, (Product p) => p.category);

  print(groupedProducts);
  // 출력: {Electronics: [Instance of 'Product', Instance of 'Product'], Fruits: [Instance of 'Product', Instance of 'Product'], Fashion: [Instance of 'Product']}
}

결과는 위와 동일하지만 코드가 훨씬 간결하고 의도가 명확해졌습니다. "products 리스트를 각 product의 category를 기준으로 그룹화하라"는 의미가 코드에 그대로 드러납니다. groupBy는 Flutter UI를 구성할 때 특히 강력한 힘을 발휘합니다.

Flutter UI와 groupBy의 환상적인 조합

ListView.builder를 사용해 그룹화된 목록(예: 섹션 헤더가 있는 리스트)을 만든다고 상상해 보세요. groupBy로 데이터를 미리 가공해두면 UI 코드가 놀랍도록 단순해집니다.

// 가상의 Flutter 위젯 코드
import 'package:flutter/material.dart';
import 'package:collection/collection.dart';

// ... Product 클래스 정의 ...
// ... products 데이터 ...

class ProductScreen extends StatelessWidget {
  final List<Product> products;
  
  const ProductScreen({Key? key, required this.products}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    // 1. 데이터를 카테고리별로 그룹화한다.
    final grouped = groupBy(products, (Product p) => p.category);
    final categories = grouped.keys.toList();

    return ListView.builder(
      // 2. 전체 아이템 수는 카테고리 수 + 총 상품 수
      itemCount: categories.length + products.length,
      itemBuilder: (context, index) {
        // 이 부분은 더 정교한 로직이 필요하지만, groupBy의 기본 아이디어를 보여줍니다.
        // 실제 구현에서는 grouped_list와 같은 패키지를 사용하면 더 편리합니다.
        
        // 여기서는 간단한 개념만 보여주기 위해 논리를 단순화합니다.
        // 실제로는 각 섹션의 아이템 수를 누적하여 인덱스를 계산해야 합니다.
        
        // 이 예제는 groupBy가 어떻게 데이터를 구조화하여 UI 로직을 단순화하는지 보여주는 데 목적이 있습니다.
        // 예를 들어, 카테고리별로 ListView를 중첩하는 구조로 만들 수 있습니다.
        return Column(
          children: categories.map((category) {
            final itemsInCategory = grouped[category]!;
            return Column(
              crossAxisAlignment: CrossAxisAlignment.start,
              children: [
                // 섹션 헤더
                Padding(
                  padding: const EdgeInsets.all(8.0),
                  child: Text(category, style: Theme.of(context).textTheme.headline6),
                ),
                // 해당 카테고리의 아이템들
                ...itemsInCategory.map((product) => ListTile(title: Text(product.name))),
              ],
            );
          }).toList(),
        );
      },
    );
  }
}

위 예시처럼, groupBy를 사용하면 복잡한 중첩 반복문 없이도 데이터를 UI 구조에 맞는 형태로 손쉽게 변환할 수 있습니다. 이는 비즈니스 로직과 UI 렌더링 로직을 깔끔하게 분리하는 데 큰 도움이 됩니다.

3. 컬렉션 확장 함수: 코드의 군살을 빼다

collection 패키지는 Iterable(List, Set 등)에 유용한 확장 함수(extension methods)들을 다수 추가하여, 흔히 발생하는 여러 상황을 더 간결하게 처리할 수 있도록 돕습니다.

null-safe한 원소 찾기: firstWhereOrNull

기본 firstWhere 메서드는 조건에 맞는 원소가 없을 경우 StateError 예외를 발생시킵니다. 그래서 보통 try-catch로 감싸거나, where로 필터링한 후 isEmpty를 체크하는 번거로운 과정을 거쳐야 합니다.

// 기존 방식
var list = [1, 2, 3];
int? result;
try {
  result = list.firstWhere((e) => e > 5);
} catch (e) {
  result = null;
}
print(result); // null

firstWhereOrNull 확장 함수는 이 과정을 한 줄로 줄여줍니다. 조건에 맞는 원소가 있으면 해당 원소를, 없으면 null을 반환합니다.

import 'package:collection/collection.dart';

var list = [1, 2, 3];
final result = list.firstWhereOrNull((e) => e > 5);
print(result); // null

final result2 = list.firstWhereOrNull((e) => e > 2);
print(result2); // 3

이와 유사하게, 조건에 맞는 원소가 유일해야 하는 singleWhere에 대응하는 singleWhereOrNull도 제공됩니다.

인덱스와 함께 반복하기: forEachIndexed

반복문을 실행할 때 원소의 값과 인덱스가 모두 필요한 경우가 많습니다. 보통은 고전적인 for 루프를 사용합니다.

var fruits = ['apple', 'banana', 'orange'];
for (var i = 0; i < fruits.length; i++) {
  print('Index $i: ${fruits[i]}');
}

forEachIndexed를 사용하면 더 선언적이고 깔끔한 코드를 작성할 수 있습니다.

import 'package:collection/collection.dart';

var fruits = ['apple', 'banana', 'orange'];
fruits.forEachIndexed((index, element) {
  print('Index $index: $element');
});

마찬가지로 인덱스를 활용해 새로운 리스트를 생성하고 싶을 때는 mapIndexed를 사용할 수 있습니다.

import 'package:collection/collection.dart';

var fruits = ['apple', 'banana', 'orange'];
final indexedFruits = fruits.mapIndexed((index, element) => '$index: $element').toList();
print(indexedFruits); // [0: apple, 1: banana, 2: orange]

청크(Chunk) 단위로 나누기: chunked

긴 리스트를 일정한 크기의 여러 작은 리스트로 나누고 싶을 때 (예: 갤러리 앱에서 한 줄에 3개의 이미지를 보여주기 위해 데이터를 3개씩 묶을 때) chunked 함수가 매우 유용합니다.

import 'package:collection/collection.dart';

final numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];

// 리스트를 3개씩 묶기
final chunks = numbers.chunked(3);
print(chunks.toList()); // [[1, 2, 3], [4, 5, 6], [7, 8, 9], [10]]

// Flutter 그리드 뷰와 함께 활용
// GridView.builder(
//   itemCount: chunks.length,
//   itemBuilder: (context, index) {
//     final chunk = chunks.elementAt(index);
//     return Row(
//       children: chunk.map((item) => Expanded(child: MyItemWidget(item))).toList(),
//     );
//   }
// )

이 외에도 collection 패키지는 sum, average, min, max 등 숫자 리스트에 대한 집계 함수, 두 리스트를 합치는 followedBy 등 수많은 편의 기능을 제공하여 개발자의 삶을 윤택하게 만들어줍니다.

4. 정렬, 그 이상의 것: 비교자와 안정 정렬

리스트의 sort() 메서드는 간단한 정렬에는 유용하지만, 복잡한 요구사항 앞에서는 한계를 보입니다. 예를 들어, 여러 기준으로 정렬(1차 정렬 후 1차 정렬 키가 같으면 2차 기준으로 정렬)하거나, 대소문자 구분 없이 문자열을 정렬해야 하는 경우가 그렇습니다.

다중 조건 정렬: compareBy와 thenBy

collection 패키지는 정렬 로직을 조합할 수 있는 강력한 함수들을 제공합니다. compareBy로 첫 번째 정렬 기준을 만들고, thenBy로 다음 정렬 기준을 계속해서 연결할 수 있습니다.

예를 들어, 사용자 목록을 1) 레벨(level)이 높은 순으로, 2) 레벨이 같다면 이름(name)의 알파벳 순으로 정렬하는 시나리오를 생각해 봅시다.

import 'package:collection/collection.dart';

class User {
  final String name;
  final int level;
  User(this.name, this.level);
  @override
  String toString() => 'User(name: $name, level: $level)';
}

void main() {
  var users = [
    User('Chris', 5),
    User('Alice', 7),
    User('Bob', 5),
    User('David', 3),
  ];

  // sort 메서드에 비교 함수를 전달
  users.sort((a, b) {
    // 1. 레벨을 내림차순으로 비교
    final levelCompare = b.level.compareTo(a.level);
    if (levelCompare != 0) {
      return levelCompare;
    }
    // 2. 레벨이 같으면 이름을 오름차순으로 비교
    return a.name.compareTo(b.name);
  });

  print(users);
  // [User(name: Alice, level: 7), User(name: Bob, level: 5), User(name: Chris, level: 5), User(name: David, level: 3)]
}

위 코드는 동작하지만, 비교 로직이 복잡하고 재사용이 어렵습니다. compareBythenBy를 사용하면 이 로직을 훨씬 우아하게 표현할 수 있습니다.

import 'package:collection/collection.dart';

// User 클래스는 위와 동일

void main() {
  var users = [
    User('Chris', 5),
    User('Alice', 7),
    User('Bob', 5),
    User('David', 3),
  ];
  
  // compareBy와 thenBy를 사용한 정렬
  users.sort(
    // 1. User 객체에서 level을 추출하여 내림차순 정렬 (b.compareTo(a))
    (a, b) => compareBy(b, a, (user) => user.level)
    // 2. 그 다음 이름(name)을 추출하여 오름차순 정렬
            .thenBy((user) => user.name)
  );

  // 더 간결한 sortBy 확장 함수 사용 (내부적으로 위와 유사하게 동작)
  // users.sortBy((user) => -user.level).thenSortBy((user) => user.name);
  // 하지만 compareBy/thenBy 조합이 더 명시적이고 유연합니다.

  print(users);
  // [User(name: Alice, level: 7), User(name: Bob, level: 5), User(name: Chris, level: 5), User(name: David, level: 3)]
}

위 코드에서 `compareBy`는 정렬할 두 객체와 키를 추출하는 함수를 받습니다. `thenBy`는 앞선 비교 결과가 0(동일함)일 경우에만 실행되는 다음 비교 함수를 정의합니다. 이를 통해 여러 정렬 조건을 체인처럼 엮을 수 있습니다.

안정 정렬(Stable Sort): mergeSort

기본 List.sort()는 안정 정렬(stable sort)을 보장하지 않습니다. 안정 정렬이란, 정렬 키가 동일한 원소들의 기존 상대적 순서가 정렬 후에도 유지되는 것을 의미합니다. 예를 들어, 위 `User` 예제에서 'Bob'과 'Chris'는 레벨이 5로 같습니다. 만약 원본 리스트에서 'Chris'가 'Bob'보다 앞에 있었다면, 불안정 정렬에서는 정렬 후 'Bob'이 'Chris' 앞으로 올 수도 있습니다. 하지만 안정 정렬에서는 'Chris'가 여전히 'Bob'보다 앞에 위치하게 됩니다.

이러한 순서 유지가 중요한 시나리오에서는 collection 패키지가 제공하는 mergeSort 함수를 사용해야 합니다. mergeSort는 병합 정렬 알고리즘을 기반으로 하며, 안정 정렬을 보장합니다.

import 'package:collection/collection.dart';

void main() {
  var users = [
    User('Chris', 5), // Chris가 Bob보다 먼저 옴
    User('Alice', 7),
    User('Bob', 5),
    User('David', 3),
  ];
  
  // 레벨(내림차순) 기준으로만 정렬. 
  // 안정 정렬이므로 레벨이 같은 Chris와 Bob의 순서는 유지되어야 함.
  mergeSort(users, compare: (a, b) => b.level.compareTo(a.level));
  
  print(users);
  // [User(name: Alice, level: 7), User(name: Chris, level: 5), User(name: Bob, level: 5), User(name: David, level: 3)]
  // Chris가 Bob보다 앞에 있는 것을 볼 수 있다.
}

mergeSortList.sort()와 동일하게 비교(compare) 함수를 인자로 받으므로, 앞에서 다룬 compareBy/thenBy 조합과도 함께 사용할 수 있습니다.

5. 특수 목적 컬렉션

collection 패키지는 일반적인 List, Map, Set 외에도 특정 목적에 최적화된 유용한 컬렉션 클래스들을 제공합니다.

우선순위 큐: PriorityQueue

PriorityQueue는 일반적인 큐(Queue)처럼 원소를 추가(add)하고 제거(removeFirst)하지만, 한 가지 큰 차이점이 있습니다. 원소를 제거할 때 들어온 순서(FIFO)가 아니라 '가장 높은 우선순위'를 가진 원소를 먼저 제거합니다. 우선순위는 큐를 생성할 때 전달하는 비교(comparator) 함수에 의해 결정됩니다.

이는 작업 스케줄링, 최단 경로 탐색 알고리즘(예: 다익스트라) 등에서 매우 유용합니다.

import 'package:collection/collection.dart';

class Task {
  final String name;
  final int priority; // 숫자가 낮을수록 우선순위가 높음
  Task(this.name, this.priority);
  @override
  String toString() => 'Task($name, priority: $priority)';
}

void main() {
  // 우선순위(priority)가 낮은 순서대로 처리하는 우선순위 큐
  final queue = PriorityQueue<Task>((a, b) => a.priority.compareTo(b.priority));
  
  queue.add(Task('보통 작업', 5));
  queue.add(Task('긴급 작업', 1));
  queue.add(Task('중요 작업', 3));
  queue.add(Task('사소한 작업', 10));

  while (queue.isNotEmpty) {
    print('처리 중: ${queue.removeFirst()}');
  }
}

실행 결과:

처리 중: Task(긴급 작업, priority: 1)
처리 중: Task(중요 작업, priority: 3)
처리 중: Task(보통 작업, priority: 5)
처리 중: Task(사소한 작업, priority: 10)

들어온 순서와 관계없이 우선순위가 높은(숫자가 낮은) 작업부터 처리되는 것을 확인할 수 있습니다.

정규화된 맵: CanonicalizedMap

CanonicalizedMap은 맵의 키(key)를 저장하거나 조회할 때, 키를 '정규화(canonicalize)'하는 함수를 거치도록 만든 특수한 맵입니다. 가장 흔한 사용 사례는 대소문자를 구분하지 않는 맵을 만드는 것입니다.

import 'package:collection/collection.dart';

void main() {
  // 키를 소문자로 정규화하는 맵 생성
  final headerMap = CanonicalizedMap<String, String, String>.from(
    {'Content-Type': 'application/json'},
    (key) => key.toLowerCase(), // 정규화 함수
  );

  // 어떤 대소문자로 접근하든 동일한 값에 접근 가능
  print(headerMap['content-type']); // application/json
  print(headerMap['Content-type']); // application/json
  print(headerMap['CONTENT-TYPE']); // application/json

  headerMap['HOST'] = 'example.com';
  print(headerMap['host']); // example.com
}

HTTP 헤더 처리나 사용자 입력을 키로 사용하는 경우 등, 키의 형식이 일정하지 않을 수 있는 상황에서 매우 유용하게 쓰일 수 있습니다.

결론: 왜 collection 패키지를 반드시 사용해야 하는가?

지금까지 살펴본 것처럼 collection 패키지는 Dart의 기본 컬렉션 기능을 한 차원 높은 수준으로 끌어올리는 필수적인 라이브러리입니다. 단순히 코드를 짧게 만드는 것을 넘어, 코드의 의도를 명확히 하고, 복잡한 데이터 처리 로직을 표준화하며, 보이지 않는 곳에서 성능을 최적화해 줍니다.

Equality를 통한 정확한 상태 비교, groupBy를 통한 우아한 데이터 구조화, 각종 확장 함수를 통한 생산성 향상, 고급 정렬 기능을 통한 유연한 데이터 정렬, 그리고 특수 목적 컬렉션을 통한 문제 해결 능력까지. 이 모든 것이 여러분의 Flutter/Dart 프로젝트의 품질을 높여줄 것입니다.

아직 collection 패키지를 사용해보지 않으셨다면, 지금 바로 여러분의 프로젝트에 추가해 보세요. 아마도 "이걸 왜 이제야 알았을까?"라고 생각하게 될 것입니다. Dart 팀이 제공하는 이 강력하고 신뢰할 수 있는 도구를 활용하여, 더 깨끗하고, 더 효율적이며, 더 즐거운 개발을 경험하시길 바랍니다.


0 개의 댓글:

Post a Comment