Skip to content

컬렉션과 반복문

Dart는 데이터를 저장하고 조작하기 위한 다양한 컬렉션 타입과 반복문을 제공합니다. 이 장에서는 Dart의 컬렉션 타입(List, Set, Map)과 이를 처리하기 위한 다양한 반복문 및 반복 연산을 알아보겠습니다.

List는 순서가 있는 항목의 집합으로, 다른 언어의 배열과 유사합니다.

// 리터럴을 사용한 생성
var fruits = ['사과', '바나나', '오렌지'];
// 타입을 지정한 리스트
List<String> names = ['홍길동', '김철수', '이영희'];
// 빈 리스트 생성
var emptyList = <int>[];
// 생성자를 이용한 리스트 생성
var fixedList = List<int>.filled(5, 0); // [0, 0, 0, 0, 0]
var growableList = List<int>.empty(growable: true);
var generatedList = List<int>.generate(5, (i) => i * i); // [0, 1, 4, 9, 16]
var fruits = ['사과', '바나나', '오렌지', '딸기', '포도'];
// 인덱스로 접근
print(fruits[0]); // 사과
// 길이 확인
print(fruits.length); // 5
// 첫 번째와 마지막 항목
print(fruits.first); // 사과
print(fruits.last); // 포도
// 추가
fruits.add('키위');
print(fruits); // [사과, 바나나, 오렌지, 딸기, 포도, 키위]
// 여러 항목 추가
fruits.addAll(['멜론', '수박']);
print(fruits); // [사과, 바나나, 오렌지, 딸기, 포도, 키위, 멜론, 수박]
// 삭제
fruits.remove('바나나');
print(fruits); // [사과, 오렌지, 딸기, 포도, 키위, 멜론, 수박]
// 인덱스로 삭제
fruits.removeAt(1);
print(fruits); // [사과, 딸기, 포도, 키위, 멜론, 수박]
// 조건으로 삭제
fruits.removeWhere((fruit) => fruit.length <= 2);
print(fruits); // [사과, 딸기, 포도, 키위, 멜론, 수박]
// 정렬
fruits.sort();
print(fruits); // [딸기, 멜론, 사과, 수박, 포도, 키위]
// 인덱스 찾기
print(fruits.indexOf('포도')); // 4
// 존재 여부 확인
print(fruits.contains('사과')); // true
print(fruits.contains('바나나')); // false
var numbers = [1, 2, 3, 4, 5];
// 매핑 (각 요소 변환)
var doubled = numbers.map((n) => n * 2).toList();
print(doubled); // [2, 4, 6, 8, 10]
// 필터링 (조건에 맞는 요소만 선택)
var evenNumbers = numbers.where((n) => n.isEven).toList();
print(evenNumbers); // [2, 4]
// fold (누적 연산)
var sum = numbers.fold<int>(0, (prev, curr) => prev + curr);
print(sum); // 15
// reduce (항목들을 하나로 결합)
var product = numbers.reduce((a, b) => a * b);
print(product); // 120
// 평탄화 (중첩 리스트를 단일 리스트로)
var nested = [[1, 2], [3, 4], [5]];
var flattened = nested.expand((list) => list).toList();
print(flattened); // [1, 2, 3, 4, 5]
var numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
// 범위 추출 (sublist)
var slice = numbers.sublist(2, 5);
print(slice); // [3, 4, 5]
// 리스트 복사
var copy = List<int>.from(numbers);
print(copy); // [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
// 모든 요소 바꾸기
numbers.replaceRange(0, 3, [99, 98, 97]);
print(numbers); // [99, 98, 97, 4, 5, 6, 7, 8, 9, 10]
// 뒤집기
var reversed = numbers.reversed.toList();
print(reversed); // [10, 9, 8, 7, 6, 5, 4, 97, 98, 99]
// 리스트의 특정 부분 채우기
numbers.fillRange(5, 8, 0);
print(numbers); // [99, 98, 97, 4, 5, 0, 0, 0, 9, 10]

