[Flutter] JsonCodable, 그게 뭔데? 1편

Cody Yun
Flutter Seoul
Published in
35 min readMay 16, 2024

구글 I/O 2024에서 플러터 3.22가 공개 되었습니다.

Landing Flutter 3.22 and Dart 3.4 at Google I/O 2024 by Michael Thomsen

다트의 3.4 업데이트에 포함된 macro가 기다리던 업데이트이기 다트와 플러터팀의 Product Manager인 Michael Thomsen이 작성한 “Announcing Dart 3.4” 포스팅에 대한 번역글을 포스팅 했었습니다.

다트 매크로 시스템

다트 매크로 시스템은 다트 언어가 정적 메타 프로그래밍을 지원하도록 설계된 개발 단계의 기능입니다. 다트팀은 매크로를 ‘정적’, ‘메타’, ‘프로그래밍’이라는 세 단어로 설명하고 있습니다. 여기서 ‘정적’은 컴파일 시점에 실행된다는 것을 나타내며, ‘메타’는 ‘프로그래밍’ 이라는 단어와 결합하여 프로그래밍 에 사용되는 코드를 생성하는 코드를 의미합니다. 이를 간단히 표현하면 ‘매크로는 컴파일 시점에 코드를 생성하는 코드’라는 의미를 가집니다.

모리우츠 코르넬리우스 에셔(M. C. Escher, 1898–1972)의 손을 그리는 손

컴파일 타임에 코드를 생성하기 때문에 코드 작성 시점에는 눈에 보이지 않아 학습 곡선이 있고, 디버깅이 어려우며, 컴파일 시간을 증가 시킨다는 단점이 있습니다. 이러한 단점에도 컴파일타임에 생성된 코드가 실행되기 때문에 성능 상 이점이 있고, 컴파일 과정에서 잘못된 코드를 검출해 프로그램의 안정성을 높입니다. 특히 코드를 만드는 코드를 통해 유연하고 재사용 가능한 코드를 작성한다는게 macro의 가장 큰 장점입니다.

이번에 배포된 다트 3.4에서 macro를 사용하기 위해서는 experimental 설정을 통해 활성화 해야 macro를 사용할 수 있는데, macro 사용을 위한 설정과 간단한 데모앱을 제작해보며 보다 자세히 살펴보겠습니다.

환경 설정하기

현재 macro는 preview 버젼이라 dart dev 채널이나 flutter master channel에서만 사용이 가능합니다. 사용하고 있는 채널을 확인하기 위해 터미널에서 flutter channel 이라 입력합니다.

cody@Codyui-MacBookPro deepdive_macro % flutter channel
Flutter channels:
master (latest development branch, for contributors)
main (latest development branch, follows master channel)
beta (updated monthly, recommended for experienced users)
* stable (updated quarterly, for new users and for production app releases)

사용하고 있는 채널에 * 표시가 되는데 제 개발 환경은 stable 채널을 사용하고 있다 알려줍니다. 채널을 master 채널로 변경하기 위해 flutter channel master라고 입력합니다.

cody@Codyui-MacBookPro ~ % flutter channel master
Switching to flutter channel 'master'...

Upgrading engine...
Downloading Darwin arm64 Dart SDK from Flutter engine 460df6caef0e9deee343af70ab7f478ee7a11a4f...
% Total % Received % Xferd Average Speed Time Time Time Current
Dload Upload Total Spent Left Speed

