Post

[Capstone-Start] Dart ํ•™์Šต โ€” Extension Methods, Mixin, Code Generation

[Capstone-Start] Dart ํ•™์Šต โ€” Extension Methods, Mixin, Code Generation

๐Ÿ“… ์ผ์‹œ: 2026. 05. 27

๐Ÿ’ฌ ๋‚ด์šฉ: Extension methods, Mixin, build_runner ์ฝ”๋“œ ์ƒ์„ฑ โ€” Flutter ๊ฐœ๋ฐœ์—์„œ ์ž์ฃผ ๋ณด๋Š” ๋ฉ”ํƒ€ ํŒจํ„ด ์ •๋ฆฌ


์‹œ์—ฐ ํ™˜๊ฒฝ ์•ˆ์ •ํ™”๋Š” ์–ด์ œ(5/26)๊นŒ์ง€ ๋๋‚ฌ๊ณ , ์˜ค๋Š˜์€ ์บก์Šคํ†ค ์ฝ”๋“œ๋ฅผ ๋‹ค์‹œ ํ•œ ๋ฒˆ ํ›‘์œผ๋ฉด์„œ ๋ฐœ๊ฒฌํ•œ Dart์˜ ๋ฉ”ํƒ€ ํŒจํ„ด 3๊ฐ€์ง€ โ€” Extension methods, Mixin, build_runner๋ฅผ ํ†ตํ•œ ์ฝ”๋“œ ์ƒ์„ฑ์„ ์ •๋ฆฌํ•ฉ๋‹ˆ๋‹ค.

์ด ์„ธ ๊ฐ€์ง€๋Š” ์ฒ˜์Œ Flutter ์ฝ”๋“œ๋ฅผ ๋ณผ ๋•Œ ๊ฐ€์žฅ ๋‚ฏ์„ค๊ฒŒ ๋А๊ปด์กŒ๋˜ ํ‚ค์›Œ๋“œ๋“ค์ž…๋‹ˆ๋‹ค. with WidgetsBindingObserver, extension StringX on String, *.g.dart ์ž๋™ ์ƒ์„ฑ ํŒŒ์ผ์ด ์–ด๋–ค ๋ฉ”์ปค๋‹ˆ์ฆ˜์ธ์ง€ ์ฒ˜์Œ์—๋Š” ํ•œ ๋ฒˆ์”ฉ ๋ฉˆ์ถฐ์„œ ์ฐพ์•„๋ด์•ผ ํ–ˆ์Šต๋‹ˆ๋‹ค. ๊ฒ€์ƒ‰ํ•ด์„œ ๋“ค์–ด์˜ค์‹  ๋ถ„๋“ค๊ป˜ ๋„์›€์ด ๋  ๋งŒํ•œ ์ •๋ฆฌ๋กœ ๋‚จ๊น๋‹ˆ๋‹ค.

1. Extension Methods โ€” ํƒ€์ž…์— ์™ธ๋ถ€์—์„œ ๋ฉ”์„œ๋“œ ์ถ”๊ฐ€

Extension์€ ๊ธฐ์กด ํƒ€์ž…(ํด๋ž˜์Šค/์ธํ„ฐํŽ˜์ด์Šค/built-in)์— ์†Œ์Šค ์ฝ”๋“œ๋ฅผ ์ˆ˜์ •ํ•˜์ง€ ์•Š๊ณ  ๋ฉ”์„œ๋“œ๋ฅผ ์ถ”๊ฐ€ํ•˜๋Š” ๊ธฐ๋Šฅ์ž…๋‹ˆ๋‹ค. C#์˜ extension method, Kotlin์˜ ํ™•์žฅ ํ•จ์ˆ˜, Swift์˜ extension๊ณผ ๋™์ผํ•œ ๊ฐœ๋…์ž…๋‹ˆ๋‹ค.

๊ธฐ๋ณธ ์‚ฌ์šฉ

1
2
3
4
5
6
7
8
9
extension StringExtensions on String {
  String get reversed => split('').reversed.join();
  bool get isBlank => trim().isEmpty;
}

void main() {
  print('Hello'.reversed); // 'olleH'
  print('  '.isBlank);     // true
}

