Skip to content

JSON 직렬화 (freezed, json_serializable)

REST API와 통신할 때 JSON 형식의 데이터를 주고받는 경우가 많습니다. Flutter/Dart에서는 이러한 JSON 데이터를 Dart 객체로 변환하고, 반대로 Dart 객체를 JSON으로 직렬화하는 과정이 필요합니다. 이 장에서는 json_serializablefreezed 패키지를 활용한 JSON 직렬화 방법을 살펴보겠습니다.

외부 API에서 데이터를 가져오면 보통 JSON 형태로 제공됩니다. 이를 그대로 사용하면 다음과 같은 문제가 발생합니다:

  1. 타입 안전성 부재: JSON은 동적 타입이므로 컴파일 시점에 오류를 발견하기 어렵습니다.
  2. 코드 자동 완성 불가: IDE에서 속성 이름을 자동 완성할 수 없습니다.
  3. 리팩토링 어려움: 속성 이름이 변경될 때 모든 참조를 업데이트하기 어렵습니다.
  4. 문서화 부족: 속성의 의미와 타입이 명확하게 정의되지 않습니다.

이러한 문제를 해결하기 위해 JSON을 Dart 클래스로 변환하는 과정이 필요합니다.

가장 기본적인 방법은 JSON 변환 코드를 직접 작성하는 것입니다:

class User {
final int id;
final String name;
final String email;
final DateTime createdAt;
User({
required this.id,
required this.name,
required this.email,
required this.createdAt,
});
// JSON에서 User 객체로 변환
factory User.fromJson(Map<String, dynamic> json) {
return User(
id: json['id'] as int,
name: json['name'] as String,
email: json['email'] as String,
createdAt: DateTime.parse(json['created_at'] as String),
);
}
// User 객체에서 JSON으로 변환
Map<String, dynamic> toJson() {
return {
'id': id,
'name': name,
'email': email,
'created_at': createdAt.toIso8601String(),
};
}
}

이 방법은 간단한 모델에는 잘 작동하지만, 다음과 같은 단점이 있습니다:

  1. 반복적인 코드: 비슷한 코드를 여러 모델마다 작성해야 합니다.
  2. 오류 가능성: 수동으로 작성하다 보면 타입 캐스팅이나 누락된 필드 등의 오류가 발생할 수 있습니다.
  3. 유지보수 어려움: 모델이 변경될 때마다 JSON 변환 코드도 수정해야 합니다.

json_serializable 패키지는 코드 생성을 통해 JSON 직렬화 코드를 자동으로 생성해 줍니다.

pubspec.yaml 파일에 필요한 패키지를 추가합니다:

dependencies:
json_annotation: ^4.8.1
dev_dependencies:
build_runner: ^2.4.6
json_serializable: ^6.7.1
import 'package:json_annotation/json_annotation.dart';
// 생성될 코드의 파일명을 지정합니다
part 'user.g.dart';
// 클래스에 어노테이션을 추가합니다
@JsonSerializable()
class User {
final int id;
final String name;
final String email;
// JSON 필드명이 Dart 속성명과 다른 경우 매핑
@JsonKey(name: 'created_at')
final DateTime createdAt;
User({
required this.id,
required this.name,
required this.email,
required this.createdAt,
});
// 팩토리 메서드는 생성된 코드를 호출합니다
factory User.fromJson(Map<String, dynamic> json) => _$UserFromJson(json);
// toJson 메서드도 생성된 코드를 호출합니다
Map<String, dynamic> toJson() => _$UserToJson(this);
}

다음 명령어로 코드를 생성합니다:

Terminal window
flutter pub run build_runner build

변경 사항을 감시하면서 지속적으로 코드를 생성하려면:

Terminal window
flutter pub run build_runner watch

기존 생성 파일과 충돌이 있을 경우:

Terminal window
flutter pub run build_runner build --delete-conflicting-outputs
@JsonSerializable()
class Product {
final int id;
final String name;
// 커스텀 변환기 사용
@JsonKey(
fromJson: _colorFromJson,
toJson: _colorToJson,
)
final Color color;
Product({
required this.id,
required this.name,
required this.color,
});
factory Product.fromJson(Map<String, dynamic> json) => _$ProductFromJson(json);
Map<String, dynamic> toJson() => _$ProductToJson(this);
// 커스텀 변환 함수
static Color _colorFromJson(String hex) => Color(int.parse(hex.substring(1), radix: 16) + 0xFF000000);
static String _colorToJson(Color color) => '#${color.value.toRadixString(16).substring(2)}';
}
@JsonSerializable(explicitToJson: true)
class Order {
final int id;
final DateTime orderDate;
final User customer; // 중첩 객체
final List<OrderItem> items; // 중첩 객체 리스트
Order({
required this.id,
required this.orderDate,
required this.customer,
required this.items,
});
factory Order.fromJson(Map<String, dynamic> json) => _$OrderFromJson(json);
Map<String, dynamic> toJson() => _$OrderToJson(this);
}
@JsonSerializable()
class OrderItem {
final int id;
final String productName;
final int quantity;
final double price;
OrderItem({
required this.id,
required this.productName,
required this.quantity,
required this.price,
});
factory OrderItem.fromJson(Map<String, dynamic> json) => _$OrderItemFromJson(json);
Map<String, dynamic> toJson() => _$OrderItemToJson(this);
}
@JsonSerializable()
class User {
final int id;
final String name;
final String email;
// API에서는 받지만 JSON으로 변환할 때는 제외
@JsonKey(includeToJson: false)
final String? authToken;
// JSON으로 변환할 때만 포함
@JsonKey(includeFromJson: false)
final bool isLoggedIn;
User({
required this.id,
required this.name,
required this.email,
this.authToken,
this.isLoggedIn = false,
});
factory User.fromJson(Map<String, dynamic> json) => _$UserFromJson(json);
Map<String, dynamic> toJson() => _$UserToJson(this);
}
@JsonSerializable()
class Settings {
// 기본값 설정
@JsonKey(defaultValue: 'light')
final String theme;
@JsonKey(defaultValue: true)
final bool notifications;
Settings({
required this.theme,
required this.notifications,
});
factory Settings.fromJson(Map<String, dynamic> json) => _$SettingsFromJson(json);
Map<String, dynamic> toJson() => _$SettingsToJson(this);
}
@JsonSerializable(includeIfNull: false) // null 값은 JSON에 포함하지 않음
class Profile {
final int id;
final String name;
// null일 수 있는 필드
final String? bio;
final String? avatarUrl;
// null일 경우 기본값 설정
@JsonKey(defaultValue: 0)
final int followersCount;
Profile({
required this.id,
required this.name,
this.bio,
this.avatarUrl,
required this.followersCount,
});
factory Profile.fromJson(Map<String, dynamic> json) => _$ProfileFromJson(json);
Map<String, dynamic> toJson() => _$ProfileToJson(this);
}

freezed는 불변(immutable) 모델 클래스를 위한 코드 생성 패키지로, 다음과 같은 기능을 제공합니다:

  1. 불변성: 객체가 생성된 후 변경할 수 없도록 합니다.
  2. copyWith: 기존 객체를 기반으로 일부 속성만 변경한 새 객체를 쉽게 생성합니다.
  3. 동등성 비교: == 연산자와 hashCode를 자동으로 구현합니다.
  4. 직렬화: json_serializable과 통합되어 JSON 직렬화를 지원합니다.
  5. 패턴 매칭: 다양한 타입의 데이터를 안전하게 처리할 수 있습니다.
  6. 공용체(union types): 여러 타입을 하나의 타입으로 그룹화할 수 있습니다.

pubspec.yaml에 다음 패키지를 추가합니다:

dependencies:
freezed_annotation: ^2.4.1
json_annotation: ^4.8.1
dev_dependencies:
build_runner: ^2.4.6
freezed: ^2.4.5
json_serializable: ^6.7.1
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:json_annotation/json_annotation.dart';
// 생성될 코드 파일명 지정
part 'user.freezed.dart';
part 'user.g.dart';
@freezed
class User with _$User {
const factory User({
required int id,
required String name,
required String email,
@JsonKey(name: 'created_at') required DateTime createdAt,
}) = _User;
factory User.fromJson(Map<String, dynamic> json) => _$UserFromJson(json);
}
Terminal window
flutter pub run build_runner build --delete-conflicting-outputs
final user = User(
id: 1,
name: '홍길동',
email: 'hong@example.com',
createdAt: DateTime.now(),
);
// 이름만 변경한 새 객체 생성
final updatedUser = user.copyWith(name: '김철수');
print(user.name); // '홍길동'
print(updatedUser.name); // '김철수'
final user1 = User(
id: 1,
name: '홍길동',
email: 'hong@example.com',
createdAt: DateTime.now(),
);
// 같은 값을 가진 새 객체
final user2 = User(
id: 1,
name: '홍길동',
email: 'hong@example.com',
createdAt: user1.createdAt,
);
print(user1 == user2); // true