0 0 0 0 0 0 0 0 --:--:-- --:--:-- --:--:-- 0
0 0 0 0 0 0 0 0 --:--:-- --:--:-- --:--:-- 0
2 201M 2 5808k 0 0 5114k 0 0:00:40 0:00:01 0:00:39 5121k
5 201M 5 10.7M 0 0 5152k 0 0:00:39 0:00:02 0:00:37 5155k
8 201M 8 16.4M 0 0 5372k 0 0:00:38 0:00:03 0:00:35 5375k
13 201M 13 26.9M 0 0 6690k 0 0:00:30 0:00:04 0:00:26 6692k
18 201M 18 37.2M 0 0 7424k 0 0:00:27 0:00:05 0:00:22 7665k
22 201M 22 44.4M 0 0 7413k 0 0:00:27 0:00:06 0:00:21 7934k
27 201M 27 54.6M 0 0 7836k 0 0:00:26 0:00:07 0:00:19 8982k
31 201M 31 62.4M 0 0 7857k 0 0:00:26 0:00:08 0:00:18 9414k
35 201M 35 72.2M 0 0 8098k 0 0:00:25 0:00:09 0:00:16 9262k
40 201M 40 80.7M 0 0 8154k 0 0:00:25 0:00:10 0:00:15 8905k
44 201M 44 90.2M 0 0 8300k 0 0:00:24 0:00:11 0:00:13 9388k
48 201M 48 97.3M 0 0 8211k 0 0:00:25 0:00:12 0:00:13 8745k
53 201M 53 107M 0 0 8370k 0 0:00:24 0:00:13 0:00:11 9206k
57 201M 57 114M 0 0 8305k 0 0:00:24 0:00:14 0:00:10 8684k
61 201M 61 123M 0 0 8389k 0 0:00:24 0:00:15 0:00:09 8865k
65 201M 65 131M 0 0 8359k 0 0:00:24 0:00:16 0:00:08 8492k
70 201M 70 140M 0 0 8427k 0 0:00:24 0:00:17 0:00:07 8950k
74 201M 74 149M 0 0 8435k 0 0:00:24 0:00:18 0:00:06 8608k
76 201M 76 153M 0 0 8239k 0 0:00:24 0:00:19 0:00:05 8051k
80 201M 80 161M 0 0 8224k 0 0:00:25 0:00:20 0:00:05 7723k
84 201M 84 170M 0 0 8247k 0 0:00:24 0:00:21 0:00:03 7884k
89 201M 89 179M 0 0 8293k 0 0:00:24 0:00:22 0:00:02 7835k
93 201M 93 187M 0 0 8320k 0 0:00:24 0:00:23 0:00:01 7900k
97 201M 97 195M 0 0 8309k 0 0:00:24 0:00:24 --:--:-- 8579k
100 201M 100 201M 0 0 8214k 0 0:00:25 0:00:25 --:--:-- 8175k
Building flutter tool...
Resolving dependencies...
Downloading packages...
Got dependencies.
Downloading android-arm-profile/darwin-x64 tools... 1,080ms
Downloading android-arm-release/darwin-x64 tools... 450ms
Downloading android-arm64-profile/darwin-x64 tools... 594ms
Downloading android-arm64-release/darwin-x64 tools... 627ms
Downloading android-x64-profile/darwin-x64 tools... 472ms
Downloading android-x64-release/darwin-x64 tools... 386ms
Downloading android-x86 tools... 3.3s
Downloading android-x64 tools... 2,500ms
Downloading android-arm tools... 1,705ms
Downloading android-arm-profile tools... 1,162ms
Downloading android-arm-release tools... 1,286ms
Downloading android-arm64 tools... 3.4s
Downloading android-arm64-profile tools... 817ms
Downloading android-arm64-release tools... 861ms
Downloading android-x64-profile tools... 870ms
Downloading android-x64-release tools... 847ms
Downloading android-x86-jit-release tools... 1,255ms
Downloading ios tools... 14.4s
Downloading ios-profile tools... 9.6s
Downloading ios-release tools... 7.3s
Downloading Web SDK... 5.8s
Downloading package sky_engine... 264ms
Downloading flutter_patched_sdk tools... 521ms
Downloading flutter_patched_sdk_product tools... 526ms
Downloading darwin-arm64 tools... 4.6s
Downloading darwin-x64/framework tools... 4.2s
Downloading darwin-x64/gen_snapshot tools... 689ms
Downloading darwin-x64-profile/framework tools... 2,024ms
Downloading darwin-x64-profile tools... 440ms
Downloading darwin-x64-profile/gen_snapshot tools... 735ms
Downloading darwin-x64-release/framework tools... 1,736ms
Downloading darwin-x64-release tools... 414ms
Downloading darwin-x64-release/gen_snapshot tools... 849ms
Downloading darwin-arm64/font-subset tools... 306ms
Successfully switched to flutter channel 'master'.
To ensure that you're on the latest build from this channel, run 'flutter upgrade'

