패턴 매칭과 레코드
Dart 3.0에서 대대적으로 추가된 기능입니다. 레코드(Record)로 여러 값을 쉽게 묶어 반환하고, 패턴 매칭으로 복잡한 구조 분해를 선언적으로 처리합니다. 현대 Dart 코드에서 가장 많이 쓰이는 신문법입니다.
레코드 (Record)
여러 값을 하나로 묶는 익명 타입입니다. 클래스를 만들 필요 없이 함수에서 여러 값을 반환할 때 특히 유용합니다.
// 위치 필드
(String, int) getUser() {
return ('Alice', 30);
}
// 명명된 필드
({String name, int age}) getUserNamed() {
return (name: 'Alice', age: 30);
}
void main() {
// 위치 필드 접근
final user = getUser();
print(user.$1); // Alice
print(user.$2); // 30
// 구조 분해
final (name, age) = getUser();
print('$name is $age'); // Alice is 30
// 명명된 필드 접근
final userNamed = getUserNamed();
print(userNamed.name); // Alice
print(userNamed.age); // 30
// 혼합 사용
(int, {String label}) mixed = (42, label: 'answer');
print(mixed.$1); // 42
print(mixed.label); // answer
}
레코드 활용 예시
// 파싱 결과 반환
(bool success, String? data, String? error) parseResponse(String raw) {
if (raw.isEmpty) return (false, null, 'empty response');
return (true, raw, null);
}
// HTTP 응답 처리
final (ok, data, err) = parseResponse(responseBody);
if (!ok) {
print('Error: $err');
return;
}
print('Data: $data');
// Map.entries 처리
final scores = {'Alice': 95, 'Bob': 87, 'Charlie': 92};
final topScorer = scores.entries
.map((e) => (e.key, e.value))
.reduce((a, b) => a.$2 > b.$2 ? a : b);
print('${topScorer.$1}: ${topScorer.$2}'); // Alice: 95
패턴 매칭 — 변수 분해
// List 패턴
final [first, second, ...rest] = [1, 2, 3, 4, 5];
print(first); // 1
print(second); // 2
print(rest); // [3, 4, 5]
// Map 패턴
final {'name': String name, 'age': int age} = {'name': 'Alice', 'age': 30};
print('$name: $age');
// 객체 패턴
class Point {
final int x, y;
const Point(this.x, this.y);
}
final point = Point(3, 4);
final Point(:x, :y) = point; // 명명된 필드 분해
print('$x, $y'); // 3, 4
switch 표현식
Dart 3.0의 switch는 문(statement)이 아닌 표현식(expression)으로도 사용됩니다.
// switch 표현식
String describe(Object value) => switch (value) {
int n when n < 0 => 'negative',
int n when n == 0 => 'zero',
int n => 'positive $n',
String s => 'string: $s',
_ => 'unknown',
};
print(describe(-5)); // negative
print(describe(42)); // positive 42
print(describe('hi')); // string: hi
// 레코드와 switch 조합
String classifyPoint((int x, int y) point) => switch (point) {
(0, 0) => 'origin',
(_, 0) => 'on x-axis',
(0, _) => 'on y-axis',
(final x, final y) when x == y => 'diagonal',
_ => 'general',
};
print(classifyPoint((0, 0))); // origin
print(classifyPoint((3, 0))); // on x-axis
print(classifyPoint((5, 5))); // diagonal
sealed class
sealed 클래스는 같은 파일 내에서만 하위 클래스를 만들 수 있는 봉인된 클래스입니다. switch에서 완전성 검사(exhaustive check)가 적용됩니다.
// sealed: 이 파일 외부에서 상속 불가
sealed class Result<T> {}
class Success<T> extends Result<T> {
final T value;
Success(this.value);
}
class Failure<T> extends Result<T> {
final String error;
Failure(this.error);
}
class Loading<T> extends Result<T> {}
// switch가 모든 케이스를 강제로 커버
String describeResult(Result<String> result) => switch (result) {
Success(:final value) => 'Success: $value',
Failure(:final error) => 'Error: $error',
Loading() => 'Loading...',
// 빠진 케이스가 있으면 컴파일 오류 발생
};
void main() {
final r1 = Success('Hello');
final r2 = Failure<String>('Not found');
final r3 = Loading<String>();
print(describeResult(r1)); // Success: Hello
print(describeResult(r2)); // Error: Not found
print(describeResult(r3)); // Loading...
}
if-case 패턴
조건문 안에서 패턴 매칭을 사용합니다.
dynamic json = {'type': 'user', 'name': 'Alice', 'age': 30};
// if-case로 타입과 구조 동시 체크
if (json case {'type': 'user', 'name': String name, 'age': int age}) {
print('User: $name, Age: $age');
} else {
print('Unknown structure');
}
// 리스트 패턴
List<Object> items = [1, 'hello', 3.14];
if (items case [int n, String s, _]) {
print('First: $n, Second: $s');
}
정리
핵심 요점만 정리합니다.
- 레코드는 가벼운 다중 반환 값 타입입니다. 클래스 없이
(String, int)형태로 사용합니다. - 패턴 매칭으로 List, Map, 객체의 구조를 분해해서 변수에 바인딩합니다.
- switch 표현식은 exhaustive 체크를 제공해 누락된 케이스를 컴파일 타임에 잡아냅니다.
sealed class는 하위 타입을 제한하고 switch와 함께 쓰면 타입 안전한 상태 머신을 구현할 수 있습니다.if-case로 특정 구조에 대한 조건 체크와 분해를 한 번에 처리합니다.
PART 02가 끝났습니다. 다음 PART에서는 프로젝트 구조와 패키지 관리를 다룹니다.