5. Union Types (대수적 데이터 타입)

Section titled “5. Union Types (대수적 데이터 타입)”

freezed의 강력한 기능 중 하나는 Union Types입니다. 이를 통해 여러 가능한 상태나 변형을 한 클래스로 표현할 수 있습니다.

import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:json_annotation/json_annotation.dart';
part 'api_result.freezed.dart';
part 'api_result.g.dart';
@freezed
class ApiResult<T> with _$ApiResult<T> {
// 성공 상태
const factory ApiResult.success({
required T data,
}) = Success<T>;
// 에러 상태
const factory ApiResult.error({
required String message,
@Default(400) int statusCode,
}) = Error<T>;
// 로딩 상태
const factory ApiResult.loading() = Loading<T>;
factory ApiResult.fromJson(Map<String, dynamic> json) =>
_$ApiResultFromJson(json);
}
Future<ApiResult<User>> fetchUser(int id) async {
try {
// 로딩 상태 반환
ApiResult<User> loadingResult = ApiResult.loading();
// API 호출
final response = await dio.get('/users/$id');
// 성공 상태 반환
return ApiResult.success(data: User.fromJson(response.data));
} catch (e) {
// 에러 상태 반환
return ApiResult.error(message: '사용자 정보를 가져오는 데 실패했습니다');
}
}
// 위젯에서 사용
Widget build(BuildContext context) {
return FutureBuilder<ApiResult<User>>(
future: fetchUser(1),
builder: (context, snapshot) {
if (!snapshot.hasData) {
return CircularProgressIndicator();
}
// when 메서드로 패턴 매칭
return snapshot.data!.when(
success: (user) => UserInfoWidget(user: user),
error: (message, statusCode) => ErrorWidget(message: message),
loading: () => LoadingWidget(),
);
},
);
}
@freezed
class Settings with _$Settings {
const factory Settings({
@Default('light') String theme,
@Default(true) bool notifications,
@Default([]) List<String> favorites,
}) = _Settings;
factory Settings.fromJson(Map<String, dynamic> json) => _$SettingsFromJson(json);
}
@freezed
class Product with _$Product {
const factory Product({
required int id,
required String name,
@JsonKey(
fromJson: _colorFromJson,
toJson: _colorToJson,
)
required Color color,
}) = _Product;
factory Product.fromJson(Map<String, dynamic> json) => _$ProductFromJson(json);
// 정적 메서드는 클래스 바디에 정의
static Color _colorFromJson(String hex) => Color(int.parse(hex.substring(1), radix: 16) + 0xFF000000);
static String _colorToJson(Color color) => '#${color.value.toRadixString(16).substring(2)}';
}
@freezed
class Order with _$Order {
const factory Order({
required int id,
required DateTime orderDate,
required User customer,
required List<OrderItem> items,
}) = _Order;
factory Order.fromJson(Map<String, dynamic> json) => _$OrderFromJson(json);
}
@freezed
class OrderItem with _$OrderItem {
const factory OrderItem({
required int id,
required String productName,
required int quantity,
required double price,
}) = _OrderItem;
factory OrderItem.fromJson(Map<String, dynamic> json) => _$OrderItemFromJson(json);
}
@freezed
class User with _$User {
// 프라이빗 생성자로 검증 로직 추가
const factory User({
required int id,
required String name,
required String email,
}) = _User;
factory User.fromJson(Map<String, dynamic> json) => _$UserFromJson(json);
// named 생성자를 통해 팩토리 패턴 구현
factory User.create({
required int id,
required String name,
required String email,
}) {
// 이메일 유효성 검사
if (!_isValidEmail(email)) {
throw ArgumentError('Invalid email format');
}
return User(id: id, name: name, email: email);
}
// 프라이빗 헬퍼 메서드
static bool _isValidEmail(String email) {
return RegExp(r'^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$').hasMatch(email);
}
}

