Dart Records & 패턴 매칭
Dart 3.0 — 다중 값 반환, 구조 분해, switch 패턴
Dart 3.0에서 도입된 Records는 클래스를 정의하지 않고도 여러 값을 괄호로 그룹화하는 불변 타입입니다. 위치 기반($1, $2)이나 명명된 필드(.name, .age)로 접근하며, 구조 분해로 한 줄에 여러 변수를 추출할 수 있습니다. 패턴 매칭과 결합하면 switch/if-case에서 타입과 조건을 동시에 검사하여 코드를 크게 간소화할 수 있습니다.
Records란?
Dart 3.0에서 도입된 Records는 클래스 정의 없이 여러 값을 하나의 객체로 그룹화하는 불변(immutable) 컬렉션 타입입니다. 구조적 타이핑(structural typing)을 사용하며, 같은 필드 구조를 가지면 같은 타입으로 취급됩니다.
1. 위치 기반 레코드 (Positional Records)
괄호 안에 값을 나열하면 위치 기반 레코드가 됩니다. $1, $2로 접근합니다.
var person = ('홍길동', 30);
print(person); // (홍길동, 30)
print(person.$1); // 홍길동
print(person.$2); // 30
2. 명명된 레코드 (Named Records)
필드에 이름을 부여하면 가독성이 크게 향상됩니다. 위치 기반과 혼합 사용도 가능합니다.
// 명명된 필드
var person = (name: '홍길동', age: 30);
print(person.name); // 홍길동
print(person.age); // 30
// 위치 + 명명 혼합
var data = ('홍길동', age: 30, active: true);
print(data.$1); // 홍길동
print(data.age); // 30
print(data.active); // true
3. 타입 어노테이션
레코드 변수에 명시적 타입을 지정할 수 있습니다.
// 위치 기반 타입
(String, int) person = ('홍길동', 30);
// 명명된 필드 타입
({String name, int age}) person = (name: '홍길동', age: 30);
// 혼합 타입
(String, {int age, bool active}) data = ('홍길동', age: 30, active: true);
4. 함수에서 다중 값 반환
Records의 가장 실용적인 사용 사례입니다. 별도 클래스 없이 함수에서 여러 값을 반환할 수 있습니다.
(String, int) getUserInfo() {
return ('홍길동', 30);
}
void main() {
var (name, age) = getUserInfo();
print('이름: $name, 나이: $age');
// 이름: 홍길동, 나이: 30
}
5. 구조 분해 (Destructuring)
레코드의 각 필드를 개별 변수로 추출합니다. 명명된 레코드는 축약 구문도 지원합니다.
var person = (name: '홍길동', age: 30);
// 전체 구조 분해 (변수명 변경)
var (name: userName, age: userAge) = person;
print('이름: $userName, 나이: $userAge');
// 축약 구조 분해 (필드명 그대로)
var (:name, :age) = person;
print('이름: $name, 나이: $age');
6. 동등성 비교 (Equality)
Records는 값 기반 동등성을 지원합니다. 같은 구조와 값을 가지면 동일한 것으로 판단합니다.
var person1 = (name: '홍길동', age: 30);
var person2 = (name: '홍길동', age: 30);
var person3 = (name: '김철수', age: 25);
print(person1 == person2); // true
print(person1 == person3); // false
var p1 = ('홍길동', 30);
var p2 = ('홍길동', 30);
print(p1 == p2); // true
7. switch 패턴 매칭
Dart 3.0의 패턴 매칭은 switch문에서 타입 검사와 조건을 동시에 수행합니다. when 가드로 추가 조건도 걸 수 있습니다.
void describe(Object obj) {
switch (obj) {
case (String name, int age):
print('이름: $name, 나이: $age');
default:
print('기타 객체: $obj');
}
}
// when 가드 사용
void process(dynamic value) {
switch (value) {
case (String n, int a) when a >= 18:
print('성인: $n, $a살');
case (String n, int a):
print('미성년자: $n, $a살');
default:
print('기타 값: $value');
}
}
process(('홍길동', 30)); // 성인: 홍길동, 30살
process(('김영희', 15)); // 미성년자: 김영희, 15살
8. if-case 패턴 매칭
if문 안에서 패턴 매칭을 수행합니다. JSON 파싱이나 타입 검증에 특히 유용합니다.
void processValue(Object value) {
if (value case (String name, int age)) {
print('이름: $name, 나이: $age');
} else if (value case String s when s.length > 5) {
print('긴 문자열: $s');
} else {
print('처리할 수 없는 값: $value');
}
}
processValue(('홍길동', 30));
// 이름: 홍길동, 나이: 30
9. 중첩 패턴 매칭
리스트와 레코드를 조합한 복잡한 구조도 패턴으로 분해할 수 있습니다.
var data = [('홍길동', 30), ('김철수', 25)];
if (data case [(String s, int i), var rest]) {
print('첫 번째 사람: $s, $i살');
// 첫 번째 사람: 홍길동, 30살
print('나머지: $rest');
// 나머지: (김철수, 25)
}
10. 실전 예제: 통계 계산
Records로 최솟값, 최댓값, 평균을 한 번에 반환하는 함수입니다.
(double min, double max, double average) calculateStats(
List<double> values) {
if (values.isEmpty) return (0, 0, 0);
double sum = 0;
double min = values[0];
double max = values[0];
for (var value in values) {
sum += value;
if (value < min) min = value;
if (value > max) max = value;
}
return (min, max, sum / values.length);
}
void main() {
var numbers = [10.5, 25.3, 17.2, 8.7, 30.1];
var (min, max, avg) = calculateStats(numbers);
print('최소값: $min'); // 최소값: 8.7
print('최대값: $max'); // 최대값: 30.1
print('평균값: $avg'); // 평균값: 18.36
}
11. 실전 예제: API 응답 처리
Records와 패턴 매칭을 결합하면 API 응답을 안전하게 처리할 수 있습니다.
(bool success, {String? data, String? error})
fetchUserData(String userId) {
if (userId == 'user123') {
return (true,
data: '{"name": "홍길동"}',
error: null);
} else {
return (false,
data: null,
error: '사용자를 찾을 수 없습니다.');
}
}
void main() {
var result = fetchUserData('user123');
if (result.$1) {
print('데이터: ${result.data}');
}
var fail = fetchUserData('unknown');
if (!fail.$1) {
print('오류: ${fail.error}');
}
}
핵심 정리
- Records = 클래스 없는 불변 다중 값 컨테이너 (Dart 3.0+)
- 위치 기반($1, $2) vs 명명 기반(.name, .age) 선택 가능
- 구조 분해로 한 줄에 여러 변수 추출
- 값 기반 동등성 — 같은 구조+값이면 == true
- 패턴 매칭(switch/if-case)과 결합하면 타입+조건 동시 검사 가능
구현 순서
위치 기반 레코드 — var result = ('성공', 200); → result.$1, result.$2로 접근
명명된 레코드 — var user = (name: '홍길동', age: 30); → user.name, user.age로 가독성 향상
구조 분해 — var (:name, :age) = user;로 한 줄에 여러 변수 추출
switch 패턴 매칭 — case String s when s.isNotEmpty: 타입과 조건을 동시에 검사, case _:로 와일드카드
if-case — if (json case {'name': String name}) { } 형태로 JSON 파싱과 타입 검증을 한 번에
장점
- ✓ 클래스 없이 가볍게 여러 값을 그룹화하여 반환 가능
- ✓ 패턴 매칭으로 if-else 체인을 깔끔한 switch로 대체
단점
- ✗ Dart 3.0 이상에서만 사용 가능
- ✗ 복잡한 중첩 패턴은 오히려 가독성을 해칠 수 있음