extension <์ด๋ฆ„> on <ํƒ€์ž…> ๊ตฌ๋ฌธ์œผ๋กœ ์ถ”๊ฐ€ํ•ฉ๋‹ˆ๋‹ค. ๋ฉ”์„œ๋“œ๋Š” ๋งˆ์น˜ ๊ทธ ํƒ€์ž…์˜ ์›๋ž˜ ๋ฉ”์„œ๋“œ์ธ ๊ฒƒ์ฒ˜๋Ÿผ ์  ํ‘œ๊ธฐ๋กœ ํ˜ธ์ถœ๋ฉ๋‹ˆ๋‹ค.

์บก์Šคํ†ค์—์„œ ์‚ฌ์šฉํ•œ ์˜ˆ โ€” DateTime ํ™•์žฅ

์‹๋‹จ ํŽ˜์ด์ง€์—์„œ ์ผ์š”์ผ~ํ† ์š”์ผ ์ฃผ๊ฐ„ ์ŠคํŠธ๋ฆฝ์„ ๊ทธ๋ฆด ๋•Œ, Dart์˜ DateTime.weekday๊ฐ€ ์›”์š”์ผ์ด 1, ์ผ์š”์ผ์ด 7์ธ ISO ๊ธฐ์ค€์ด๋ผ ๋งค๋ฒˆ weekday % 7์„ ๋ถ€๋ฅด๋Š” ๊ฒŒ ๊ฑฐ์Šฌ๋ ธ์Šต๋‹ˆ๋‹ค. ๊ทธ๋ž˜์„œ shared/extensions/์— date_x.dart๋กœ ํ™•์žฅ์„ ๋ฌถ์—ˆ์Šต๋‹ˆ๋‹ค.

1
2
3
4
5
6
7
8
9
10
11
12
13
extension DateTimeX on DateTime {
  /// ์ผ์š”์ผ์„ 0์œผ๋กœ ๋‘๋Š” weekday (Diet ํŽ˜์ด์ง€ ์ฃผ๊ฐ„ ์ŠคํŠธ๋ฆฝ์šฉ)
  int get sundayIndex => weekday % 7;

  /// ํ•ด๋‹น ์ฃผ์˜ ์ผ์š”์ผ 0์‹œ
  DateTime startOfWeek() {
    final sundayOffset = sundayIndex;
    return DateTime(year, month, day - sundayOffset);
  }

  /// ์‹œ๊ฐ„์„ 0:00์œผ๋กœ ์ž˜๋ผ๋‚ธ ๊ฐ™์€ ๋‚ ์งœ
  DateTime get atMidnight => DateTime(year, month, day);
}

์ด๋ ‡๊ฒŒ ๋ฌถ์–ด๋‘๋‹ˆ ํ˜ธ์ถœ๋ถ€๊ฐ€ ๊น”๋”ํ•ด์ง‘๋‹ˆ๋‹ค.

1
2
final monday = today.startOfWeek().add(const Duration(days: 1));
final index = selectedDate.sundayIndex;

์ฃผ์˜์  โ€” ์ •์  ๋ถ„์„์—์„œ๋งŒ ๋™์ž‘

Extension์€ ์ปดํŒŒ์ผ๋Ÿฌ๊ฐ€ ์ •์ ์œผ๋กœ ํ˜ธ์ถœ ์œ„์น˜๋ฅผ ๋ณด๊ณ  ์–ด๋–ค extension์˜ ๋ฉ”์„œ๋“œ์ธ์ง€ ๊ฒฐ์ •ํ•ฉ๋‹ˆ๋‹ค. ๋Ÿฐํƒ€์ž„ ๋‹คํ˜•์„ฑ์€ ์ ์šฉ๋˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค.

1
2
3
4
5
6
extension A on Object { String name() => 'A'; }
extension B on String { String name() => 'B'; }

Object x = 'hello';
print(x.name()); // 'A' โ€” ์ •์  ํƒ€์ž…์€ Object
print((x as String).name()); // 'B'

์ด ๋™์ž‘์€ Java์˜ ์ •์  ๋ฉ”์„œ๋“œ ๋””์ŠคํŒจ์น˜์™€ ์œ ์‚ฌํ•ฉ๋‹ˆ๋‹ค. extension์„ โ€œ๋Ÿฐํƒ€์ž„ ๋ฉ”์„œ๋“œ ์ถ”๊ฐ€โ€๋กœ ์˜คํ•ดํ•˜๋ฉด ํ•จ์ •์— ๋น ์งˆ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

Generic Extension

