JSON ์ง๋ ฌํ - json_serializable, freezed
์ง๋ ๊ธ์์ ์๋ฒ ์๋ต์ User.fromJson์ผ๋ก ๋ชจ๋ธ ํด๋์ค๋ก ๋ฐ๊ฟจ๋ค. ํ๋๊ฐ ๋์ธ ๊ฐ์ผ ๋๋ ๊ด์ฐฎ์๋ฐ, ์๋ฒ๊ฐ ์ฃผ๋ ํญ๋ชฉ์ด ์ด ๊ฐ, ์ค๋ฌด ๊ฐ๊ฐ ๋๋ฉด json['...'] as String์ ์์ผ๋ก ์ฐ๋ ์ผ์ด ์ ์ ๊ณ ํต์ค๋ฝ๋ค. ๊ฒ๋ค๊ฐ ์คํ๋ฅผ ๋ด๋ ์ปดํ์ผ๋ฌ๊ฐ ๋ชจ๋ฅธ๋ค. ์๋ฒ๊ฐ created_at์ด๋ผ๋ ์ด๋ฆ์ผ๋ก ๋ ์ง๋ฅผ ์ฃผ๋ ๊ฒฝ์ฐ๋ฅผ ์ง์ ์จ๋ณด์.
class User {
final int id;
final String name;
final DateTime createdAt;
User({required this.id, required this.name, required this.createdAt});
factory User.fromJson(Map<String, dynamic> json) {
return User(
id: json['id'] as int,
name: json['name'] as String,
createdAt: DateTime.parse(json['createdAt'] as String), // ์๋ฒ๋ created_at์ผ๋ก ์ค
);
}
}์ปดํ์ผ์ ํต๊ณผํ๋ค. ๊ทธ๋ฐ๋ฐ ์คํํ๋ฉด ์ด ์ค์์ ์ฃฝ๋๋ค.
type 'Null' is not a subtype of type 'String' in type castJSON์๋ createdAt์ด๋ผ๋ ํค๊ฐ ์๊ณ created_at๋ง ์์ด์ json['createdAt']์ด null์ด๊ณ , ๊ทธ๊ฑธ String์ผ๋ก ๋จ์ธ(as String)ํ๋ค๊ฐ ํฐ์ง๋ค. ๋ชจ๋ธ์ ์ฐ๋ฉด ์คํ๋ฅผ ์ปดํ์ผ๋ฌ๊ฐ ์ก์์ค๋ค๊ณ ํ๋๋ฐ, JSON ํค๋ฅผ ๋ฌธ์์ด๋ก ์ ๋ ์ด ์๋ฆฌ๋งํผ์ ์ฌ์ ํ ์คํ๊ฐ ์คํ ์ค์์ผ ๋๋ฌ๋๋ค.
์์ธ: ๋ณํ ์ฝ๋๋ฅผ ์ฌ๋์ด ์ง์ ์ฐ๊ณ ์๋ค
json['ํค']์ ํค๋ ๊ทธ๋ฅ ๋ฌธ์์ด์ด๋ผ ์ปดํ์ผ๋ฌ๊ฐ ๊ฒ์ฌํ ์ ์๋ค. ํ๋๊ฐ ๋์๋ก ์ด๋ฐ ์ค์ด ๋์ด๋๊ณ , ์๋ฒ ํ๋ ์ด๋ฆ์ด ๋ฐ๋๋ฉด ์ผ์ผ์ด ์ฐพ์ ๊ณ ์ณ์ผ ํ๋ค. ์ด ๋ฐ๋ณต ์์
์ ์ฝ๋ ์์ฑ๊ธฐ๊ฐ ๋์ ์จ์ฃผ๊ฒ ํ๋ ๊ฒ json_serializable์ด๋ค.
ํด๊ฒฐ 1: json_serializable๋ก ๋ณํ ์ฝ๋๋ฅผ ์์ฑํ๋ค
์ค์น๋ถํฐ ํ๋ค. ์ฑ์ ๋ค์ด๊ฐ๋ ๊ฑด json_annotation์ด๊ณ , ์ฝ๋๋ฅผ ๋ง๋๋ ๋๊ตฌ ๋ ๊ฐ๋ ๊ฐ๋ฐ ๋๋ง ์ฐ๋ dev ์์กด์ฑ์ด๋ค.
flutter pub add json_annotation
flutter pub add dev:json_serializable
flutter pub add dev:build_runnerํด๋์ค์ ์ด๋ ธํ ์ด์ ์ ๋ถ์ธ๋ค.
import 'package:json_annotation/json_annotation.dart';
part 'user.g.dart'; // ์์ฑ๋ ํ์ผ์ ์ด ํ์ผ์ ์ผ๋ถ๋ก ์ ์ธ
@JsonSerializable(fieldRename: FieldRename.snake)
class User {
final int id;
final String name;
final DateTime createdAt; // JSON์ created_at๊ณผ ์ฐ๊ฒฐ๋จ
User({required this.id, required this.name, required this.createdAt});
factory User.fromJson(Map<String, dynamic> json) => _$UserFromJson(json);
Map<String, dynamic> toJson() => _$UserToJson(this);
}@JsonSerializable(): "์ด ํด๋์ค์ ๋ณํ ์ฝ๋๋ฅผ ๋ง๋ค์ด๋ฌ๋ผ"๋ ํ์๋คfieldRename: FieldRename.snake: ํ๋ ์ด๋ฆcreatedAt์ JSON์์๋created_at์ผ๋ก ๋์์ํจ๋ค. ํ๋ ํ๋๋ง ๋ฐ๊พธ๋ ค๋ฉด ๊ทธ ํ๋์@JsonKey(name: 'created_at')์ ๋ถ์ธ๋คpart 'user.g.dart': ์์ฑ๋ ํ์ผ์ ์ด ํ์ผ์ ์ด์ด ๋ถ์ธ๋ค๋ ์ ์ธ์ด๋ค_$UserFromJson,_$UserToJson: ์์ฑ๊ธฐ๊ฐ ๋ง๋ค์ด์ค ํจ์๋ค. ์ง๊ธ์ ์์ง ์๋ค
์ด์ ์์ฑ๊ธฐ๋ฅผ ๋๋ฆฐ๋ค.
dart run build_runner build --delete-conflicting-outputsuser.g.dart ํ์ผ์ด ์๊ธฐ๊ณ _$UserFromJson์ด ์ฑ์์ง๋ค. ์ดํ๋ก๋ User.fromJson(response.data)๊ฐ JSON ํค๋ฅผ ์์์ ๋ง์ถฐ ๋ณํํ๋ค. ํ๋๊ฐ ๋์ด๋ ์ด๋
ธํ
์ด์
์ด ๋ถ์ ํด๋์ค์ ํ๋๋ง ์ถ๊ฐํ๊ณ ๋ค์ ์์ฑํ๋ฉด ๋๋ค.
์ฝ์ง ํฌ์ธํธ: ์์ฑ๊ธฐ๋ฅผ ์ ๋๋ ธ๋๋ ๋นจ๊ฐ ์ค
The function '_$UserFromJson' isn't defined.์์ฑํ๊ธฐ ์ ์ด๊ฑฐ๋, part 'user.g.dart'; ์ค์ ๋นผ๋จน์๊ฑฐ๋, ํ๋๋ฅผ ๊ณ ์ณ๋๊ณ ์์ฑ๊ธฐ๋ฅผ ๋ค์ ์ ๋๋ ธ์ ๋ ๋์ค๋ ์๋ฌ๋ค. ์์ฑ๋ ํ์ผ์ ์๋ณธ๊ณผ ๋ณ๊ฐ๋ผ์ ์ด๋
ธํ
์ด์
ํด๋์ค๋ฅผ ๊ณ ์น๋ฉด ๋ค์ ์์ฑํด์ผ ํ๋ค. ๊ณ ์น ๋๋ง๋ค ๋ช
๋ น์ ์น๊ธฐ ๋ฒ๊ฑฐ๋ก์ฐ๋ฉด ๊ฐ์ ๋ชจ๋๋ฅผ ์ผ๋๋ค.
dart run build_runner watch -dํ์ผ์ด ๋ฐ๋ ๋๋ง๋ค ์๋์ผ๋ก ๋ค์ ์์ฑํ๋ค.
๋ค์ ๋ฌธ์ : ๋ชจ๋ธ์ด ๊ฐ์์ง ๋น๊ตํ๋ฉด false๊ฐ ๋์จ๋ค
json_serializable์ด fromJson, toJson์ ๋ง๋ค์ด์ฃผ์ง๋ง, ๋ชจ๋ธ์๋ ์์ง ๋ถํธํ ์ ์ด ๋จ์ ์๋ค.
final now = DateTime.now();
final a = User(id: 1, name: 'ํ๊ธธ๋', createdAt: now);
final b = User(id: 1, name: 'ํ๊ธธ๋', createdAt: now);
print(a == b); // false๋ด์ฉ์ด ๋๊ฐ์๋ฐ false๋ค. ํ๋ฒํ ํด๋์ค์ ==๋ ๊ฐ์ ๊ฐ์ฒด์ธ์ง(์ฃผ์๊ฐ ๊ฐ์์ง)๋ง ๋น๊ตํ๊ธฐ ๋๋ฌธ์ด๋ค. ์ด๊ฒ ๋ฌธ์ ๊ฐ ๋๋ ๊ณณ์ด ๋ง๋ค. Cubit์์ ์ ์ํ๊ฐ ์ด์ ์ํ์ ๊ฐ์ผ๋ฉด ๋ฌด์๋๋ค๊ณ ํ๋๋ฐ, ์ด ๋น๊ต๊ฐ ==๋ค. ๋ด์ฉ์ด ๊ฐ์ ์ํ๋ฅผ ๋งค๋ฒ ์ ์ํ๋ก ์ทจ๊ธํด์ ํ๋ฉด์ด ๋ถํ์ํ๊ฒ ๋ค์ ๊ทธ๋ ค์ง๊ฑฐ๋, ๋ฐ๋๋ก Riverpod์์ ๊ฐ์ ๊ฐ์ผ๋ก ๊ฐฑ์ ํด๋ ์๋ฆผ์ด ๊ฐ๋ค.
==๋ฅผ ์ ๋๋ก ์ฐ๋ ค๋ฉด operator ==์ hashCode๋ฅผ ์ง์ ๋ง๋ค์ด์ผ ํ๊ณ , ๊ฐ์ ์ผ๋ถ๋ง ๋ฐ๊พผ ๋ณต์ฌ๋ณธ์ ๋ง๋๋ copyWith, ๋๋ฒ๊น
์ฉ toString๋ ํ์ํ๋ค. ํ๋๊ฐ ๋ ๋๋ง๋ค ์ด๊ฑธ ์ ๋ถ ๊ณ ์ณ์ผ ํ๋ค. ์ด ๋ณด์ผ๋ฌํ๋ ์ดํธ๊น์ง ์์ฑํด์ฃผ๋ ๊ฒ freezed๋ค.
ํด๊ฒฐ 2: freezed๋ก ==, copyWith, toString๊น์ง ํ ๋ฒ์
flutter pub add freezed_annotation
flutter pub add dev:freezedimport 'package:freezed_annotation/freezed_annotation.dart';
part 'user.freezed.dart';
part 'user.g.dart';
@freezed
abstract class User with _$User {
const factory User({
required int id,
required String name,
@JsonKey(name: 'created_at') required DateTime createdAt,
}) = _User;
factory User.fromJson(Map<String, Object?> json) => _$UserFromJson(json);
}ํ๋๋ฅผ factory ์์ฑ์์ ํ๋ผ๋ฏธํฐ๋ก ํ ๋ฒ๋ง ์ ๋๋ค. ํ๋ ์ ์ธ, ์์ฑ์, fromJson์ฉ ์ฝ๋๊ฐ ๋ชจ๋ ํ ๊ณณ์ ๋ชจ์ธ๋ค. ์์ฑ์ ๊ธ์์ ๋ณธ factory๊ฐ ์ฌ๊ธฐ์ ํต์ฌ์ผ๋ก ์ฐ์ธ๋ค.
abstract class User with _$User: ์์ ๊ธ์์ ๋ณธwith(mixin)๋ค. ์์ฑ๋_$User๋ฅผ ์์ด์==,copyWith,toString์ ์ป๋๋ค= _User: ์์ฑ๊ธฐ๊ฐ ๋ง๋ค์ด์ค ์ค์ ๊ตฌํ ํด๋์ค์ ์ฐ๊ฒฐํ๋คpartํ์ผ์ด ๋ ๊ฐ๋ค.user.freezed.dart๋ freezed๊ฐ,user.g.dart๋fromJson์ฉ json_serializable์ด ๋ง๋ ๋ค
๊ฐ์ ๋ช ๋ น์ผ๋ก ์์ฑํ๋ฉด ์ด์ ์ด๋ฐ ์ผ์ด ๋๋ค.
final a = User(id: 1, name: 'ํ๊ธธ๋', createdAt: now);
final b = User(id: 1, name: 'ํ๊ธธ๋', createdAt: now);
print(a == b); // true, ๋ด์ฉ์ด ๊ฐ์ผ๋ฉด ๊ฐ์ ๊ฐ
print(a.copyWith(name: '๊น์ฒ ์')); // ์ผ๋ถ๋ง ๋ฐ๊พผ ์ ๊ฐ์ฒด๋ฅผ ๋ง๋ค์ด์ค
print(a); // User(id: 1, name: ํ๊ธธ๋, createdAt: ...)copyWith๋ ์๋ณธ์ ๊ณ ์น์ง ์๊ณ ๋ฐ๊พผ ๋ถ๋ถ๋ง ๋ค๋ฅธ ์ ๊ฐ์ฒด๋ฅผ ๋๋ ค์ค๋ค. Cubit์์ ์ํ๋ฅผ ์ง์ ๊ณ ์น์ง ์๊ณ ์๋ก ๋ง๋ค์ด emitํ๋ ๊ท์น๊ณผ ์ ๋ง๋๋ค.
emit(state.copyWith(name: '๊น์ฒ ์'));์ฝ์ง ํฌ์ธํธ: ์์ ํํ ๋ฆฌ์ผ ์ฝ๋๊ฐ ์ ๋๋ค
freezed๋ 3.0๋ถํฐ ๊ท์น์ด ๋ฐ๋์ด์, ํด๋์ค๋ฅผ abstract๋ sealed๋ก ์ ์ธํด์ผ ํ๋ค. ์ธํฐ๋ท์ ์์ ๊ธ์ ๋ง์ด ๋ณด์ด๋ class User with _$User๋ฅผ ๊ทธ๋๋ก ๋ฐ๋ผ ์ฐ๋ฉด ๋งํ๋ค. ์๋ก ์ธ ๋๋ ์์ฒ๋ผ abstract class๋ก ์์ํ๋ค.
json_serializable๊ณผ ๊ฐ์ด ์ฐ๋ฉด @JsonKey๊ฐ ์๋ ์๋ฆฌ์์ invalid_annotation_target ๊ฒฝ๊ณ ๊ฐ ๋ฐ ์ ์๋ค. ์ด ๊ฒฝ๊ณ ๋ analysis_options.yaml์์ ๋๋ผ๊ณ ์๋ด๋์ด ์๋ค.
analyzer:
errors:
invalid_annotation_target: ignorefreezed๋ก ์ํ ๋ฌถ๊ธฐ: sealed
Bloc ๊ธ์์ sealed class๋ก ์ง์ ๋ง๋ ์ํ ๋ค ๊ฐ๋, freezed๋ก ==๊น์ง ํ ๋ฒ์ ์ป์ ์ ์๋ค.
@freezed
sealed class UserState with _$UserState {
const factory UserState.loading() = UserLoading;
const factory UserState.loaded(User user) = UserLoaded;
const factory UserState.error(String message) = UserError;
}์ํ๋ง๋ค ํด๋์ค๋ฅผ ๋ฐ๋ก ์ฐ๋ ์ฝ๋๊ฐ ํ ๋ฉ์ด๋ฆฌ๋ก ์ค๊ณ , ๊ฐ ์ํ๊ฐ ๋ด์ฉ์ด ๊ฐ์ผ๋ฉด ๊ฐ์ ๊ฐ์ผ๋ก ๋น๊ต๋๋ค.
Kotlin, Swift์ ๋น๊ตํ๋ฉด
// Kotlin - data class + kotlinx.serialization
@Serializable
data class User(
val id: Int,
val name: String,
@SerialName("created_at") val createdAt: String,
)// Swift - Codable
struct User: Codable {
let id: Int
let name: String
let createdAt: Date
enum CodingKeys: String, CodingKey {
case id, name
case createdAt = "created_at"
}
}- ๊ฐ ์ค์ฌ ํด๋์ค: Flutter
freezed/ Kotlindata class/ Swiftstruct - JSON ๋ณํ: Flutter๋
json_serializable์ด ์ฝ๋๋ฅผ ์์ฑ / Kotlin์@Serializable/ Swift๋Codable์ด ์๋ - ํค ์ด๋ฆ ๋์: Flutter
@JsonKey(name:)/ Kotlin@SerialName/ SwiftCodingKeys - ๊ฐ ๋น๊ต์ ๋ณต์ฌ: Flutter๋ freezed๊ฐ
==,copyWith์์ฑ / Kotlindata class๊ฐ ๊ธฐ๋ณธ ์ ๊ณต / Swiftstruct๋Equatable์ ๋ถ์ด๋ฉด ๋จ - ์์ฑ ๋จ๊ณ: Flutter๋ง
build_runner๋ฅผ ๋ฐ๋ก ๋๋ ค์ผ ํจ
Kotlin์ data class๊ฐ ==, copy, toString์ ์ธ์ด๊ฐ ๊ธฐ๋ณธ์ผ๋ก ์ฃผ๋ ๊ฒ์, Dart๋ freezed๊ฐ ์ฝ๋ ์์ฑ์ผ๋ก ํ๋ด ๋ธ๋ค. ๊ทธ๋์ Dart์์๋ build_runner๋ผ๋ ๋ณ๋ ๋จ๊ณ๊ฐ ์๊ธด๋ค.
์ ๋ฆฌ
json['ํค']๋ฅผ ์์ผ๋ก ์ฐ๋ฉด ํค ์คํ๊ฐ ์คํ ์ค์์ผ ํฐ์ง๋ฏ๋ก, ํด๋์ค์@JsonSerializable()์ ๋ถ์ฌ ๋ณํ ์ฝ๋๋ฅผ ์์ฑํ๊ฒ ํ๋ค- ์์ฑํ๋ ค๋ฉด
part 'ํ์ผ.g.dart'๋ฅผ ์ ์ธํ๊ณdart run build_runner build๋ฅผ ๋๋ฆฌ๋ฉฐ, ํ๋๋ฅผ ๊ณ ์น๋ฉด ๋ค์ ์์ฑํด์ผ ํ๋ค - ์๋ฒ์
snake_caseํค๋fieldRename: FieldRename.snake๋@JsonKey(name:)๋ก ๋์์ํจ๋ค - ํ๋ฒํ ํด๋์ค๋ ๋ด์ฉ์ด ๊ฐ์๋
==๊ฐfalse๋ผ์ ์ํ ๋น๊ต๊ฐ ์ด๊ธ๋๋ฏ๋ก,freezed๋ก==,copyWith,toString์ ์์ฑํ๋ค - freezed 3.0๋ถํฐ๋
abstract class๋sealed class๋ก ์ ์ธํ๊ณfactory์์ฑ์์ ํ๋๋ฅผ ์ ๋๋ค - Kotlin
data class, SwiftCodable์ด ๊ธฐ๋ณธ์ผ๋ก ์ฃผ๋ ๊ฑธ Dart๋ ์ฝ๋ ์์ฑ์ผ๋ก ์ป๋๋ค
๋ค์ ๊ธ์์๋ ์ฑ์ ๊ป๋ค ์ผ๋ ๋จ๋ ๋ฐ์ดํฐ๋ฅผ ์ ์ฅํ๋ ๋ก์ปฌ ์ ์ฅ์(shared_preferences, drift)๋ฅผ ๋ค๋ค๋ณด๊ฒ ๋ค.