freezed와 json_serializable의 통합 활용

Section titled “freezed와 json_serializable의 통합 활용”

실제 앱에서는 freezed와 json_serializable을 함께 사용하여 강력한 모델 클래스를 만들 수 있습니다. 다음은 일반적인 사용 패턴입니다:

import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:json_annotation/json_annotation.dart';
part 'api_response.freezed.dart';
part 'api_response.g.dart';
@Freezed(genericArgumentFactories: true)
class ApiResponse<T> with _$ApiResponse<T> {
const factory ApiResponse({
required bool success,
String? message,
T? data,
@Default([]) List<String> errors,
}) = _ApiResponse<T>;
factory ApiResponse.fromJson(
Map<String, dynamic> json,
T Function(Object? json) fromJsonT,
) =>
_$ApiResponseFromJson(json, fromJsonT);
}
@freezed
class User with _$User {
const factory User({
required int id,
required String name,
required String email,
@JsonKey(name: 'created_at') required DateTime createdAt,
@Default([]) List<String> roles,
String? avatarUrl,
}) = _User;
factory User.fromJson(Map<String, dynamic> json) => _$UserFromJson(json);
}
@freezed
class Product with _$Product {
const factory Product({
required int id,
required String name,
required double price,
@Default(0) int stock,
String? description,
@Default([]) List<String> categories,
@Default(false) bool isFeatured,
}) = _Product;
factory Product.fromJson(Map<String, dynamic> json) => _$ProductFromJson(json);
}
class ApiService {
final Dio _dio;
ApiService(this._dio);
Future<ApiResponse<List<User>>> getUsers() async {
try {
final response = await _dio.get('/users');
return ApiResponse.fromJson(
response.data,
(json) => (json as List)
.map((item) => User.fromJson(item as Map<String, dynamic>))
.toList(),
);
} catch (e) {
return ApiResponse(
success: false,
message: '사용자 목록을 가져오는데 실패했습니다',
errors: [e.toString()],
);
}
}
Future<ApiResponse<User>> getUserById(int id) async {
try {
final response = await _dio.get('/users/$id');
return ApiResponse.fromJson(
response.data,
(json) => User.fromJson(json as Map<String, dynamic>),
);
} catch (e) {
return ApiResponse(
success: false,
message: '사용자 정보를 가져오는데 실패했습니다',
errors: [e.toString()],
);
}
}
}
class UserViewModel extends ChangeNotifier {
final ApiService _apiService;
UserViewModel(this._apiService);
ApiResult<List<User>> _users = ApiResult.loading();
ApiResult<List<User>> get users => _users;
Future<void> fetchUsers() async {
_users = ApiResult.loading();
notifyListeners();
try {
final response = await _apiService.getUsers();
if (response.success && response.data != null) {
_users = ApiResult.success(data: response.data!);
} else {
_users = ApiResult.error(
message: response.message ?? '알 수 없는 오류가 발생했습니다',
);
}
} catch (e) {
_users = ApiResult.error(message: e.toString());
}
notifyListeners();
}
}
class UsersScreen extends StatelessWidget {
@override
Widget build(BuildContext context) {
final viewModel = Provider.of<UserViewModel>(context);
return Scaffold(
appBar: AppBar(title: Text('사용자 목록')),
body: viewModel.users.when(
loading: () => Center(child: CircularProgressIndicator()),
success: (users) => ListView.builder(
itemCount: users.length,
itemBuilder: (context, index) {
final user = users[index];
return ListTile(
title: Text(user.name),
subtitle: Text(user.email),
trailing: Icon(Icons.arrow_forward_ios),
onTap: () => Navigator.pushNamed(
context,
'/user-details',
arguments: user.id,
),
);
},
),
error: (message, _) => Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text('오류: $message', style: TextStyle(color: Colors.red)),
SizedBox(height: 16),
ElevatedButton(
onPressed: () => viewModel.fetchUsers(),
child: Text('다시 시도'),
),
],
),
),
),
floatingActionButton: FloatingActionButton(
onPressed: () => viewModel.fetchUsers(),
child: Icon(Icons.refresh),
),
);
}
}

Todo 앱을 위한 데이터 모델을 freezedjson_serializable을 사용하여 구현해 보겠습니다:

todo.dart
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:json_annotation/json_annotation.dart';
part 'todo.freezed.dart';
part 'todo.g.dart';
@freezed
class Todo with _$Todo {
const factory Todo({
required String id,
required String title,
@Default('') String description,
@Default(false) bool completed,
@JsonKey(name: 'created_at') required DateTime createdAt,
@JsonKey(name: 'updated_at') DateTime? updatedAt,
@Default('normal') String priority,
@Default([]) List<String> tags,
}) = _Todo;
factory Todo.fromJson(Map<String, dynamic> json) => _$TodoFromJson(json);
// 추가 팩토리 메서드
factory Todo.create({
required String title,
String description = '',
String priority = 'normal',
List<String> tags = const [],
}) {
return Todo(
id: DateTime.now().millisecondsSinceEpoch.toString(),
title: title,
description: description,
createdAt: DateTime.now(),
priority: priority,
tags: tags,
);
}
}
// todo_status.dart
@freezed
class TodoStatus with _$TodoStatus {
const factory TodoStatus.initial() = _Initial;
const factory TodoStatus.loading() = _Loading;
const factory TodoStatus.loaded({required List<Todo> todos}) = _Loaded;
const factory TodoStatus.error({required String message}) = _Error;
}
// todo_repository.dart
class TodoRepository {
final Dio _dio;
TodoRepository(this._dio);
Future<List<Todo>> getTodos() async {
try {
final response = await _dio.get('/todos');
return (response.data as List)
.map((json) => Todo.fromJson(json as Map<String, dynamic>))
.toList();
} catch (e) {
throw Exception('할 일 목록을 가져오는데 실패했습니다: $e');
}
}
Future<Todo> createTodo(Todo todo) async {
try {
final response = await _dio.post(
'/todos',
data: todo.toJson(),
);
return Todo.fromJson(response.data as Map<String, dynamic>);
} catch (e) {
throw Exception('할 일을 생성하는데 실패했습니다: $e');
}
}
Future<Todo> updateTodo(Todo todo) async {
try {
final response = await _dio.put(
'/todos/${todo.id}',
data: todo.toJson(),
);
return Todo.fromJson(response.data as Map<String, dynamic>);
} catch (e) {
throw Exception('할 일을 업데이트하는데 실패했습니다: $e');
}
}
Future<void> deleteTodo(String id) async {
try {
await _dio.delete('/todos/$id');
} catch (e) {
throw Exception('할 일을 삭제하는데 실패했습니다: $e');
}
}
}
// todo_view_model.dart
class TodoViewModel extends ChangeNotifier {
final TodoRepository _repository;
TodoViewModel(this._repository);
TodoStatus _status = TodoStatus.initial();
TodoStatus get status => _status;
Future<void> fetchTodos() async {
_status = TodoStatus.loading();
notifyListeners();
try {
final todos = await _repository.getTodos();
_status = TodoStatus.loaded(todos: todos);
} catch (e) {
_status = TodoStatus.error(message: e.toString());
}
notifyListeners();
}
Future<void> createTodo(String title, String description) async {
try {
final newTodo = Todo.create(
title: title,
description: description,
);
await _repository.createTodo(newTodo);
await fetchTodos(); // 목록 새로고침
} catch (e) {
_status = TodoStatus.error(message: '할 일을 생성하는데 실패했습니다: ${e.toString()}');
notifyListeners();
}
}
Future<void> toggleTodoCompleted(Todo todo) async {
try {
final updatedTodo = todo.copyWith(
completed: !todo.completed,
updatedAt: DateTime.now(),
);
await _repository.updateTodo(updatedTodo);
await fetchTodos(); // 목록 새로고침
} catch (e) {
_status = TodoStatus.error(message: '할 일 상태를 변경하는데 실패했습니다: ${e.toString()}');
notifyListeners();
}
}
Future<void> deleteTodo(String id) async {
try {
await _repository.deleteTodo(id);
await fetchTodos(); // 목록 새로고침
} catch (e) {
_status = TodoStatus.error(message: '할 일을 삭제하는데 실패했습니다: ${e.toString()}');
notifyListeners();
}
}
}
// todo_screen.dart
class TodoScreen extends StatelessWidget {
@override
Widget build(BuildContext context) {
final viewModel = Provider.of<TodoViewModel>(context);
return Scaffold(
appBar: AppBar(title: Text('할 일 목록')),
body: viewModel.status.when(
initial: () {
// 초기 데이터 로드
WidgetsBinding.instance.addPostFrameCallback((_) {
viewModel.fetchTodos();
});
return Center(child: Text('데이터를 로드합니다...'));
},
loading: () => Center(child: CircularProgressIndicator()),
loaded: (todos) => todos.isEmpty
? Center(child: Text('할 일이 없습니다.'))
: ListView.builder(
itemCount: todos.length,
itemBuilder: (context, index) {
final todo = todos[index];
return TodoItem(
todo: todo,
onToggle: () => viewModel.toggleTodoCompleted(todo),
onDelete: () => viewModel.deleteTodo(todo.id),
);
},
),
error: (message) => Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text('오류: $message', style: TextStyle(color: Colors.red)),
SizedBox(height: 16),
ElevatedButton(
onPressed: () => viewModel.fetchTodos(),
child: Text('다시 시도'),
),
],
),
),
),
floatingActionButton: FloatingActionButton(
onPressed: () => _showAddTodoDialog(context, viewModel),
child: Icon(Icons.add),
),
);
}
void _showAddTodoDialog(BuildContext context, TodoViewModel viewModel) {
final titleController = TextEditingController();
final descriptionController = TextEditingController();
showDialog(
context: context,
builder: (context) => AlertDialog(
title: Text('새 할 일 추가'),
content: Column(
mainAxisSize: MainAxisSize.min,
children: [
TextField(
controller: titleController,
decoration: InputDecoration(labelText: '제목'),
),
TextField(
controller: descriptionController,
decoration: InputDecoration(labelText: '설명'),
maxLines: 3,
),
],
),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: Text('취소'),
),
ElevatedButton(
onPressed: () {
final title = titleController.text.trim();
final description = descriptionController.text.trim();
if (title.isNotEmpty) {
viewModel.createTodo(title, description);
Navigator.pop(context);
}
},
child: Text('추가'),
),
],
),
);
}
}
// todo_item.dart
class TodoItem extends StatelessWidget {
final Todo todo;
final VoidCallback onToggle;
final VoidCallback onDelete;
const TodoItem({
Key? key,
required this.todo,
required this.onToggle,
required this.onDelete,
}) : super(key: key);
@override
Widget build(BuildContext context) {
return Dismissible(
key: Key(todo.id),
background: Container(
color: Colors.red,
alignment: Alignment.centerRight,
padding: EdgeInsets.symmetric(horizontal: 16),
child: Icon(Icons.delete, color: Colors.white),
),
direction: DismissDirection.endToStart,
onDismissed: (_) => onDelete(),
child: ListTile(
title: Text(
todo.title,
style: TextStyle(
decoration: todo.completed ? TextDecoration.lineThrough : null,
color: todo.completed ? Colors.grey : null,
),
),
subtitle: todo.description.isNotEmpty ? Text(todo.description) : null,
leading: Checkbox(
value: todo.completed,
onChanged: (_) => onToggle(),
),
trailing: Row(
mainAxisSize: MainAxisSize.min,
children: [
if (todo.priority == 'high')
Icon(Icons.priority_high, color: Colors.red),
Text(
DateFormat.yMMMd().format(todo.createdAt),
style: TextStyle(fontSize: 12),
),
],
),
),
);
}
}
  • JSON 직렬화는 외부 API와 통신하는 Flutter 앱에서 필수적인 기능입니다.
  • 수동 직렬화는 간단한 모델에는 적합하지만, 모델이 복잡해질수록 코드 중복과 오류 가능성이 증가합니다.
  • json_serializable 패키지는 코드 생성을 통해 JSON 직렬화 코드를 자동으로 생성해 줍니다.
  • freezed 패키지는 불변성, 복사본 생성, 동등성 비교, JSON 직렬화, 유니온 타입 등 강력한 기능을 제공합니다.
  • freezed와 json_serializable의 통합을 통해 타입 안전하고 유지보수하기 쉬운 모델 클래스를 만들 수 있습니다.

이 장에서 배운 기술을 활용하면 API 통신 코드의 안정성과 유지보수성을 크게 향상시킬 수 있습니다. 특히 복잡한 데이터 구조를 다루는 대규모 앱에서는 이러한 코드 생성 도구가 필수적입니다.