extension๋„ generic์„ ๋ฐ›์„ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ์บก์Šคํ†ค์—์„œ๋Š” Iterable์— ์ž์ฃผ ์“ฐ๋Š” ํ—ฌํผ๋ฅผ ๋ฌถ์–ด ๋‘์—ˆ์Šต๋‹ˆ๋‹ค.

1
2
3
4
5
6
7
8
9
10
extension IterableX<T> on Iterable<T> {
  T? firstOrNull(bool Function(T) test) {
    for (final e in this) {
      if (test(e)) return e;
    }
    return null;
  }
}

final firstHigh = scores.firstOrNull((s) => s > 90);

ํ‘œ์ค€ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ์— firstWhere๊ฐ€ ์žˆ์ง€๋งŒ, ์กฐ๊ฑด์— ๋งž๋Š” ๊ฒŒ ์—†์œผ๋ฉด throwํ•˜๋Š” ์ •์ฑ…์ด ๋ถˆํŽธํ•  ๋•Œ ์œ„ ํ—ฌํผ๊ฐ€ ์œ ์šฉํ•ฉ๋‹ˆ๋‹ค.

2. Mixin โ€” with ํ‚ค์›Œ๋“œ๋กœ ํ•ฉ์„ฑํ•˜๋Š” ํ–‰์œ„

Mixin์€ ํด๋ž˜์Šค์— ๊ณตํ†ต ํ–‰์œ„(๋ฉ”์„œ๋“œ/ํ•„๋“œ)๋ฅผ ํ•ฉ์„ฑํ•˜๋Š” ๋ฉ”์ปค๋‹ˆ์ฆ˜์ž…๋‹ˆ๋‹ค. Dart๋Š” ๋‹ค์ค‘ ์ƒ์†์„ ์ง€์›ํ•˜์ง€ ์•Š์ง€๋งŒ, mixin์œผ๋กœ ๋น„์Šทํ•œ ํšจ๊ณผ๋ฅผ ์•ˆ์ „ํ•˜๊ฒŒ ์–ป์„ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

๊ธฐ๋ณธ ์‚ฌ์šฉ

1
2
3
4
5
6
7
8
9
10
11
mixin Loggable {
  void log(String message) {
    print('[$runtimeType] $message');
  }
}

class UserController with Loggable {
  void load() {
    log('loading user'); // mixin์ด ์ œ๊ณตํ•œ ๋ฉ”์„œ๋“œ
  }
}

with๋กœ mixin์„ ํ•ฉ์„ฑํ•˜๋ฉด ๊ทธ ํด๋ž˜์Šค์— mixin์˜ ๋ฉค๋ฒ„๊ฐ€ ๊ทธ๋Œ€๋กœ ๋ฐ•ํž™๋‹ˆ๋‹ค. ์—ฌ๋Ÿฌ mixin์„ ํ•ฉ์„ฑํ•  ์ˆ˜๋„ ์žˆ์Šต๋‹ˆ๋‹ค.

1
class HomeController with Loggable, ChangeNotifier { ... }

Flutter์—์„œ ์ž์ฃผ ๋ณด๋Š” mixin

Flutter SDK๋Š” mixin์„ ๊ต‰์žฅํžˆ ๋งŽ์ด ์”๋‹ˆ๋‹ค. ๋‹ค์Œ์€ ์บก์Šคํ†ค์—์„œ ์ง์ ‘ ๋งˆ์ฃผ์นœ mixin๋“ค์ž…๋‹ˆ๋‹ค.

Mixin์šฉ๋„
ChangeNotifierlistener ํŒจํ„ด (addListener / notifyListeners)
WidgetsBindingObserver์•ฑ ๋ผ์ดํ”„์‚ฌ์ดํด (didChangeAppLifecycleState)
SingleTickerProviderStateMixin์• ๋‹ˆ๋ฉ”์ด์…˜ ์ปจํŠธ๋กค๋Ÿฌ์šฉ vsync ์ œ๊ณต
AutomaticKeepAliveClientMixinํƒญ ์ „ํ™˜ ์‹œ ์œ„์ ฏ ์ƒํƒœ ์œ ์ง€
1
2
3
4
5
6
class _ExercisePageState extends State<ExercisePage>
    with SingleTickerProviderStateMixin {
  late final TabController _tabController =
      TabController(length: 2, vsync: this);
  // ...
}