설정이 잘 됐는지 확인하기 위해 터미널에서 플러터와 다트 버젼을 확인합니다. macro는 다트 버젼 3.5.0–152 이상에서 사용할 수 있습니다.

cody@Codyui-MacBookPro ~ % flutter channel
Flutter channels:
* master (latest development branch, for contributors)
main (latest development branch, follows master channel)
beta (updated monthly, recommended for experienced users)
stable (updated quarterly, for new users and for production app releases)
cody@Codyui-MacBookPro ~ %
cody@Codyui-MacBookPro ~ % flutter --version
Flutter 3.22.0-35.0.pre.33 • channel master • https://github.com/flutter/flutter.git
Framework • revision 82fb5db893 (2 hours ago) • 2024-05-16 06:13:24 -0400
Engine • revision 460df6caef
Tools • Dart 3.5.0 (build 3.5.0-160.0.dev) • DevTools 2.36.0-dev.10
cody@Codyui-MacBookPro ~ %
cody@Codyui-MacBookPro ~ % dart --version
Dart SDK version: 3.5.0-160.0.dev (dev) (Wed May 15 17:06:58 2024 -0700) on "macos_arm64"

JsonCodable 데모용 프로젝트로 사용할 deepdive_macro 프로젝트를 생성하고, dart pub add json으로 JsonCodable을 제공하는 패키지를 추가합니다.

cody@Codyui-MacBookPro ~ % flutter create deepdive_macro
Creating project deepdive_macro...
Resolving dependencies in `deepdive_macro`...
Downloading packages...
Got dependencies in `deepdive_macro`.
Wrote 129 files.

All done!
You can find general documentation for Flutter at: https://docs.flutter.dev/
Detailed API documentation is available at: https://api.flutter.dev/
If you prefer video documentation, consider: https://www.youtube.com/c/flutterdev

In order to run your application, type:

$ cd deepdive_macro
$ flutter run

Your application code is in deepdive_macro/lib/main.dart.

cody@Codyui-MacBookPro ~ %
cody@Codyui-MacBookPro ~ % cd deepdive_macro
cody@Codyui-MacBookPro deepdive_macro % dart pub add json
Resolving dependencies...
Downloading packages...
+ _macros 0.1.5 from sdk dart
+ json 0.20.1
+ macros 0.1.0-main.5
meta 1.14.0 (1.15.0 available)
Changed 3 dependencies!
1 package has newer versions incompatible with dependency constraints.
Try `dart pub outdated` for more information.

JsonCodable 사용해 다트 패키지 json을 추가하면, _macros 0.1.5, macros 0.1.0-main.5, json 0.20.1 패키지가 추가됩니다. 다음으로 macros 사용을 위해 코드 애널라이저 설정 파일인 analysis_options.yaml 파일에 macros를 추가합니다.

# ./analysis_options.yaml 파일

include: package:flutter_lints/flutter.yaml

linter:
rules:

analyzer:
enable-experiment:
- macros

이제 macro 사용을 위한 환경 구성을 마쳤습니다. 환경 구성이 불편하긴 하지만 실 서비스에 실수로 실험적인 기능이 포함되어 빌드 안정성을 헤치는 상황을 생각해보면 납득이 됩니다.

JsonCodable 데모 프로젝트

JsonCodable은 Json 직렬화와 역직렬화 코드를 생성하는 macro입니다. Json 직렬화에 역직렬화에 사용하는 json_serializable 이라는 패키지가 있긴 하지만 차이점은 코드 생성 시점입니다. JsonCodable은 컴파일 타임에 코드를 생성하고, json_serializable은 build_runner를 통해 컴파일 이전에 코드를 생성합니다.

플러터 프로젝트 생성 시 자동으로 만들어지는 카운터 프로젝트에서 불필요한 주석과 코드를 제거했습니다.

import 'package:flutter/material.dart';

void main() {
runApp(const MyApp());
}

class MyApp extends StatelessWidget {
const MyApp({super.key});

@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
useMaterial3: true,
),
home: const MyHomePage(),
);
}
}

