[Capstone-Start] Dart ํ์ต โ Records, Patterns, Switch Expression
๐ ์ผ์: 2026. 05. 26
๐ฌ ๋ด์ฉ: Dart 3์ ์ ๊ธฐ๋ฅ โ Records, Pattern Matching, Switch Expression์ด ์ค์ ์ฝ๋์ ์ด๋ป๊ฒ ๋ค์ด๊ฐ๋์ง ์ ๋ฆฌ
์บก์คํค ์์ฐ ํ๊ฒฝ์ด ์ด์ (5/25) ์์ ํ๋ ํ, ์ค๋์ ๊ทธ๋์ ์ง ์ฝ๋๋ฅผ ํ ๋ฒ ํ์ผ๋ฉฐ ์ ๋ฆฌํ๋ ์๊ฐ์ ๊ฐ์ก์ต๋๋ค. ๊ทธ๋ฌ๋ฉด์ ๊นจ๋ฌ์ ๊ฒ, Dart 3์์ ์๋ก ๋ค์ด์จ records / patterns / switch expression์ด ์ฝ๋ ๊ณณ๊ณณ์ ์์ฐ์ค๋ฝ๊ฒ ๋ฐํ ์๋ค๋ ์ ์
๋๋ค. Stage 8์์ (_, _) wildcard๋ฅผ ์ฐ๊ฑฐ๋, Stage 2์์ Result<T> switch matching์ ๊น๊ฑฐ๋, fl_chart ๋ฐ์ดํฐ์ (double x, double y) records๋ฅผ ์ฐ๋ ๋ฑ์ ํ์ ์ด ๋จ์ ์์์ต๋๋ค.
์ฒ์์ ์ด ๊ธฐ๋ฅ๋ค์ ์ธ ๋๋ โKotlin ๋น์ทํ ๊ฒโ์ด๋ผ๋ ์ง๊ด์ผ๋ก ๋ฐ์๋๋ฐ, ์ค๋ ๋ค์ ์ ๋ฆฌํ๋ฉด์ ์ ์ ํ์ต ๋ ธํธ๋ก ๋จ๊ฒจ ๋ก๋๋ค. ๊ฒ์ํด์ ๋ค์ด์ค์ ๋ถ๊ป ๋์์ด ๋ ์ ์๋๋ก Dart 2 ์์ ์ฝ๋์ Dart 3 ์ฝ๋์ before/after๋ฅผ ๊ฐ์ด ๋ณด์ฌ๋๋ฆฌ๋ ํํ๋ก ์ ๋ฆฌํฉ๋๋ค.
1. Records โ ์ฆ์์์ ๋ง๋๋ tuple
Record๋ ์ฌ๋ฌ ๊ฐ์ ํ๋์ immutable ๋ฌถ์์ผ๋ก ๋ง๋๋ ๊ฒฝ๋ ํ์ ์ ๋๋ค. Python์ tuple, Kotlin์ Pair/Triple, TypeScript์ anonymous object์ ๋น์ทํ ๊ฐ๋ ์ ๋๋ค.
Positional Records
1
2
3
4
(int, String) pair = (1, 'apple');
print(pair.$1); // 1
print(pair.$2); // apple
ํ๋๋ช
์ด ์๋ record๋ $1, $2 ์์ผ๋ก ์์น ์ธ๋ฑ์ค๋ก ์ ๊ทผํฉ๋๋ค. ํจ์๊ฐ ์ฌ๋ฌ ๊ฐ์ ๋ฐํํด์ผ ํ ๋ ๋ณ๋ ํด๋์ค๋ฅผ ๋ง๋ค์ง ์๊ณ ์ฆ์์์ ์ธ ์ ์์ด ํธ๋ฆฌํฉ๋๋ค.
1
2
3
(int, int) divmod(int a, int b) => (a ~/ b, a % b);
final (q, r) = divmod(17, 5); // q = 3, r = 2
์ข๋ณ์ (q, r) ํจํด์ ์ฐ๋ฉด ํ ์ค์ destructuring์ด ๋ฉ๋๋ค.
Named Records โ ({ ... })
1
2
3
4
({int x, int y}) point = (x: 10, y: 20);
print(point.x); // 10
print(point.y); // 20
ํ๋๋ช
์ด ์๋ record๋ ์ ํ๊ธฐ๋ก ์ ๊ทผํ ์ ์๊ณ , ์๊ทธ๋์ฒ๊ฐ ๋ ๋ช
ํํด์ง๋๋ค. ์บก์คํค์ fl_chart ๋ํผ์์๋ ์ขํ๋ฅผ ({double x, double y}) ํํ์ record๋ก ๋ฐ๋๋ก ํ์ต๋๋ค.
1
2
3
4
5
6
7
8
9
10
11
class AppLineChart extends StatelessWidget {
const AppLineChart({super.key, required this.points, ...});
final List<({double x, double y})> points;
}
// ํธ์ถ
AppLineChart(points: [
(x: 0, y: 70),
(x: 1, y: 71),
(x: 2, y: 72.5),
]);
์ฐจํธ ๋ผ์ด๋ธ๋ฌ๋ฆฌ ์๊ทธ๋์ฒ์ ๋๋ฉ์ธ ํ์
์ ๋
ธ์ถํ์ง ์์ผ๋ฉด์๋, point.x/point.y ์ ๊ทผ์ด ๊ฐ๋ฅํ ๊ฐ๋
์ฑ์ ์ป์์ต๋๋ค. ์์ ๋ฐ์ดํฐ ๋ฌถ์์๋ record๊ฐ ๊ฑฐ์ ํญ์ ๋ต์
๋๋ค.
Records๋ฅผ ํด๋์ค ๋์ ์ฐ๋ ๊ฐ์ด๋
๋ค๋ง record๊ฐ ๋ง๋ฅ์ ์๋๋๋ค. ๋ค์ ๊ธฐ์ค์ผ๋ก record vs class๋ฅผ ๊ฒฐ์ ํฉ๋๋ค.
- ์ด๋ฆ์ด ์๋ฏธ๋ฅผ ๊ฐ์ง๋ฉด class โ
User/Meal์ฒ๋ผ ๋๋ฉ์ธ ์๋ฏธ๊ฐ ์๋ ๊ฐ์ฒด๋ ํด๋์ค. - ํ๋๊ฐ 3๊ฐ ์ด์์ด๊ณ ์ฌ์ฌ์ฉ๋๋ค๋ฉด class โ record๋ ์๊ทธ๋์ฒ๊ฐ ๊ธธ์ด์ง๋ฉด ๊ฐ๋ ์ฑ์ด ๋จ์ด์ง๋๋ค.
- ๋ฉ์๋๋ฅผ ๋ถ์ด๊ณ ์ถ์ผ๋ฉด class โ record๋ ๋ฉ์๋๋ฅผ ๊ฐ์ง ์ ์์ต๋๋ค.
- ๋ก์ปฌยท์์ยท๋ฐํ๊ฐ ๋ฌถ์์ด๋ฉด record โ ์งง๊ฒ ์ด์ ์์ ๋ฐ์ดํฐ์ ์ ํฉ.
2. Patterns โ destructuring + ์กฐ๊ฑด ๋งค์นญ
Dart 3์ ํจํด์ ๋ ๊ฐ์ง ์ญํ ์ ๋์์ ํฉ๋๋ค.
- Destructuring โ ๋ณตํฉ ๊ตฌ์กฐ์์ ๊ฐ์ ๊บผ๋ด๊ธฐ
- Matching โ ์ด๋ค ํํ์ธ์ง ๊ฒ์ฌ
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 1. Variable pattern โ ๊ทธ๋ฅ ๋ณ์์ ๋ด๊ธฐ
final user = User(id: 1, name: '๋ฏผ์');
final User(:id, :name) = user; // id = 1, name = '๋ฏผ์'
// 2. List pattern โ ๋ฆฌ์คํธ ํํ์ ๋์์ ๋ถํด
final list = [1, 2, 3];
if (list case [final first, _, final last]) {
print('$first, $last'); // 1, 3
}
// 3. Map pattern
final json = {'type': 'circle', 'r': 5};
if (json case {'type': 'circle', 'r': final r}) {
print(r); // 5
}
: ๋จ์ถ (shorthand)
Object(:fieldName)์ Object(fieldName: fieldName)์ ๋จ์ถ ํ๊ธฐ์
๋๋ค. ๊ฐ์ ์ด๋ฆ์ ๋ณ์์ ๋ฐ์ธ๋ฉํ ๋ ํ ๊ธ์๋ผ๋ ์ค์ด๋ ค๋ syntactic sugar์
๋๋ค.
1
2
3
4
5
// ํ์ด์ด ํํ
final Circle(r: r) = circle;
// ๋จ์ถํ
final Circle(:r) = circle;
Wildcard _ ํจํด โ Stage 8์์ ์ค์ ์ฌ์ฉ
์ด ๋ถ๋ถ์ด ์บก์คํค ์ฝ๋์ ๊ฐ์ฅ ๋น๋ฒํ๊ฒ ๋ฑ์ฅํฉ๋๋ค. separatorBuilder ๊ฐ์ ์ฝ๋ฐฑ์ ์ธ์ ๋ ๊ฐ๋ฅผ ๋ฐ์ง๋ง ๋ ๋ค ์ ์ฐ๋ ๊ฒฝ์ฐ๊ฐ ๋ง์ต๋๋ค. ์์ (Dart 2)์๋ ๋ค์์ฒ๋ผ ์ผ์ต๋๋ค.
1
2
3
4
5
6
// Dart 2 โ ๋ฏธ์ฌ์ฉ ์ธ์์ ์ด๋ฆ์ ๋ถ์ฌ์ผ ํ์
ListView.separated(
itemCount: items.length,
separatorBuilder: (context, index) => const Divider(),
itemBuilder: ...,
)
Dart 3์์๋ wildcard _๋ก โ์ ์ด๋คโ๋ ์๋๋ฅผ ๋ช
์ํ ์ ์์ต๋๋ค.
1
2
3
4
5
6
// Dart 3 โ ๋ฏธ์ฌ์ฉ ์ธ์๋ฅผ wildcard๋ก
ListView.separated(
itemCount: items.length,
separatorBuilder: (_, _) => const Divider(),
itemBuilder: (_, i) => ListTile(title: Text(items[i].name)),
)
Stage 8์์ lint๊ฐ ์ก์์ ์ผ๊ด๋ก ์ ๋ฆฌํ๋ ํ์ ์ด ์๋๋ฐ, ๊ทธ๊ฒ ์ด ํจํด์ ๋๋ค.
1
2
style: drop deprecated activeColor + use Dart 3 wildcards (phase 8.8 fixup)
style(notification): use Dart 3 wildcard `(_, _)` in separatorBuilder
3. Switch Expression โ ์(expression)์ผ๋ก์์ switch
Dart 2 ์์ ์ switch๋ statement๋ผ ๋ณ์์ ์ง์ ํ ๋นํ ์ ์์์ต๋๋ค. ๋ค์์ฒ๋ผ if ์ฌ์ฌ์ ์ฐ๊ฑฐ๋, ํจ์๋ก ๊ฐ์ธ์ผ ํ์ต๋๋ค.
1
2
3
4
5
6
7
8
9
// Dart 2
String label;
if (status == Status.success) {
label = '์ฑ๊ณต';
} else if (status == Status.failure) {
label = '์คํจ';
} else {
label = '์งํ ์ค';
}
Dart 3์ switch expression์ ์(expression)์ด๋ผ ๋ณ์์ ์ง์ ํ ๋นํ ์ ์๊ณ , ๋ชจ๋ ์ผ์ด์ค๋ฅผ ์ฒ๋ฆฌํ์ง ์์ผ๋ฉด ์ปดํ์ผ ์๋ฌ๊ฐ ๋ฐ์ํฉ๋๋ค.
1
2
3
4
5
final label = switch (status) {
Status.success => '์ฑ๊ณต',
Status.failure => '์คํจ',
Status.inProgress => '์งํ ์ค',
};
break๋ ์๊ณ , :์ด ์๋๋ผ => ํ์ดํ ํ๊ธฐ๋ฅผ ์๋๋ค. ํํ์ ํ ์ค์ด๋ผ ๊ฐ๋
์ฑ์ด ๋งค์ฐ ์ข์ต๋๋ค.
sealed class์ ๊ฒฐํฉ
์ง๋ ๊ธ์์ ์ ๋ฆฌํ AppError sealed์ ๊ฒฐํฉํ๋ฉด ์ง๊ฐ๊ฐ ๋๋ฌ๋ฉ๋๋ค.
1
2
3
4
5
6
String describe(AppError e) => switch (e) {
NetworkError(:final message) => '๋คํธ์ํฌ ์ค๋ฅ: $message',
NotFoundError(:final resource) => '$resource์(๋ฅผ) ์ฐพ์ ์ ์์ต๋๋ค',
UnauthorizedError() => '๋ก๊ทธ์ธ์ด ํ์ํฉ๋๋ค',
UnknownError(:final error) => '์ ์ ์๋ ์ค๋ฅ: $error',
};
์ variant๋ฅผ AppError์ ์ถ๊ฐํ๋ฉด ์ switch๊ฐ ์ปดํ์ผ ์๋ฌ๋ก ๋งํ์, ์ฌ์ฉ์ฒ๋ฅผ ๊ฐ์ ๋ก ์
๋ฐ์ดํธํ๊ฒ ๋ฉ๋๋ค. ๋๋ฉ์ธ ๋ณ๊ฒฝ ์ ๋๋ฝ์ ์ปดํ์ผ๋ฌ๊ฐ ์ก์์ฃผ๋ ์์ ๋ง์ด ๋ฉ๋๋ค.
if-case โ ๋จ์ผ ํจํด ๋งค์นญ
์ ์ฒด ๋ถ๊ธฐ๊ฐ ์๋๋ผ ํน์ ํจํด๋ง ๋งค์นญํ๊ณ ์ถ์ผ๋ฉด if-case ๊ตฌ๋ฌธ์ ์๋๋ค.
1
2
3
if (result case Ok(:final value)) {
print('์ฑ๊ณต: $value');
}
์ด๊ฒ ๊ต์ฅํ ์ ์ฉํ ํจํด์
๋๋ค. Result<T>์์ ์ฑ๊ณต ์ผ์ด์ค๋ง ๋ถ๊ธฐํ๊ณ ์คํจ๋ ๋ค๋ฅธ ํ๋ฆ์ผ๋ก ๋ณด๋ด๊ณ ์ถ์ ๋ ํ ์ค๋ก ๊ฐ๋ฅํฉ๋๋ค.
4. Guard ์กฐ๊ฑด โ when
ํจํด ๋งค์นญ์ ์ถ๊ฐ ์กฐ๊ฑด์ ๋ถ์ด๊ณ ์ถ์ ๋ when ํค์๋๋ฅผ ์๋๋ค. ํจํด์ด ๋งค์นญ๋๋๋ผ๋ when ์กฐ๊ฑด์ด false๋ฉด ๋ค์ ์ผ์ด์ค๋ก ๋์ด๊ฐ๋๋ค.
1
2
3
4
5
6
7
String classify(int score) => switch (score) {
>= 90 => 'A',
>= 80 => 'B',
>= 70 => 'C',
_ when score < 0 => 'invalid',
_ => 'F',
};
์์์๋ relational pattern(>= 90)๋ ์ฐ๊ณ ์์ต๋๋ค. Dart 3๊ฐ ๊น์ ๋์ ํจํด ์ข
๋ฅ๊ฐ ์๋ ๋ง์์, ๊ฐ์ ํํ์ ํ๋์ ์ฌ๋ฌ ํจํด์ ์กฐํฉํ ์ ์์ต๋๋ค.
5. ์ค์ ์บก์คํค ์ฝ๋ ์์ ๋ชจ์
ํ์ต ์ ๋ฆฌ ์ฐจ์์์ ์บก์คํค์์ ์ด ๊ธฐ๋ฅ๋ค์ด ์ค์ ๋ก ์ด๋์ ๋ค์ด๊ฐ๋์ง ํ ์๋ฆฌ์ ๋ชจ์ ๋ดค์ต๋๋ค.
Records โ fl_chart ์ขํ
1
2
3
4
5
6
final List<({double x, double y})> weightPoints = [
(x: 0, y: 70.5),
(x: 1, y: 70.8),
(x: 2, y: 71.2),
// ...
];
Map<String, double>์ด๋ ๋ณ๋ ํด๋์ค๋ณด๋ค ํจ์ฌ ๊ฐ๋ณ์ต๋๋ค.
Switch expression โ Result ๋งค์นญ
1
2
3
4
state = switch (result) {
Ok(:final value) => AsyncData(value),
Err(:final error) => AsyncError(error, StackTrace.current),
};
repository๊ฐ ๋ฐํํ Result<DietDay>๋ฅผ Riverpod์ AsyncValue๋ก ํ ์ค์ ๋ณํํฉ๋๋ค.
Wildcard โ ๋ฏธ์ฌ์ฉ ์ฝ๋ฐฑ ์ธ์
1
2
3
4
5
ListView.separated(
itemCount: notifications.length,
separatorBuilder: (_, _) => const Divider(height: 1),
itemBuilder: (_, i) => NotificationTile(item: notifications[i]),
)
if-case โ Stream ์ด๋ฒคํธ ๋ถ๊ธฐ
1
2
3
4
5
notificationStream.listen((event) {
if (event case PushArrived(:final notification)) {
inAppCenter.add(notification);
}
});
PushArrived๊ฐ sealed family์ ํ variant๋ผ๋ฉด, if-case๋ก ์์ฐ์ค๋ฝ๊ฒ unpack๋ฉ๋๋ค.
6. ์ ๋ฆฌ / ๋ค์ ํ์ต ๊ณํ
Dart 3์ records / patterns / switch expression์ ์ฒ์ ๋ดค์ ๋๋ โKotlin์ด๋ Rust ๊ฐ์ ๊ฒโ์ด๋ผ๊ณ ๋ง ๋๊ผ๋๋ฐ, ์ค์ ๋ก ์ฝ๋์ ๋ฐ์ ๋ณด๋ ๋ค์ ํจ๊ณผ๊ฐ ๋ถ๋ช ํ์ต๋๋ค.
- ์์ ๋ฐ์ดํฐ ๋ฌถ์์ ๋ณด์ผ๋ฌํ๋ ์ดํธ๊ฐ ์ฌ๋ผ์ง๋๋ค. record๋ก ๋.
- sealed class์ ๊ฒฐํฉํ ๋ exhaustiveness ๊ฒ์ฌ๊ฐ ์ปดํ์ผ๋ฌ ์์ ๋ง์ด ๋ฉ๋๋ค.
- ์ฝ๋ฐฑ ์๊ทธ๋์ฒ๊ฐ ๊น๋ํด์ง๋๋ค.
_๋ก ๋ฌด๊ด ์ธ์๋ฅผ ๋ช ์ํ๋ฉด ์๋๊ฐ ๋ถ๋ช ํ ๋๋ฌ๋ฉ๋๋ค.
๋ค์ ํ์ต์ ์บก์คํค ์ฝ๋๋ฅผ ๋ค์ ํ ๋ฒ ํ์ผ๋ฉด์ ๋ฐ๊ฒฌํ ๋ ๋ค๋ฅธ Dart ๋ฉํ ํจํด โ Extension methods, Mixin, build_runner๋ฅผ ํตํ ์ฝ๋ ์์ฑ์ ์ ๋ฆฌํ ์์ ์ ๋๋ค. ํ๊ธฐ ๋ง์ง๋ง ํ์ต ๋ ธํธ๊ฐ ๋ ๊ฒ ๊ฐ์ต๋๋ค.
๐ ์์ฝ
Dart 3์ records / patterns / switch expression์ ์บก์คํค ์ฝ๋์ ์์ฐ์ค๋ฝ๊ฒ ๋
น์๋ ํต์ฌ ๊ธฐ๋ฅ๋ค์
๋๋ค. records๋ ์์ ๋ฐ์ดํฐ ๋ฌถ์(์ฐจํธ ์ขํ ๋ฑ)์์ ๋ณด์ผ๋ฌํ๋ ์ดํธ๋ฅผ ์์ ๊ณ , switch expression์ sealed class์ ๊ฒฐํฉํด ๋ชจ๋ variant๋ฅผ ๋ง๋ผํ์ง ์์ผ๋ฉด ์ปดํ์ผ ์๋ฌ๋ฅผ ๋์ ๋๋ฉ์ธ ํ์ฅ ์ ์์ ๋ง์ด ๋ฉ๋๋ค. _ wildcard ํจํด์ ์ฝ๋ฐฑ์ ๋ฏธ์ฌ์ฉ ์ธ์๋ฅผ ๋ช
์์ ์ผ๋ก ํํํด ๊ฐ๋
์ฑ์ ๋์ด๊ณ , if-case๋ก ๋จ์ผ ํจํด ๋งค์นญ์ด ํ ์ค์ ๊ฐ๋ฅํฉ๋๋ค. ์ ์ ๋ฌธ๋ฒ ์ถ๊ฐ๋ก ํํ๋ ฅ์ด ํฌ๊ฒ ๋์ด๋, Dart 3์์ ๊ฐ์ฅ ๋ง์กฑ์ค๋ฌ์ด ๋ณํ์์ต๋๋ค.