vsync: this๋กœ ์ž์‹ ์„ ticker provider๋กœ ๋„˜๊ธฐ๋Š” ํŒจํ„ด์ธ๋ฐ, mixin์ด ์ œ๊ณตํ•œ createTicker ๋ฉ”์„œ๋“œ ๋•์— ๊ฐ€๋Šฅํ•ฉ๋‹ˆ๋‹ค.

on ํ‚ค์›Œ๋“œ โ€” mixin ์‚ฌ์šฉ์ฒ˜ ์ œํ•œ

mixin์€ ํŠน์ • ํด๋ž˜์Šค์˜ ํ•˜์œ„์—์„œ๋งŒ ์“ฐ์ผ ์ˆ˜ ์žˆ๋„๋ก on์œผ๋กœ ์ œ์•ฝํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. mixin์ด ์˜์กดํ•˜๋Š” ๋ฉ”์„œ๋“œ/ํ•„๋“œ๊ฐ€ ๋ถ€๋ชจ ํด๋ž˜์Šค์— ์žˆ์„ ๋•Œ ์œ ์šฉํ•ฉ๋‹ˆ๋‹ค.

1
2
3
4
5
mixin ScrollLogger on State<StatefulWidget> {
  void logScroll(double offset) {
    debugPrint('[$widget] offset=$offset');
  }
}

์œ„ mixin์€ State<StatefulWidget> ๋˜๋Š” ๊ทธ ํ•˜์œ„ ํด๋ž˜์Šค์—๋งŒ ํ•ฉ์„ฑ๋  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

Extension vs Mixin โ€” ์–ธ์ œ ๋ฌด์—‡์„ ์“ธ๊นŒ

๊ฐ™์€ ๋“ฏ ๋‹ค๋ฅธ ๋‘ ๊ธฐ๋Šฅ์„ ๋‹ค์Œ ๊ธฐ์ค€์œผ๋กœ ๊ตฌ๋ถ„ํ•ฉ๋‹ˆ๋‹ค.

๊ธฐ์ค€ExtensionMixin
๋Œ€์ƒ๊ธฐ์กด ํƒ€์ž… (์ˆ˜์ • ๋ถˆ๊ฐ€)์ƒˆ ํด๋ž˜์Šค๋ฅผ ๋งŒ๋“ค ๋•Œ ํ•ฉ์„ฑ
์ƒํƒœ๊ฐ€์งˆ ์ˆ˜ ์—†์Œ๊ฐ€์งˆ ์ˆ˜ ์žˆ์Œ
this ์ ‘๊ทผextension ์•ˆ์—์„œ this ๊ฐ€๋Šฅ์ธ์Šคํ„ด์Šค์˜ ์ผ๋ถ€๊ฐ€ ๋จ
๋‹คํ˜•์„ฑ์ •์  ๋””์ŠคํŒจ์น˜๋™์  ๋””์ŠคํŒจ์น˜
์ถฉ๋Œํƒ€์ž…์— ๊ฐ™์€ ์ด๋ฆ„ ์žˆ์œผ๋ฉด ์›๋ž˜ ์šฐ์„ ํ•ฉ์„ฑ ์ˆœ์„œ๋กœ ๊ฒฐ์ •

์š”์•ฝํ•˜๋ฉด, ์ด๋ฏธ ์žˆ๋Š” ํƒ€์ž…์— ๊ฐ€๋ฒผ์šด ์œ ํ‹ธ๋ฆฌํ‹ฐ๋ฅผ ๋ถ™์ด๊ณ  ์‹ถ์œผ๋ฉด extension, ์ƒˆ ํด๋ž˜์Šค๋ฅผ ๋งŒ๋“ค ๋•Œ ํ–‰์œ„/์ƒํƒœ๋ฅผ ํ•ฉ์„ฑํ•˜๊ณ  ์‹ถ์œผ๋ฉด mixin์ž…๋‹ˆ๋‹ค.

3. Code Generation โ€” build_runner๋กœ ๋ณด์ผ๋Ÿฌํ”Œ๋ ˆ์ดํŠธ ์ž๋™ํ™”