Set은 중복되지 않는 항목의 컬렉션입니다.

// 리터럴을 사용한 생성
var fruits = {'사과', '바나나', '오렌지'};
// 타입을 지정한 집합
Set<String> names = {'홍길동', '김철수', '이영희'};
// 빈 집합 생성
var emptySet = <int>{};
// 리스트에서 생성 (중복 제거됨)
var numbers = Set<int>.from([1, 2, 2, 3, 3, 3, 4, 5, 5]);
print(numbers); // {1, 2, 3, 4, 5}
var set1 = {1, 2, 3, 4, 5};
var set2 = {4, 5, 6, 7, 8};
// 합집합
var union = set1.union(set2);
print(union); // {1, 2, 3, 4, 5, 6, 7, 8}
// 교집합
var intersection = set1.intersection(set2);
print(intersection); // {4, 5}
// 차집합
var difference = set1.difference(set2);
print(difference); // {1, 2, 3}
// 요소 추가
set1.add(6);
print(set1); // {1, 2, 3, 4, 5, 6}
// 여러 요소 추가
set1.addAll({7, 8, 9});
print(set1); // {1, 2, 3, 4, 5, 6, 7, 8, 9}
// 요소 삭제
set1.remove(9);
print(set1); // {1, 2, 3, 4, 5, 6, 7, 8}
// 존재 여부 확인
print(set1.contains(5)); // true
print(set1.contains(10)); // false
// 부분집합 확인
print({1, 2}.isSubsetOf(set1)); // true
print(set1.isSupersetOf({1, 2})); // true
var numbers = {1, 2, 3, 4, 5};
// 매핑 (집합으로 변환)
var doubled = numbers.map((n) => n * 2).toSet();
print(doubled); // {2, 4, 6, 8, 10}
// 필터링
var evenNumbers = numbers.where((n) => n.isEven).toSet();
print(evenNumbers); // {2, 4}
// 리스트로 변환
var numbersList = numbers.toList();
print(numbersList); // [1, 2, 3, 4, 5] (순서는 보장되지 않음)

Map은 키-값 쌍의 컬렉션으로, 키를 사용하여 값을 검색할 수 있습니다.

// 리터럴을 사용한 생성
var person = {
'name': '홍길동',
'age': 30,
'isStudent': false
};
// 타입을 지정한 맵
Map<String, int> scores = {
'수학': 90,
'영어': 85,
'과학': 95
};
// 빈 맵 생성
var emptyMap = <String, dynamic>{};
// 생성자를 이용한 맵 생성
var map1 = Map<String, int>();
var map2 = Map.from({'a': 1, 'b': 2});
var map3 = Map.of({'x': 10, 'y': 20});
var person = {
'name': '홍길동',
'age': 30,
'isStudent': false
};
// 값 접근
print(person['name']); // 홍길동
// 키 확인
print(person.containsKey('age')); // true
print(person.containsKey('email')); // false
// 값 확인
print(person.containsValue(30)); // true
print(person.containsValue('김철수')); // false
// 키 목록과 값 목록
print(person.keys.toList()); // [name, age, isStudent]
print(person.values.toList()); // [홍길동, 30, false]
// 항목 추가/업데이트
person['email'] = 'hong@example.com';
person['age'] = 31;
print(person); // {name: 홍길동, age: 31, isStudent: false, email: hong@example.com}
// 항목 삭제
person.remove('isStudent');
print(person); // {name: 홍길동, age: 31, email: hong@example.com}
// 여러 항목 추가
person.addAll({
'address': '서울시',
'phone': '010-1234-5678'
});
print(person);
// {name: 홍길동, age: 31, email: hong@example.com, address: 서울시, phone: 010-1234-5678}
// 조건부 추가
person.putIfAbsent('gender', () => '남성');
print(person);
// {name: 홍길동, age: 31, email: hong@example.com, address: 서울시, phone: 010-1234-5678, gender: 남성}
// 이미 있는 키에는 추가되지 않음
person.putIfAbsent('gender', () => '여성');
print(person['gender']); // 남성 (변경되지 않음)
var scores = {
'수학': 90,
'영어': 85,
'과학': 95,
'국어': 80
};
// 매핑 변환
var scaledScores = scores.map((k, v) => MapEntry(k, v * 1.1));
print(scaledScores);
// {수학: 99.0, 영어: 93.5, 과학: 104.5, 국어: 88.0}
// 필터링
var highScores = scores.entries
.where((entry) => entry.value >= 90)
.fold(<String, int>{}, (map, entry) {
map[entry.key] = entry.value;
return map;
});
print(highScores); // {수학: 90, 과학: 95}
// forEach로 처리
scores.forEach((key, value) {
print('$key: $value');
});
// 수학: 90
// 영어: 85
// 과학: 95
// 국어: 80