class MyHomePage extends StatefulWidget {
const MyHomePage({super.key});

@override
State<MyHomePage> createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
int _counter = 0;

void _incrementCounter() {
setState(() => _counter++);
}

@override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
const Text(
'You have pushed the button this many times:',
),
Text(
'$_counter',
style: Theme.of(context).textTheme.headlineMedium,
),
],
),
),
floatingActionButton: FloatingActionButton(
onPressed: _incrementCounter,
tooltip: 'Increment',
child: const Icon(Icons.add),
),
);
}
}

기본 프로젝트에서 _MyHomePageState의 _incrementCounter에서는 SharedPreferences로 json 포맷으로 저장하고, initState에서 SharedPreferences를 통해 json 포맷을 가져와 초기화하도록 변경하겠습니다. 이러한 구현을 위해 shared_preferences 패키지를 프로젝트에 추가합니다.

cody@Codyui-MacBookPro deepdive_macro % flutter pub add shared_preferences
Resolving dependencies...
Downloading packages... (1.0s)
+ ffi 2.1.2
+ file 7.0.0
+ flutter_web_plugins 0.0.0 from sdk flutter
meta 1.14.0 (1.15.0 available)
+ path_provider_linux 2.2.1
+ path_provider_platform_interface 2.1.2
+ path_provider_windows 2.2.1
+ platform 3.1.4
+ plugin_platform_interface 2.1.8
+ shared_preferences 2.2.3
+ shared_preferences_android 2.2.2
+ shared_preferences_foundation 2.4.0
+ shared_preferences_linux 2.3.2
+ shared_preferences_platform_interface 2.3.2
+ shared_preferences_web 2.3.0
+ shared_preferences_windows 2.3.2
+ web 0.5.1
+ win32 5.5.1
+ xdg_directories 1.0.4
Changed 18 dependencies!
1 package has newer versions incompatible with dependency constraints.
Try `flutter pub outdated` for more information.

기본 카운터 프로젝트에 JsonCodable과 SharedPreferences를 사용해 마지막 카운트 상태를 저장하고, 불러오기 위해 아래와 같이 구현했습니다.

import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:json/json.dart';
import 'package:shared_preferences/shared_preferences.dart';

void main() async {
WidgetsFlutterBinding.ensureInitialized();
final sharedPreferences = await SharedPreferences.getInstance();
runApp(MyApp(sharedPreferences: sharedPreferences));
}

class MyApp extends StatelessWidget {
final SharedPreferences sharedPreferences;
const MyApp({super.key, required this.sharedPreferences});

@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
useMaterial3: true,
),
home: MyHomePage(sharedPreferences: sharedPreferences),
);
}
}

class MyHomePage extends StatefulWidget {
final SharedPreferences sharedPreferences;
const MyHomePage({
super.key,
required this.sharedPreferences,
});

@override
State<MyHomePage> createState() => _MyHomePageState();
}

@JsonCodable()
class Counter {
int _count = 0;
int get count => _count;

Counter();

void increment() => _count++;
}

class _MyHomePageState extends State<MyHomePage> {
late Counter _counter;

@override
void initState() {
super.initState();
final counterJsonString = widget.sharedPreferences.getString('counter');
_counter = counterJsonString == null || counterJsonString.isEmpty
? Counter()
: Counter.fromJson(jsonDecode(counterJsonString));
}

void _incrementCounter() {
setState(() {
_counter.increment();
widget.sharedPreferences.setString(
'counter',
jsonEncode(_counter.toJson()),
);
});
}

@override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
const Text(
'You have pushed the button this many times:',
),
Text(
'${_counter.count}',
style: Theme.of(context).textTheme.headlineMedium,
),
],
),
),
floatingActionButton: FloatingActionButton(
onPressed: _incrementCounter,
tooltip: 'Increment',
child: const Icon(Icons.add),
),
);
}
}

변경된 부분을 하나씩 살펴봅시다. 먼저 SharedPreferences와 JsonCodable을 사용하기 위해 import를 합니다.

import 'package:json/json.dart';
import 'package:shared_preferences/shared_preferences.dart';