Dart๋Š” ๋งคํฌ๋กœ(์ปดํŒŒ์ผ ์‹œ์ ์— ์ฝ”๋“œ๋ฅผ ๋ณ€ํ˜•์‹œํ‚ค๋Š” ๊ธฐ๋Šฅ)๊ฐ€ ์ •์‹ ์ง„์ž…ํ•˜๊ธฐ ์ „์ด๋ผ, ๊ทธ๋™์•ˆ์˜ ๋Œ€์•ˆ์œผ๋กœ build_runner ๊ธฐ๋ฐ˜ ์ฝ”๋“œ ์ƒ์„ฑ์„ ๊ด‘๋ฒ”์œ„ํ•˜๊ฒŒ ์”๋‹ˆ๋‹ค. ์–ด๋…ธํ…Œ์ด์…˜์„ ๋ถ™์ธ ์†Œ์Šค ํŒŒ์ผ์„ ๋ณด๊ณ  .g.dart ๋˜๋Š” .freezed.dart๋ฅผ ์ƒ์„ฑํ•ด ์ฃผ๋Š” ๋ฐฉ์‹์ž…๋‹ˆ๋‹ค.

์บก์Šคํ†ค์—์„œ ์“ฐ๋Š” codegen ๋„๊ตฌ๋“ค

pubspec.yaml์˜ dev_dependencies๋ฅผ ๋ณด๋ฉด ํ•œ๋ˆˆ์— ์ •๋ฆฌ๋ฉ๋‹ˆ๋‹ค.

1
2
3
4
5
6
dev_dependencies:
  build_runner: ^2.4.13
  riverpod_generator: ^2.6.5
  freezed: ^3.0.0
  json_serializable: ^6.8.0
  drift_dev: ^2.20.3

๊ฐ ๋„๊ตฌ๊ฐ€ ๋งŒ๋“ค์–ด ์ฃผ๋Š” ๊ฒƒ์€ ๋‹ค์Œ๊ณผ ๊ฐ™์Šต๋‹ˆ๋‹ค.

๋„๊ตฌ์ž…๋ ฅ ์–ด๋…ธํ…Œ์ด์…˜์ƒ์„ฑ ํŒŒ์ผ๋งŒ๋“œ๋Š” ์ฝ”๋“œ
freezed@freezed*.freezed.dartsealed ๊ณ„์ธต, ==, hashCode, copyWith, toString
json_serializable@JsonSerializable*.g.dartfromJson / toJson
riverpod_generator@riverpod*.g.dartProvider ์ •์˜
drift_dev@DriftDatabase*.g.dartDB ํด๋ž˜์Šค, DAO, ์ฟผ๋ฆฌ ํ—ฌํผ

์‹ค์ œ ๋ชจ๋ธ ์˜ˆ์‹œ

์บก์Šคํ†ค์˜ DietDay ๋ชจ๋ธ์€ ์œ„ ๋„๊ตฌ๋“ค์„ ๋‹ค์Œ์ฒ˜๋Ÿผ ๊ฒฐํ•ฉํ•ฉ๋‹ˆ๋‹ค.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import 'package:freezed_annotation/freezed_annotation.dart';

part 'diet_day.freezed.dart';
part 'diet_day.g.dart';

@freezed
class DietDay with _$DietDay {
  const factory DietDay({
    required DateTime date,
    required int calories,
    required int sodium,
    required List<MealEntry> meals,
  }) = _DietDay;

  factory DietDay.fromJson(Map<String, dynamic> json) =>
      _$DietDayFromJson(json);
}

part ์ง€์‹œ์–ด๋กœ ์ƒ์„ฑ ํŒŒ์ผ์„ ๊ฐ™์€ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ์— ํฌํ•จ์‹œํ‚ค๊ณ , freezed๊ฐ€ sealed ๊ณ„์ธต + copyWith๋ฅผ, json_serializable์ด fromJson/toJson์„ ์ž๋™์œผ๋กœ ๋งŒ๋“ค์–ด ์ค๋‹ˆ๋‹ค. ์†์œผ๋กœ ์งค ์ฝ”๋“œ๊ฐ€ ๊ฑฐ์˜ 0์ค„์— ๊ฐ€๊นŒ์›Œ์ง‘๋‹ˆ๋‹ค.

build_runner ์‚ฌ์šฉ๋ฒ•

1
2
3
4
5
# ํ•œ ๋ฒˆ ์ƒ์„ฑ
dart run build_runner build --delete-conflicting-outputs

# ํŒŒ์ผ ๋ณ€๊ฒฝ ๊ฐ์ง€ํ•˜๋ฉฐ ์ž๋™ ์žฌ์ƒ์„ฑ
dart run build_runner watch --delete-conflicting-outputs