Dart는 컬렉션과 다른 이터러블(iterable) 객체를 처리하기 위한 다양한 반복문을 제공합니다.

// 기본 for 루프
for (int i = 0; i < 5; i++) {
print(i); // 0, 1, 2, 3, 4
}
// 복잡한 조건과 증가식
for (int i = 10; i > 0; i -= 2) {
print(i); // 10, 8, 6, 4, 2
}
var fruits = ['사과', '바나나', '오렌지'];
// 컬렉션 항목 반복
for (var fruit in fruits) {
print(fruit); // 사과, 바나나, 오렌지
}
// 인덱스가 필요한 경우
for (int i = 0; i < fruits.length; i++) {
print('${i + 1}번째 과일: ${fruits[i]}');
}
// 1번째 과일: 사과
// 2번째 과일: 바나나
// 3번째 과일: 오렌지
// while 반복문
int count = 0;
while (count < 5) {
print(count);
count++;
}
// 0, 1, 2, 3, 4
// do-while 반복문 (최소 한 번은 실행됨)
int num = 5;
do {
print(num);
num--;
} while (num > 0);
// 5, 4, 3, 2, 1
var numbers = [1, 2, 3, 4, 5];
// 리스트의 forEach
numbers.forEach((number) {
print(number * 2); // 2, 4, 6, 8, 10
});
// 맵의 forEach
var scores = {'수학': 90, '영어': 85, '과학': 95};
scores.forEach((subject, score) {
print('$subject: $score점');
// 수학: 90점
// 영어: 85점
// 과학: 95점
});
// break로 반복 중단
for (int i = 0; i < 10; i++) {
if (i == 5) break;
print(i); // 0, 1, 2, 3, 4
}
// continue로 현재 반복 건너뛰기
for (int i = 0; i < 5; i++) {
if (i == 2) continue;
print(i); // 0, 1, 3, 4
}
// 레이블을 사용한 중첩 반복문 제어
outerLoop: for (int i = 0; i < 3; i++) {
for (int j = 0; j < 3; j++) {
if (i == 1 && j == 1) {
break outerLoop; // 바깥 반복문까지 중단
}
print('$i, $j');
// 0, 0
// 0, 1
// 0, 2
// 1, 0
}
}

Dart에서는 컬렉션 리터럴 내에 for 루프와 if 조건을 삽입할 수 있습니다.

// 컬렉션 for
var numbers = [1, 2, 3];
var doubled = [
0,
for (var n in numbers) n * 2,
4
];
print(doubled); // [0, 2, 4, 6, 4]
// 컬렉션 if
bool includeZ = true;
var letters = ['a', 'b', if (includeZ) 'z'];
print(letters); // [a, b, z]
// 복합 사용
var items = [
'home',
if (userLoggedIn) 'profile',
for (var item in defaultItems) item,
if (isAdmin) 'admin'
];

스프레드 연산자(...)를 사용하여 컬렉션을 다른 컬렉션에 삽입할 수 있습니다.