SharedPreferences는 사용을 위한 인스턴스를 가져오는 getInstance 메소드는 비동기 메소드라 main 함수를 async로 변경하고, MyApp과 MyHomePage의 생성자로 비동기로 가져온 SharedPreferences 객체의 인스턴스를 전달합니다.

void main() async {
WidgetsFlutterBinding.ensureInitialized();
final sharedPreferences = await SharedPreferences.getInstance();
runApp(MyApp(sharedPreferences: sharedPreferences));
}

class MyApp extends StatelessWidget {
final SharedPreferences sharedPreferences;
const MyApp({super.key, required this.sharedPreferences});

@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
useMaterial3: true,
),
home: MyHomePage(sharedPreferences: sharedPreferences),
);
}
}

class MyHomePage extends StatefulWidget {
final SharedPreferences sharedPreferences;
const MyHomePage({
super.key,
required this.sharedPreferences,
});

@override
State<MyHomePage> createState() => _MyHomePageState();
}

카운터 정보를 Json으로 저장하고 불러올 때 객체의 인스턴스를 직렬화 및 역직렬화 해야 하는데, 이를 위해 Counter 클래스를 선언하고, _count 속성과 이를 반환하는 getter, 값을 증가 시키는 increment 메소드를 구현합니다. 불변성은 이번 글의 주제가 아니기에 increment 메소드를 통해 값을 간단히 증가시켰습니다. 이후 Counter 클래스의 선언 앞에 @JsonCodable 매크로를 추가합니다.

@JsonCodable()
class Counter {
int _count = 0;
int get count => _count;

Counter();

void increment() => _count++;
}

JsonCodable 매크로를 지정한 Counter 객체를 _MyHomePageState의 late 프로퍼티로 추가하고, initState에서는 SharedPreferences의 getString으로 가져온 Json 포맷의 문자열을 Counter의 fromJson에 jsonDecode로 변환 후 전달합니다.

class _MyHomePageState extends State<MyHomePage> {
late Counter _counter;
@override
void initState() {
super.initState();
final counterJsonString = widget.sharedPreferences.getString('counter');
_counter = counterJsonString == null || counterJsonString.isEmpty
? Counter()
: Counter.fromJson(jsonDecode(counterJsonString));
}
// ...
}

_MyHomePageState의 _incrementCounter 메소드에서는 Counter 객체의 increment() 메소드를 호출해 값을 증가 시키고, SharedPreferences에 저장하기 위해 toJson() 메소드를 호출해 변환한 Map<String, Object?> 타입의 값을 jsonEncode에 전달해 문자열로 전환합니다.

class _MyHomePageState extends State<MyHomePage> {
late Counter _counter;
// ...
void _incrementCounter() {
setState(() {
_counter.increment();
widget.sharedPreferences.setString(
'counter',
jsonEncode(_counter.toJson()),
);
});
}
// ...
}

끝으로 _MyHomePageState의 build 메소드에서는 Text 위젯으로 _counter의 count 값을 표시합니다.

class _MyHomePageState extends State<MyHomePage> {
late Counter _counter;
// ...
@override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
const Text(
'You have pushed the button this many times:',
),
Text(
'${_counter.count}',
style: Theme.of(context).textTheme.headlineMedium,
),
],
),
),
floatingActionButton: FloatingActionButton(
onPressed: _incrementCounter,
tooltip: 'Increment',
child: const Icon(Icons.add),
),
);
}
}

이제 실행해 봅시다. 실행하면 아래와 같은 오류를 만나게 됩니다.

Launching lib/main.dart on Chrome in debug mode...
lib/main.dart:54:24: Error: Member not found: 'Counter.fromJson'.
_counter = Counter.fromJson(counterModelJsonString as Map<String, Object>);
^^^^^^^^
lib/main.dart:62:18: Error: The method 'toJson' isn't defined for the class 'Counter'.
- 'Counter' is from 'package:deepdive_macro/main.dart' ('lib/main.dart').
Try correcting the name to the name of an existing method, or defining a method named 'toJson'.
_counter.toJson().toString(),
^^^^^^
Failed to compile application.

Exited (1).