--delete-conflicting-outputs๋Š” ์ด์ „ ์ƒ์„ฑ ํŒŒ์ผ์ด ๋‚จ์•„ ์žˆ์„ ๋•Œ ์ถฉ๋Œ์„ ์ž๋™ ํ•ด๊ฒฐํ•ฉ๋‹ˆ๋‹ค. CI์—์„œ๋Š” ๋งค๋ฒˆ ๊นจ๋—ํ•˜๊ฒŒ generationํ•˜๊ธฐ ์œ„ํ•ด build๋ฅผ ํ•œ ๋ฒˆ ๋Œ๋ฆฝ๋‹ˆ๋‹ค.

part / part of ์ง€์‹œ์–ด

์ฝ”๋“œ ์ƒ์„ฑ์„ ์“ฐ๋ฉด ์ž์ฃผ ๋งˆ์ฃผ์น˜๋Š” ํ‚ค์›Œ๋“œ๊ฐ€ part / part of์ž…๋‹ˆ๋‹ค.

1
2
3
// diet_day.dart
part 'diet_day.freezed.dart';
part 'diet_day.g.dart';
1
2
3
// diet_day.freezed.dart (์ž๋™ ์ƒ์„ฑ)
part of 'diet_day.dart';
// ...

part๋กœ ๋ฌถ์ธ ํŒŒ์ผ๋“ค์€ ๊ฐ™์€ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋กœ ์ทจ๊ธ‰๋ฉ๋‹ˆ๋‹ค. ์ฆ‰ ๋ณ„๋„ import ์—†์ด ์„œ๋กœ์˜ private ๋ฉค๋ฒ„์— ์ ‘๊ทผํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ์ž๋™ ์ƒ์„ฑ ํŒŒ์ผ์ด private _$DietDay ๊ฐ™์€ ๋ฉค๋ฒ„๋ฅผ ๋งŒ๋“ค ์ˆ˜ ์žˆ๋Š” ์ด์œ ๊ฐ€ ์ด๊ฒƒ์ž…๋‹ˆ๋‹ค.

์ฝ”๋“œ ์ƒ์„ฑ์„ ์“ธ ๋•Œ ์ฃผ์˜์ 

  • ์ƒ์„ฑ ํŒŒ์ผ์„ ์ง์ ‘ ์ˆ˜์ •ํ•˜์ง€ ์•Š๋Š”๋‹ค โ€” ๋‹ค์Œ ๋นŒ๋“œ์—์„œ ๋ฎ์–ด์“ฐ์ž…๋‹ˆ๋‹ค.
  • lint ๋ฃฐ์— ์ƒ์„ฑ ํŒŒ์ผ์„ ์ œ์™ธํ•œ๋‹ค โ€” analysis_options.yaml์— exclude:๋กœ **/*.g.dart, **/*.freezed.dart๋ฅผ ๋ฐ•์Šต๋‹ˆ๋‹ค.
  • .gitignore์— ๋„ฃ์ง€ ์•Š๋Š”๋‹ค โ€” ์ผ๋ฐ˜์ ์œผ๋กœ ์ƒ์„ฑ ํŒŒ์ผ์„ ์ปค๋ฐ‹์— ํฌํ•จ์‹œํ‚ค๋Š” ๊ฒŒ ๊ถŒ์žฅ๋ฉ๋‹ˆ๋‹ค. CI์—์„œ build_runner ์‹คํŒจ๊ฐ€ ๋จธ์ง€ ํ›„ ๋ฐœ๊ฒฌ๋˜๋Š” ๊ฒƒ๋ณด๋‹ค ์‚ฌ์ „์— ์žกํžˆ๋Š” ๊ฒŒ ์•ˆ์ „ํ•ฉ๋‹ˆ๋‹ค.

4. ์ข…ํ•ฉ โ€” ์บก์Šคํ†ค์—์„œ์˜ ๋ฉ”ํƒ€ ํŒจํ„ด ํ™œ์šฉ ํŒจํ„ด

์ด ์„ธ ๊ฐ€์ง€๋ฅผ ํ•ฉ์ณ ์บก์Šคํ†ค์—์„œ ์ž์ฃผ ๋“ฑ์žฅํ•œ ํŒจํ„ด์„ ์ •๋ฆฌํ•˜๋ฉด ๋‹ค์Œ๊ณผ ๊ฐ™์Šต๋‹ˆ๋‹ค.

A. ๋ชจ๋ธ ์ •์˜์˜ ํ‘œ์ค€ ํ…œํ”Œ๋ฆฟ

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import 'package:freezed_annotation/freezed_annotation.dart';

