Flutter(Dart) JSON and serialization (부제: flutter에서 풀옵션 모델 클래스 만들기)

다용도리모콘·2021년 9월 2일
0

Wiki - Flutter

목록 보기
4/7

참고: https://flutter.dev/docs/development/data-and-backend/json

JSON and serialization

모바일 앱에서 서버와 커뮤니케이션 하려면 필연적으로 JSON을 사용하게 된다. flutter에서 일반적으로 사용되는 방식은 다음과 같다.

  • 일반적인 serialization
  • 코드 제네레이터를 이용한 오토 serialization

간단한 형식의 JSON을 사용한다면 일반적인 serialization 방식으로 충분하겠지만 복잡하고 다양한 형식의 JSON을 encode, decode할 필요가 있다면(일반적으로 대부분의 프로덕션 서비스가 그렇지 않을까 싶지만) 코드 제네레이터를 사용하는 것이 편리하다.

Flutter에는 GSON/Jackson/Moshi 같은건 없나요?

없다.
이와 같은 라이브러리들은 flutter에서 지원하지 않는 런타임 리플렉션(runtime reflection)을 필요로 한다. 런타임 리플렉션은 Dart가 지원하는 tree shaking에 영향을 준다.

*tree shaking은 릴리스 빌드에서 사용하지 않는 코드를 제거해 앱의 크기를 최적화 해준다.

작은 프로젝트를 위한 기본적인 serialization

일반적인 JSON decoding에는 dart:convertjsonDecode() 메서드를 사용한다. 이 방법은 간단하고 빠르게 decoding할 수 있다는 장점이 있지만 decoding된 Map<String, dynamic> 타입을 다시 적절한 Data Type으로 변환하는 부분을 수작업으로 진행해야 하기 때문에 프로젝트가 커질수록 관리가 힘들어지고 실수를 할 확률이 높아진다.

inline에서 JSON serializing

제일 간단한 방식이지만 jsonDecode()는 Map<String, dynamic>을 리턴하기 때문에 이 값을 다시 적절한 타입의 객체로 변환하는 과정에서 오타 등의 문제가 발생할 수 있다.

//decoding에 성공하면 Map<String, dynamic> 타입이 리턴된다.
Map<String, dynamic> userMap = jsonDecode(jsonString);

//별도 타입의 객체를 만들고 싶다면 userMap의 값을 넣어 생성한다.
final user = User(name: userMap['name'], email: userMap['email']);

모델 클래스에서 JSON serializing

모델 클래스 내부에 fromJsontoJson을 정의해 두면 해당 메서드를 호출할 때마다 오타를 걱정할 필요가 없어진다. 다만 해당 메서드를 만드는 순간에는 여전히 오타의 위험성이 존재한다. 유닛테스트를 작성하는 것으로 걱정을 덜 수는 있겠지만 복잡한 모델 클래스가 생겨날 때마다 점점 더 관리하기 힘들어질 것이다.

class User {
  final String name;
  final String email;

  User(this.name, this.email);

  User.fromJson(Map<String, dynamic> json)
      : name = json['name'],
        email = json['email'];

  Map<String, dynamic> toJson() => {
        'name': name,
        'email': email,
      };
}

Map<String, dynamic> userMap = jsonDecode(jsonString);
var user = User.fromJson(userMap);

Code generation 라이브러리를 이용한 JSON serializing

여러가지 라이브러리가 있겠지만 여기서는 json_serializable을 사용해 보자.

Flutter Favorite을 받은 Code generation 라이브러리는 json_serializable, built_value 두 가지가 있다. json_serializable은 annotation을 통해 일반 클래스를 serializable하게 만들어주고, built_value는 불변 값 클래스(immutable value class)를 정의하는 높은 레벨의 방법을 JSON serialization과 함께 제공한다.

json_serializable 프로젝트에 셋팅하기

pubspec 파일에 아래의 라이브러리를 추가하고 flutter pub get 명령을 실행한다.

dependencies:
  # Your other regular dependencies here
  json_annotation: <latest_version>

dev_dependencies:
  # Your other dev_dependencies here
  build_runner: <latest_version>
  json_serializable: <latest_version>

모델 클래스 생성하기

import 'package:json_annotation/json_annotation.dart';

///클래스이름.g.dart 형식으로 작성해 놓으면 코드 제네레이터가 알아서 해당 파일을 생성하고 코드를 작성한다.
///build_runner 커맨드를 실행하기 전까지 빨간줄이 뜨겠지만 당황하지 말고 무시하자.
part 'user.g.dart';

///이 클래스에 JSON serialization 코드를 작성해 달라는 의미의 annotation.
()
class User {
  User(this.name, this.email);

  String name;
  String email;
  
  ///JSON key와 property 이름을 다르게 하고 싶다면 name에 JSON key를 설정한다.
  (name: 'registration_date_millis')
  final int registrationDateMillis;
  