Counter의 fromJson, toJson 메소드는 어디에도 없는데도 컴파일 오류가 발생하지 않았지만, 실행 시점에는 fromJson, toJson 메소드를 Counter에서 찾을 수 없다는 오류가 발생했습니다. 다시 한번 더 강조하지만 macro는 preview 기능으로 제공하고 있습니다. 실행 시 enable-experiment=macros 옵션을 줘야 macro가 정상 동작합니다.

cody@Codyui-MacBookPro deepdive_macro % flutter run -d chrome --enable-experiment=macros lib/main.dart
Launching lib/main.dart on Chrome in debug mode...
Waiting for connection from debug service on Chrome... 7.0s
This app is linked to the debug service: ws://127.0.0.1:60809/PgOKrfH7cBI=/ws
Debug service listening on ws://127.0.0.1:60809/PgOKrfH7cBI=/ws

🔥 To hot restart changes while running, press "r" or "R".
For a more detailed help message, press "h". To quit, press "q".

A Dart VM Service on Chrome is available at: http://127.0.0.1:60809/PgOKrfH7cBI=
The Flutter DevTools debugger and profiler on Chrome is available at:
http://127.0.0.1:9101?uri=http://127.0.0.1:60809/PgOKrfH7cBI=

실행해 결과를 살펴봅시다.

Go to Augmentation

fromJson, toJson은 어디에 생성된걸까요? JsonCodable을 통해 컴파일 시점에 어떤 코드가 생성됐길래 실행된걸까요? VSCode에서 Counter 클래스의 선언부 상단에 있는 Go to Augmentation을 버튼을 클릭하면 우리의 궁금증을 해결할 수 있습니다.

‘Augmentation’은 증가, 증대, 증가물이라는 뜻으로 증강현실(AR)의 증강과 같은 단어입니다. 증강현실은 현실에 정보를 증강(추가) 시킨 것인데, 매크로 역시 증강현실처럼 작성된 코드에 증강(추가)한 결과물이라 Go to Augmentation 라는 메뉴가 표시된 것 입니다.

https://dic.daum.net/search.do?q=Augmentation

코드를 자세히 보면 dart.core를 의미하는 prefix0와 augment 라는 키워드를 빼고 보면 흔한 json 직렬화와 역직렬화 코드입니다.

augment library 'package:deepdive_macro/main.dart';

import 'dart:core' as prefix0;

augment class Counter {
external Counter.fromJson(prefix0.Map<prefix0.String, prefix0.Object?> json);
external prefix0.Map<prefix0.String, prefix0.Object?> toJson();
augment Counter.fromJson(prefix0.Map<prefix0.String, prefix0.Object?> json, )
: this._count = json['_count'] as prefix0.int;
augment prefix0.Map<prefix0.String, prefix0.Object?> toJson() {
final json = <prefix0.String, prefix0.Object?>{};
json['_count'] = this._count;
return json;
}
}
augment library 'package:deepdive_macro/main.dart';

import 'dart:core' as prefix0;

augment class Counter {
external Counter.fromJson(Map<String, Object?> json);
external Map<String, Object?> toJson();
augment Counter.fromJson(Map<String, Object?> json, )
: this._count = json['_count'] as int;
augment Map<String, Object?> toJson() {
final json = <String, Object?>{};
json['_count'] = this._count;
return json;
}
}

끝으로

이번 포스팅에서는 flutter master 채널을 통해 실험적으로 제공되는 JsonCodable을 통해 간단한 데모앱을 만들고, 매크로를 통해 동작 방식에 대해 살펴봤습니다. 메타 프로그래밍의 가능케 하는 매크로의 장점이 느껴지기에는 간단한 매크로이지만, build_runner 기반으로 코드를 생성하는 다양한 패키지에 매크로가 적용될 때 플러터 개발 경험이 어떻게 변할지 충분히 기대가 됩니다. 다음 포스팅에서는 JsonCodable 매크로를 클래스에 추가한 것 만으로 코드가 생성되는 원리를 살펴보도록 하겠습니다.

언제나 그렇듯 Happy Coding👨‍💻

--

--

Cody Yun
Flutter Seoul

I wanna be a full stack software engineer at the side of user-facing application.