part 'meal_entry.freezed.dart';
part 'meal_entry.g.dart';

@freezed
class MealEntry with _$MealEntry {
  const factory MealEntry({
    required String id,
    required String name,
    required int calories,
    required int sodium,
    required DateTime takenAt,
  }) = _MealEntry;

  factory MealEntry.fromJson(Map<String, dynamic> json) =>
      _$MealEntryFromJson(json);
}

B. Controller ์ •์˜์˜ ํ‘œ์ค€ ํ…œํ”Œ๋ฆฟ (Riverpod codegen)

1
2
3
4
5
6
7
8
9
10
11
12
13
@riverpod
class DietController extends _$DietController {
  @override
  AsyncValue<DietDay> build() => const AsyncLoading();

  Future<void> load() async {
    final result = await ref.read(dietRepositoryProvider).fetchToday();
    state = switch (result) {
      Ok(:final value) => AsyncData(value),
      Err(:final error) => AsyncError(error, StackTrace.current),
    };
  }
}

C. ๋„๋ฉ”์ธ ์œ ํ‹ธ๋ฆฌํ‹ฐ๋Š” extension์œผ๋กœ

1
2
3
4
extension DietDayX on DietDay {
  bool get isSodiumOverLimit => sodium > 2000;
  int get mealCount => meals.length;
}

D. ์œ„์ ฏ ์ƒํƒœ ๋ผ์ดํ”„์‚ฌ์ดํด ํ›…์€ mixin์œผ๋กœ

1
2
3
4
class _DashboardPageState extends ConsumerState<DashboardPage>
    with WidgetsBindingObserver {
  // ...
}

๊ฐ ๋„๊ตฌ๊ฐ€ ์ž๊ธฐ ์ž˜ ํ•˜๋Š” ์ž๋ฆฌ์— ๋“ค์–ด๊ฐ€๋‹ˆ, ๋ชจ๋ธ / ์ปจํŠธ๋กค๋Ÿฌ / ์œ ํ‹ธ / ๋ผ์ดํ”„์‚ฌ์ดํด์ด ๊น”๋”ํ•˜๊ฒŒ ๋ถ„๋ฆฌ๋ฉ๋‹ˆ๋‹ค.

5. ํ•™๊ธฐ ํšŒ๊ณ  โ€” Dart๋ฅผ ํ•œ ํ•™๊ธฐ ์จ๋ณธ ์†Œ๊ฐ

์ด๋ฒˆ ํ•™๊ธฐ ์บก์Šคํ†ค์„ ์ง„ํ–‰ํ•˜๋ฉด์„œ ์ƒˆ ์–ธ์–ด๋ฅผ ์ฒ˜์Œ๋ถ€ํ„ฐ ์ตํžˆ๋Š” ๊ฒŒ ๋ถ€๋‹ด์ด๊ธด ํ–ˆ์ง€๋งŒ, Dart๋Š” ๋‹ค์Œ ์„ธ ๊ฐ€์ง€ ๋ฉด์—์„œ ์บก์Šคํ†ค์— ์ž˜ ๋งž๋Š” ์–ธ์–ด์˜€์Šต๋‹ˆ๋‹ค.

  1. TypeScript ๊ฒฝํ—˜์„ ๊ทธ๋Œ€๋กœ ๊ฐ€์ ธ๊ฐˆ ์ˆ˜ ์žˆ์—ˆ์Šต๋‹ˆ๋‹ค. ๋น„๋™๊ธฐ ๋ชจ๋ธ, async/await, ํด๋กœ์ € ๋“ฑ์ด ๊ฑฐ์˜ ๊ฐ™์•„์„œ ์ง„์ž…์ด ๋นจ๋ž์Šต๋‹ˆ๋‹ค.
  2. Sound null safety์™€ sealed class๊ฐ€ ๋„๋ฉ”์ธ ๋ชจ๋ธ๋ง์— ์ž˜ ๋งž์•˜์Šต๋‹ˆ๋‹ค. ํ—ฌ์Šค์ผ€์–ด ๋ฐ์ดํ„ฐ์ฒ˜๋Ÿผ ๋ณ€ํ˜•์ด ๋งŽ์€ ๋„๋ฉ”์ธ์—์„œ ์ปดํŒŒ์ผ๋Ÿฌ๊ฐ€ ์žก์•„์ฃผ๋Š” ์•ˆ์ „๋ง์ด ํฝ๋‹ˆ๋‹ค.
  3. build_runner ๊ธฐ๋ฐ˜ ์ฝ”๋“œ ์ƒ์„ฑ์œผ๋กœ ๋ณด์ผ๋Ÿฌํ”Œ๋ ˆ์ดํŠธ๊ฐ€ ๊ฑฐ์˜ ์—†์—ˆ์Šต๋‹ˆ๋‹ค. ๋ชจ๋ธ / ์ปจํŠธ๋กค๋Ÿฌ ์ •์˜์— ์†์œผ๋กœ ์งœ๋Š” ์ฝ”๋“œ๊ฐ€ 5์ค„ ๋ฏธ๋งŒ์ธ ๊ฒฝ์šฐ๊ฐ€ ๋Œ€๋ถ€๋ถ„์ด์—ˆ์Šต๋‹ˆ๋‹ค.

