Dart 튜토리얼 4편: 컬렉션 완전 정복
요약
핵심 요지
문서가 설명하는 범위
컬렉션리터럴과 기본 사용법을 다룬다.Iterable의 정의와 순차 접근 규칙을 설명한다.record문법과 필드 접근 규칙을 소개한다.generics와type argument의 역할을 정리한다.
읽는 시간: 14분 | 난이도: 초급
참고 자료
- Collections - List/Set/Map 기본
- Iterable collections - Iterable 처리 패턴
- Records - 레코드 구조
- Generics - 제네릭 타입
문제 상황
JavaScript와 Python 같은 동적 언어에서는 배열이나 리스트에 어떤 타입이든 넣을 수 있습니다.
실행해봐야만 잘못된 타입이 들어갔는지 알 수 있어서 디버깅이 어렵습니다.
기존 방식의 한계
// JavaScript 예시const numbers = [1, 2, 3];numbers.push("hello"); // 타입 체크 없음numbers.push(true); // 타입 체크 없음
// 나중에 사용할 때 오류 발생const sum = numbers.reduce((a, b) => a + b); // NaN 출력문제는 다음과 같습니다.
컬렉션에 어떤 타입이든 넣을 수 있어서 일관성이 없다.- 실행 전까지 잘못된 타입을 발견할 수 없다.
- 순차 접근 방식과 인덱스 접근 방식의 차이가 명확하지 않다.
해결 방법
Dart는 컬렉션 타입을 제네릭으로 선언하고, Iterable과 record로 구조를 명확히 구분합니다.
단계 1: 컬렉션 리터럴과 제네릭 타입
Why
NOTE컬렉션을 쓰기 시작하면 가장 먼저 부딪히는 문제는 “아무 값이나 들어가서, 나중에 깨지는 것”입니다.
그래서 컬렉션에 들어갈 값의 타입을 처음부터 고정하는 편이 안전합니다.var numbers = <int>[1, 2, 3];// numbers.add('hello'); // 컴파일 오류
What
NOTE
generics는List/Set/Map같은 타입에타입 인자를 붙여 요소 타입을 고정하는 기능입니다.
How
TIPDart는
List,Set,Map리터럴을 기본으로 제공하며,generics의type argument로 요소 타입을 지정합니다.var names = <String>['Seth', 'Kathy', 'Lars'];var uniqueNames = <String>{'Seth', 'Kathy', 'Lars'};var pages = <String, String>{'index.html': 'Homepage','robots.txt': 'Hints for web robots',};타입 안전성 확보
var numbers = <int>[1, 2, 3];// numbers.add('hello'); // 컴파일 오류: String을 int 리스트에 추가 불가numbers.add(4); // OK
Watch out
WARNING타입을 고정했다면, “잠깐이라도 다른 타입을 끼워 넣기”는 허용되지 않습니다.
한 번 잘못 들어가면 나중에 어디서 깨지는지 찾기 어려워지기 때문입니다.
결론: 타입 인자를 지정하면 정적 분석이 잘못된 요소 타입을 컴파일 단계에서 오류로 보고합니다.
단계 2: Iterable의 순차 접근
Why
NOTE
List처럼 인덱스 접근을 계속 쓰다 보면,Iterable에서도 같은 방식이 될 거라고 착각하기 쉽습니다.
하지만Iterable은 “순차 접근”을 핵심 규칙으로 둡니다.Iterable<int> iterable = [1, 2, 3];// iterable[1]; // 컴파일 오류
What
NOTE
Iterable은 요소를 순서대로 꺼내는 인터페이스이며, 인덱스 연산([])이 기본 규칙이 아닙니다.
How
TIP
Iterable은 요소를 순차적으로 접근하는컬렉션인터페이스입니다.
List와Set은Iterable이지만,Iterable자체는[]연산자를 지원하지 않습니다.Iterable<int> iterable = [1, 2, 3];// iterable[1]; // 컴파일 오류: Iterable에는 [] 연산자 없음int value = iterable.elementAt(1); // OK: elementAt() 메서드 사용
Watch out
WARNING인덱스가 꼭 필요하다면
List를 쓰는 편이 더 자연스럽습니다.
Iterable에 억지로 인덱스 접근을 붙이면, 코드의 의도가 흐려질 수 있습니다.순차 접근 패턴
var iterable = [1, 2, 3];// for-in 루프로 순차 접근for (var element in iterable) {print(element);}// iterator로 직접 제어var iterator = iterable.iterator;while (iterator.moveNext()) {print(iterator.current);}
결론: Iterable 전용 접근 메서드를 사용해 순차 접근 규칙을 지키고, 컴파일러가 잘못된 접근을 방지합니다.
단계 3: record로 구조적 데이터 표현
Why
NOTE서로 다른 타입의 값 몇 개를 “한 번에 묶어서” 넘기고 싶을 때가 있습니다.
이때 매번 클래스를 만들지 않아도 되는 방식이 있으면, 코드가 빨라집니다.// (이름, 나이)처럼 두 값을 같이 돌려주고 싶다.(String, int) userInfo() => ('Alice', 25);
What
NOTE
record는 여러 값을 한 번에 묶는 불변 구조이며, 위치 필드와 이름 있는 필드를 함께 가질 수 있습니다.
How
TIP
record는immutable9이며fixed length10 구조입니다.
heterogeneous11 필드를 가질 수 있어서 서로 다른 타입을 함께 담을 수 있습니다.
Dart 버전 3.0 이상이 필요합니다.var record = ('first', a: 2, b: true, 'last');print(record.$1); // first (위치 필드)print(record.a); // 2 (이름 있는 필드)print(record.b); // true (이름 있는 필드)print(record.$2); // last (위치 필드)
Watch out
WARNING위치 필드(
$1,$2)는 순서에 의존합니다.
필드가 늘어날 가능성이 있거나 의미가 중요한 값이라면, 이름 있는 필드(a, b처럼)를 쓰는 편이 더 안전합니다.함수에서 여러 값 반환
(String, int) userInfo() {return ('Alice', 25);}var info = userInfo();print('Name: ${info.$1}, Age: ${info.$2}');
결론: 클래스 선언 없이 구조적 데이터를 표현하고 접근할 수 있습니다.
타입 안전성을 유지하면서 여러 값을 반환할 수 있습니다.
Footnotes
-
collection(컬렉션): 여러 값을 담는 데이터 구조다. ↩
-
List(리스트): 순서가 있는 컬렉션이다. ↩
-
Set(셋): 중복을 허용하지 않는 컬렉션이다. ↩
-
Map(맵): 키와 값의 쌍을 저장하는 컬렉션이다. ↩
-
Iterable(이터러블): 요소를 순서대로 접근할 수 있는 컬렉션 인터페이스다. ↩
-
record(레코드): 이름 없는 필드 묶음을 표현하는 불변 구조다. ↩
-
generics(제네릭): 타입을 매개변수로 받아 재사용 가능한 타입을 만드는 기능이다. ↩
-
type argument(타입 인자): 제네릭에 전달하는 구체적인 타입이다. ↩
-
immutable(불변): 값이 생성된 뒤 변경되지 않는 성질이다. ↩
-
fixed length(고정 길이): 필드 개수가 고정되어 있다는 뜻이다. ↩
-
heterogeneous(이종): 서로 다른 타입의 필드를 함께 가질 수 있다는 뜻이다. ↩
공유
이 글이 도움이 되었다면 다른 사람과 공유해주세요!