[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 | ์ฉ๋ |
|---|---|
ChangeNotifier | listener ํจํด (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 โ ์ธ์ ๋ฌด์์ ์ธ๊น
๊ฐ์ ๋ฏ ๋ค๋ฅธ ๋ ๊ธฐ๋ฅ์ ๋ค์ ๊ธฐ์ค์ผ๋ก ๊ตฌ๋ถํฉ๋๋ค.
| ๊ธฐ์ค | Extension | Mixin |
|---|---|---|
| ๋์ | ๊ธฐ์กด ํ์ (์์ ๋ถ๊ฐ) | ์ ํด๋์ค๋ฅผ ๋ง๋ค ๋ ํฉ์ฑ |
| ์ํ | ๊ฐ์ง ์ ์์ | ๊ฐ์ง ์ ์์ |
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.dart | sealed ๊ณ์ธต, ==, hashCode, copyWith, toString |
json_serializable | @JsonSerializable | *.g.dart | fromJson / toJson |
riverpod_generator | @riverpod | *.g.dart | Provider ์ ์ |
drift_dev | @DriftDatabase | *.g.dart | DB ํด๋์ค, 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๋ ๋ค์ ์ธ ๊ฐ์ง ๋ฉด์์ ์บก์คํค์ ์ ๋ง๋ ์ธ์ด์์ต๋๋ค.
- TypeScript ๊ฒฝํ์ ๊ทธ๋๋ก ๊ฐ์ ธ๊ฐ ์ ์์์ต๋๋ค. ๋น๋๊ธฐ ๋ชจ๋ธ, async/await, ํด๋ก์ ๋ฑ์ด ๊ฑฐ์ ๊ฐ์์ ์ง์ ์ด ๋นจ๋์ต๋๋ค.
- Sound null safety์ sealed class๊ฐ ๋๋ฉ์ธ ๋ชจ๋ธ๋ง์ ์ ๋ง์์ต๋๋ค. ํฌ์ค์ผ์ด ๋ฐ์ดํฐ์ฒ๋ผ ๋ณํ์ด ๋ง์ ๋๋ฉ์ธ์์ ์ปดํ์ผ๋ฌ๊ฐ ์ก์์ฃผ๋ ์์ ๋ง์ด ํฝ๋๋ค.
- 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์ค๋ก ์ค์ฌ ์ค๋๋ค. ์บก์คํค์์ ์ด ์ธ ๊ฐ์ง๊ฐ ๊ฐ์์ ์๋ฆฌ์์ ํจ์จ์ ๋ง๋ค์ด๋ด๋ ๊ฑธ ์ง์ ์ฒด๊ฐํ์ต๋๋ค.