๋‹ค์Œ ํ•™๊ธฐ์—๋Š” ์บก์Šคํ†ค ํ›„์†์œผ๋กœ ์‹ค์ œ ๋ฐฑ์—”๋“œ ์—ฐ๋™, ์นด๋ฉ”๋ผ ๊ถŒํ•œยท์ดฌ์˜, AI Coach ์‘๋‹ต์„ ์ง„์งœ API์— ๋ถ™์ด๋Š” ์ž‘์—…์ด ๋‚จ์•˜๋Š”๋ฐ, ๊ทธ๋• ๋˜ Dart์˜ ๋‹ค๋ฅธ ์˜์—ญ(Isolate, FFI ๋“ฑ)์„ ๊ณต๋ถ€ํ•ด์•ผ ํ•  ๋“ฏํ•ฉ๋‹ˆ๋‹ค. ํ•™์Šต ๋…ธํŠธ๋Š” ๊ทธ๋•Œ ๋‹ค์‹œ ์ด์–ด๊ฐ€๋ ค ํ•ฉ๋‹ˆ๋‹ค.


๐Ÿ“Œ ์š”์•ฝ

Dart์˜ ๋ฉ”ํƒ€ ํŒจํ„ด 3์ข…์„ ์ •๋ฆฌํ–ˆ์Šต๋‹ˆ๋‹ค. Extension methods๋Š” ๊ธฐ์กด ํƒ€์ž…์— ์ •์ ์œผ๋กœ ๋ฉ”์„œ๋“œ๋ฅผ ์ถ”๊ฐ€ํ•ด ์œ ํ‹ธ๋ฆฌํ‹ฐ๋ฅผ ๋„๋ฉ”์ธ ์นœํ™”์ ์œผ๋กœ ๋ฌถ์„ ์ˆ˜ ์žˆ๊ณ , Mixin์€ with ํ‚ค์›Œ๋“œ๋กœ ํ–‰์œ„/์ƒํƒœ๋ฅผ ํ•ฉ์„ฑํ•ด ๋‹ค์ค‘ ์ƒ์† ์—†์ด๋„ ๋ผ์ดํ”„์‚ฌ์ดํดยท๋กœ๊น…ยทํ‹ฐ์ปค ๊ฐ™์€ cross-cutting concern์„ ๊น”๋”ํžˆ ๋ถ„๋ฆฌํ•ฉ๋‹ˆ๋‹ค. build_runner ๊ธฐ๋ฐ˜ ์ฝ”๋“œ ์ƒ์„ฑ์€ freezed / json_serializable / riverpod_generator / drift_dev๋ฅผ ๊ฒฐํ•ฉํ•ด ๋ชจ๋ธยท์ปจํŠธ๋กค๋ŸฌยทDB ๋ณด์ผ๋Ÿฌํ”Œ๋ ˆ์ดํŠธ๋ฅผ ๊ฑฐ์˜ 0์ค„๋กœ ์ค„์—ฌ ์ค๋‹ˆ๋‹ค. ์บก์Šคํ†ค์—์„œ ์ด ์„ธ ๊ฐ€์ง€๊ฐ€ ๊ฐ์ž์˜ ์ž๋ฆฌ์—์„œ ํšจ์œจ์„ ๋งŒ๋“ค์–ด๋‚ด๋Š” ๊ฑธ ์ง์ ‘ ์ฒด๊ฐํ–ˆ์Šต๋‹ˆ๋‹ค.

This post is licensed under CC BY 4.0 by the author.