  ///JSON에 key가 없거나 null일 때 설정하고 싶은 기본값이 있다면 설정한다.
  (defaultValue: false)
  final bool isAdult;
  
  ///true로 설정할 경우 code generator가 해당 property를 무시한다.
  (ignore: true)
  final String verificationCode;


  /// 새로운 모델 클래스를 만들어 주는 factory 생성자
  /// Map을 받아서 자동으로 generate된 _$UserFromJson에 넘겨주면 User를 리턴한다.
  factory User.fromJson(Map<String, dynamic> json) => _$UserFromJson(json);

  /// 자동으로 generate된 _$UserToJson 메서드를 통해 객체를 JSON으로 바꿔 리턴한다.
  Map<String, dynamic> toJson() => _$UserToJson(this);
}

Code Generator 실행하기

한 번 실행하기

프로젝트의 root에서 flutter pub run build_runner build를 실행한다.

코드를 변경할 때마다 자동으로 실행하기

flutter pub run build_runner watch를 실행한다.

중첩된 클래스에서 Code generate하기

중첩된 클래스(클래스의 property가 object인 경우)를 JSON으로 변환해 사용할 때 Invalid argument와 같은 에러를 경험할 수 있는데 다음과 같이 해결할 수 있다.

아래와 같은 두 모델 클래스가 있다고 가정하자.

import 'package:json_annotation/json_annotation.dart';
part 'address.g.dart';

()
class Address {
  String street;
  String city;

  Address(this.street, this.city);

  factory Address.fromJson(Map<String, dynamic> json) =>
      _$AddressFromJson(json);
  Map<String, dynamic> toJson() => _$AddressToJson(this);
}
import 'package:json_annotation/json_annotation.dart';

import 'address.dart';

part 'user.g.dart';

()
class User {
  User(this.name, this.address);

  String name;
  Address address;

  factory User.fromJson(Map<String, dynamic> json) => _$UserFromJson(json);
  Map<String, dynamic> toJson() => _$UserToJson(this);
}

build_runner를 통해 코드를 생성하고 user.toJson()을 실행 해보면 아래와 같이 출력될 것이다.

{name: John, address: Instance of 'address'}

하지만 우리가 원하는 것은 이러한 형태일 것이다.

{name: John, address: {street: My st., city: New York}}

이 결과를 얻기 위해서 모델 클래스 코드에 아래의 코드를 추가하면 된다.

import 'package:json_annotation/json_annotation.dart';

import 'address.dart';

part 'user.g.dart';

///explicitToJson을 true로 설정하면 원하는 결과를 얻을 수 있다.
(explicitToJson: true)
class User {
  User(this.name, this.address);

  String name;
  Address address;

  factory User.fromJson(Map<String, dynamic> json) => _$UserFromJson(json);
  Map<String, dynamic> toJson() => _$UserToJson(this);
}

보너스

Code Generator를 사용하면 json 문제는 해결 되지만 그것 외에도 모델 클래스에 필수적으로 필요한 메서드들을 매번 수작업으로 작성하는 것은 개발자에게는 고통스러운 일이다. 그럴 때 Dart Data Class Generator라고 하는 VSCode extension을 함께 쓰면 아주 편리하다. 이 extension을 사용하면 클래스 property만 작성하고 명령어를 실행하면 보일러 플레이트 코드를 자동으로 생성해준다.

이렇게 property만 작성하고 명령어(단축키)를 실행하면

class DataClass {
  final String property1;
  final int property2;
  final Map<String, dynamic> property3;
}

이렇게 필요한 메서드들이 자동으로 생성된다.

import 'package:flutter/foundation.dart';

class DataClass {
  final String property1;
  final int property2;
  final Map<String, dynamic> property3;
  DataClass({
    required this.property1,
    required this.property2,
    required this.property3,
  });

  DataClass copyWith({
    String? property1,
    int? property2,
    Map<String, dynamic>? property3,
  }) {
    return DataClass(
      property1: property1 ?? this.property1,
      property2: property2 ?? this.property2,
      property3: property3 ?? this.property3,
    );
  }

  
  String toString() => 'DataClass(property1: $property1, property2: $property2, property3: $property3)';

  
  bool operator ==(Object other) {
    if (identical(this, other)) return true;
  
    return other is DataClass &&
      other.property1 == property1 &&
      other.property2 == property2 &&
      mapEquals(other.property3, property3);
  }

  
  int get hashCode => property1.hashCode ^ property2.hashCode ^ property3.hashCode;
}

아마 android studio에도 유사한 extension이 있을 것이고, VSCode에도 더 좋은 extension이 있을지도 모르겠다.(아시면 추천 부탁드려요!) 단순 작업에 고통받고 있는 개발자가 있다면 이 팁이 도움이 되기를...

0개의 댓글