컬렉션을 다루는 고급 기법
앞에서 List, Set, Map을 만들고 기본 조작을 배웠습니다. 이번에는 한 단계 올라갑니다. 반복문을 직접 쓰지 않고도 컬렉션을 변환하고, 걸러내고, 집계하는 방법입니다.
Dart의 컬렉션은 Iterable이라는 공통 인터페이스를 구현합니다. 덕분에 map, where, reduce 같은 고급 메서드를 List, Set 모두에서 같은 방식으로 쓸 수 있습니다.
map — 모든 요소를 변환하기
map은 컬렉션의 각 요소에 함수를 적용해서 새로운 컬렉션을 만듭니다. 원본은 바뀌지 않습니다.
void main() {
var numbers = [1, 2, 3, 4, 5];
// 각 요소를 제곱
var squared = numbers.map((n) => n * n).toList();
print(squared); // [1, 4, 9, 16, 25]
// 문자열로 변환
var asStrings = numbers.map((n) => '숫자 $n').toList();
print(asStrings); // [숫자 1, 숫자 2, 숫자 3, 숫자 4, 숫자 5]
// 이름 목록을 대문자로
var names = ['alice', 'bob', 'charlie'];
var upper = names.map((name) => name.toUpperCase()).toList();
print(upper); // [ALICE, BOB, CHARLIE]
}
map은 Iterable을 반환합니다. .toList()를 붙여야 List가 됩니다. 바로 for-in으로 순회할 때는 변환 없이 써도 됩니다.
실제로 많이 쓰이는 패턴은 Map을 변환하는 것입니다.
void main() {
var prices = {'커피': 4500, '케이크': 6000, '주스': 3500};
// 모든 가격을 10% 인상
var increased = prices.map((key, value) => MapEntry(key, (value * 1.1).round()));
print(increased); // {커피: 4950, 케이크: 6600, 주스: 3850}
}
Map의 map은 MapEntry를 반환합니다. 키와 값을 각각 바꿀 수 있습니다.
where — 조건에 맞는 것만 필터링하기
where는 조건 함수가 true를 반환하는 요소만 골라냅니다.
void main() {
var scores = [95, 42, 87, 63, 78, 51, 90];
// 60점 이상만 필터링
var passing = scores.where((s) => s >= 60).toList();
print(passing); // [95, 87, 63, 78, 90]
// 80점 이상이면서 홀수인 것
var oddHigh = scores.where((s) => s >= 80 && s % 2 != 0).toList();
print(oddHigh); // [95, 87]
// 문자열 필터링
var words = ['dart', 'flutter', 'go', 'python', 'java'];
var longWords = words.where((w) => w.length > 4).toList();
print(longWords); // [flutter, python]
}
map과 where 조합하기 — 메서드 체이닝
map과 where를 이어 붙이면 복잡한 변환을 한 줄로 표현할 수 있습니다.
void main() {
var students = [
{'name': '김철수', 'score': 85},
{'name': '이영희', 'score': 42},
{'name': '박민준', 'score': 92},
{'name': '최지원', 'score': 67},
{'name': '한승우', 'score': 55},
];
// 60점 이상인 학생 이름만 추출
var passed = students
.where((s) => (s['score'] as int) >= 60)
.map((s) => s['name'] as String)
.toList();
print(passed); // [김철수, 박민준, 최지원]
}
where로 먼저 필터링하고, map으로 필요한 정보만 추출합니다. 순서를 바꿔도 결과는 같지만, where를 먼저 쓰면 map이 처리해야 할 요소가 줄어 더 효율적입니다.
reduce와 fold — 집계하기
reduce는 컬렉션의 요소들을 하나의 값으로 합칩니다. 왼쪽부터 차례로 두 값을 합치는 방식입니다.
void main() {
var numbers = [1, 2, 3, 4, 5];
// 합계
int sum = numbers.reduce((acc, val) => acc + val);
print(sum); // 15
// 최댓값
int max = numbers.reduce((a, b) => a > b ? a : b);
print(max); // 5
// 문자열 이어붙이기
var words = ['Hello', 'World', 'Dart'];
String joined = words.reduce((a, b) => '$a $b');
print(joined); // Hello World Dart
}
reduce는 리스트가 비어 있으면 오류가 납니다. 빈 리스트를 처리해야 할 때는 fold를 씁니다.
void main() {
var numbers = [1, 2, 3, 4, 5];
var empty = <int>[];
// fold는 초기값을 받으므로 빈 리스트도 안전
int sum1 = numbers.fold(0, (acc, val) => acc + val);
print(sum1); // 15
int sum2 = empty.fold(0, (acc, val) => acc + val);
print(sum2); // 0 (초기값 반환)
// fold로 Map 만들기 (단어 카운터)
var words = ['dart', 'flutter', 'dart', 'go', 'dart'];
var counter = words.fold<Map<String, int>>({}, (map, word) {
map[word] = (map[word] ?? 0) + 1;
return map;
});
print(counter); // {dart: 3, flutter: 1, go: 1}
}
fold의 첫 번째 인자가 초기값입니다. 반환 타입이 요소 타입과 달라도 됩니다. 위 예제처럼 int 리스트에서 Map을 만들 수 있습니다.
any와 every — 조건 검사하기
any는 하나라도 조건을 만족하면 true, every는 전부 만족해야 true입니다.
void main() {
var scores = [85, 92, 78, 60, 88];
// 하나라도 100점인가?
print(scores.any((s) => s == 100)); // false
// 하나라도 60점 미만인가?
print(scores.any((s) => s < 60)); // false
// 모두 50점 이상인가?
print(scores.every((s) => s >= 50)); // true
// 모두 90점 이상인가?
print(scores.every((s) => s >= 90)); // false
// 실용 예제 — 모든 필드가 입력됐는지 검증
var requiredFields = ['name', 'email', 'phone'];
var userData = {'name': '홍길동', 'email': '[email protected]', 'phone': ''};
bool allFilled = requiredFields.every(
(field) => (userData[field] ?? '').isNotEmpty,
);
print(allFilled ? '제출 가능' : '빈 칸이 있습니다.'); // 빈 칸이 있습니다.
}
any와 every는 조건을 만족하는 즉시 평가를 멈춥니다. 전체를 다 확인하지 않아도 되므로 효율적입니다.
toList와 toSet — 컬렉션 변환
Iterable을 List나 Set으로 바꿉니다.
void main() {
var numbers = [1, 2, 3, 2, 1, 4, 3];
// List → Set (중복 제거)
var unique = numbers.toSet();
print(unique); // {1, 2, 3, 4}
// Set → List (다시 리스트로)
var uniqueList = numbers.toSet().toList();
print(uniqueList); // [1, 2, 3, 4]
// 중복 제거 후 정렬
var sorted = numbers.toSet().toList()..sort();
print(sorted); // [1, 2, 3, 4]
}
..sort()에서 ..는 캐스케이드 연산자입니다. 메서드를 호출하되 결과 대신 원래 객체를 반환합니다. list.sort()는 void를 반환하므로 이어 쓸 수 없지만, ..sort()를 쓰면 체이닝이 가능합니다.
forEach vs for-in — 무엇을 써야 할까
둘 다 순회하지만 성격이 다릅니다.
void main() {
var fruits = ['사과', '바나나', '딸기'];
// forEach — 함수를 전달
fruits.forEach((fruit) {
print(fruit);
});
// for-in — 루프 문법
for (String fruit in fruits) {
print(fruit);
}
}
결과는 같습니다. 그러면 언제 어느 것을 써야 할까요.
for-in이 더 적합한 경우입니다. 루프 중간에 break나 continue가 필요할 때, await를 써야 할 때(비동기 상황), 인덱스가 필요할 때입니다.
void main() {
var numbers = [1, 5, 3, 8, 2, 7];
// break가 필요하면 for-in
for (int n in numbers) {
if (n > 6) {
print('$n에서 중단');
break;
}
print(n);
}
// 인덱스가 필요하면 for-in + enumerate 패턴
for (int i = 0; i < numbers.length; i++) {
print('$i번째: ${numbers[i]}');
}
}
forEach는 단순히 모든 요소에 같은 동작을 할 때 간결하게 쓸 수 있습니다. 하지만 break도 안 되고, await도 안 됩니다. 일반적으로는 for-in이 더 유연합니다.
실전 예제 — 학생 성적 분석
배운 모든 메서드를 활용한 실전 예제입니다.
void main() {
var students = [
{'name': '김철수', 'score': 85, 'grade': 'A'},
{'name': '이영희', 'score': 42, 'grade': 'F'},
{'name': '박민준', 'score': 92, 'grade': 'A'},
{'name': '최지원', 'score': 67, 'grade': 'C'},
{'name': '한승우', 'score': 55, 'grade': 'D'},
{'name': '정다은', 'score': 78, 'grade': 'B'},
];
// 합격 여부 (60점 이상)
bool anyFail = students.any((s) => (s['score'] as int) < 60);
bool allPass = students.every((s) => (s['score'] as int) >= 60);
print('불합격자 있음: $anyFail'); // true
print('전원 합격: $allPass'); // false
// 평균 점수
double avg = students
.map((s) => s['score'] as int)
.fold(0, (a, b) => a + b) / students.length;
print('평균: ${avg.toStringAsFixed(1)}'); // 69.8
// 합격자 이름 목록
var passNames = students
.where((s) => (s['score'] as int) >= 60)
.map((s) => s['name'] as String)
.toList();
print('합격자: $passNames');
// [김철수, 박민준, 최지원, 정다은]
// 학점별 학생 수 (Map으로 집계)
var gradeCount = students.fold<Map<String, int>>({}, (map, s) {
String grade = s['grade'] as String;
map[grade] = (map[grade] ?? 0) + 1;
return map;
});
print('학점 분포: $gradeCount');
// {A: 2, F: 1, C: 1, D: 1, B: 1}
// 점수 내림차순 정렬 후 상위 3명
var top3 = students.toList()
..sort((a, b) => (b['score'] as int).compareTo(a['score'] as int));
var topNames = top3.take(3).map((s) => '${s['name']}(${s['score']})').toList();
print('상위 3명: $topNames');
// [박민준(92), 김철수(85), 정다은(78)]
}
반복문 없이 선언적으로 데이터를 처리하는 것이 핵심입니다. 무엇을 하는지(what)가 코드에서 분명히 드러납니다.
다음 챕터에서는 스프레드 연산자와 컬렉션 if·for를 배웁니다. 리스트 안에서 바로 조건문과 반복문을 쓸 수 있는 Dart만의 문법입니다.