var list1 = [1, 2, 3];
var list2 = [0, ...list1, 4, 5];
print(list2); // [0, 1, 2, 3, 4, 5]
// null 조건부 스프레드 연산자 (...?)
List<int>? nullableList;
var combined = [0, ...?nullableList, 1];
print(combined); // [0, 1]
// 맵에 사용
var map1 = {'a': 1, 'b': 2};
var map2 = {'c': 3, ...map1};
print(map2); // {c: 3, a: 1, b: 2}
// 집합에 사용
var set1 = {1, 2, 3};
var set2 = {0, ...set1, 4};
print(set2); // {0, 1, 2, 3, 4}

제너레이터 함수는 값의 시퀀스를 생성할 수 있습니다. Dart는 두 가지 유형의 제너레이터 함수를 지원합니다.

sync*는 Iterable 객체를 생성합니다:

Iterable<int> getNumbers(int n) sync* {
for (int i = 0; i < n; i++) {
yield i;
}
}
void main() {
for (var num in getNumbers(5)) {
print(num); // 0, 1, 2, 3, 4
}
}

async*는 Stream 객체를 생성합니다:

Stream<int> countStream(int n) async* {
for (int i = 1; i <= n; i++) {
await Future.delayed(Duration(seconds: 1));
yield i;
}
}
void main() async {
await for (var num in countStream(5)) {
print(num); // 1초마다 1, 2, 3, 4, 5 출력
}
}
var numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
// 연속 변환
var result = numbers
.where((n) => n % 2 == 0) // 짝수만 선택
.map((n) => n * n) // 제곱
.takeWhile((n) => n <= 36) // 36 이하인 동안만
.fold(0, (sum, n) => sum + n); // 합계 계산
print(result); // 4 + 16 + 36 = 56
// 그룹화
var fruits = ['사과', '바나나', '체리', '블루베리', '아보카도'];
var byFirstLetter = fruits.fold<Map<String, List<String>>>(
{},
(map, fruit) {
var firstLetter = fruit[0];
map[firstLetter] = (map[firstLetter] ?? [])..add(fruit);
return map;
},
);
print(byFirstLetter);
// {사: [사과], 바: [바나나], 체: [체리], 블: [블루베리], 아: [아보카도]}

Dart 2.3 이상의 컬렉션 관련 기능

Section titled “Dart 2.3 이상의 컬렉션 관련 기능”

연쇄 표기법(..)을 사용하면 동일한 객체에서 연속적인 작업을 수행할 수 있습니다:

var list = [1, 2, 3]
..add(4)
..addAll([5, 6])
..remove(2)
..sort();
print(list); // [1, 3, 4, 5, 6]
// 중첩 객체에도 사용 가능
var map = {'user': {'name': '홍길동', 'age': 30}}
..['user']['email'] = 'hong@example.com'
..['user']['age'] = 31
..['active'] = true;
print(map);
// {user: {name: 홍길동, age: 31, email: hong@example.com}, active: true}

null-aware 연산자를 사용하면 null 처리가 더 간결해집니다:

// ?. 연산자 (null인 경우 실행하지 않음)
List<String>? nullableList;
nullableList?.add('항목'); // nullableList가 null이면 아무 일도 일어나지 않음
// ?? 연산자 (null인 경우 기본값 제공)
var list = nullableList ?? [];
list.add('항목');
print(list); // [항목]
// ??= 연산자 (null인 경우에만 값 할당)
Map<String, int>? scoresMap;
scoresMap ??= {};
scoresMap['수학'] = 90;
print(scoresMap); // {수학: 90}
class Student {
final String name;
final int age;
final Map<String, int> scores;
Student(this.name, this.age, this.scores);
}
void main() {
final students = [
Student('홍길동', 20, {'수학': 90, '영어': 85, '과학': 95}),
Student('김철수', 22, {'수학': 75, '영어': 90, '과학': 85}),
Student('이영희', 21, {'수학': 85, '영어': 92, '과학': 88}),
Student('박민수', 23, {'수학': 95, '영어': 80, '과학': 92}),
];
// 각 학생의 평균 점수 계산
final averageScores = students.map((student) {
final total = student.scores.values.fold<int>(0, (sum, score) => sum + score);
final average = total / student.scores.length;
return {'name': student.name, 'average': average};
}).toList();
print('평균 점수:');
for (var item in averageScores) {
print('${item['name']}: ${item['average']}');
}
// 평균 90점 이상인 학생
final highPerformers = students.where((student) {
final total = student.scores.values.fold<int>(0, (sum, score) => sum + score);
final average = total / student.scores.length;
return average >= 90;
}).map((student) => student.name).toList();
print('\n우수 학생: $highPerformers');
// 과목별 최고 점수 및 학생
final subjects = {'수학', '영어', '과학'};
final topScoresBySubject = subjects.fold<Map<String, Map<String, dynamic>>>(
{},
(map, subject) {
var topStudent = students.reduce((a, b) =>
(a.scores[subject] ?? 0) > (b.scores[subject] ?? 0) ? a : b);
map[subject] = {
'student': topStudent.name,
'score': topStudent.scores[subject]
};
return map;
},
);
print('\n과목별 최고 점수:');
topScoresBySubject.forEach((subject, data) {
print('$subject: ${data['student']} (${data['score']}점)');
});
}
class Product {
final String id;
final String name;
final double price;
final List<String> categories;
final bool inStock;
Product(this.id, this.name, this.price, this.categories, this.inStock);
}
void main() {
final products = [
Product('p1', '스마트폰', 850000, ['전자제품', '통신기기'], true),
Product('p2', '노트북', 1200000, ['전자제품', '컴퓨터'], false),
Product('p3', '헤드폰', 120000, ['전자제품', '오디오'], true),
Product('p4', '키보드', 98000, ['전자제품', '컴퓨터', '주변기기'], true),
Product('p5', '마우스', 45000, ['전자제품', '컴퓨터', '주변기기'], true),
Product('p6', '모니터', 550000, ['전자제품', '컴퓨터', '주변기기'], false),
];
// 1. 재고가 있는 제품만 필터링
final inStockProducts = products.where((p) => p.inStock).toList();
// 2. 컴퓨터 관련 제품만 필터링
final computerProducts = products
.where((p) => p.categories.contains('컴퓨터'))
.toList();
// 3. 가격 기준 정렬 (오름차순)
final sortedByPrice = List<Product>.from(products)
..sort((a, b) => a.price.compareTo(b.price));
// 4. 복합 필터: 재고 있는 주변기기 중 가격이 100,000원 미만인 제품
final affordablePeripherals = products
.where((p) => p.inStock &&
p.categories.contains('주변기기') &&
p.price < 100000)
.toList();
// 5. 카테고리별 제품 그룹화
final productsByCategory = <String, List<Product>>{};
for (var product in products) {
for (var category in product.categories) {
productsByCategory[category] ??= [];
productsByCategory[category]!.add(product);
}
}
// 결과 출력
print('재고 있는 제품: ${inStockProducts.map((p) => p.name).toList()}');
print('컴퓨터 관련 제품: ${computerProducts.map((p) => p.name).toList()}');
print('저렴한 주변기기: ${affordablePeripherals.map((p) => p.name).toList()}');
print('\n카테고리별 제품:');
productsByCategory.forEach((category, categoryProducts) {
print('$category: ${categoryProducts.map((p) => p.name).toList()}');
});
}

Dart의 컬렉션과 반복문은 강력하고 유연한 도구를 제공하여 데이터를 효율적으로 처리할 수 있게 해줍니다. List, Set, Map과 같은 기본 컬렉션 타입은 다양한 메서드를 제공하며, 컬렉션 for, 스프레드 연산자, 제너레이터 함수 등의 기능은 복잡한 데이터 처리 작업을 간결하게 표현할 수 있게 해줍니다.

다음 장에서는 Dart의 예외 처리에 대해 알아보겠습니다.