Flutter와 Firebase로 Android iOS 둘 다 만들기 11 JSON 데이터 처리하기

3 분 소요

플러터로 JSON 데이터를 처리하는 방법을 다룹니다.

개요

플러터는 다트(dart)라는 언어를 사용합니다.

JSON(JavaScript Object Notation)이라는 데이터 형식은 결국 자스의 오브젝트형 변수인데 다트의 형(type)중에는 없습니다.

그래서 json으로 데이터를 주고 받을 때 항상 형변환을 신경쓰며 코딩해야되는 번거로움이 있습니다.

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

Map

json과 비슷한 형이 있는데 바로 Map이라는 형입니다.

키와 값을 가진 변수를 만들 수 있습니다.

값 만들기

Map<String, String> data = { 'ab': 'xxx' };
Map<int, int> datai = { 111: 333 };
Map<String, dynamic> datad = { 'dd': 1.23 };

키와 값에 모두 형을 써줘야 됩니다..

키나 값이 다양할 경우에는 dynamic이란 형으로 지정하면 됩니다.

값 가져오기

appBar: AppBar(
  title: Text(data['ab'] + ' ' + datano[111].toString() + ' ' +  datad['dd'].toString()),
),

이런 식으로 가져오는 것이 가능합니다.

만약 없는 키(datano[123].toString()) 를 사용하면 null이 출력됩니다.

아직

http로 가져와보기

지난번 만든 웹서버에서 get 요청을 할 때 json 형식으로 보내봅니다.

기본형

server

router
  .get('/', (ctx, next) => {
    ctx.body = { abc: 'test', def: 123 }
  })

client

final r = await http.get(url);
print(r.body); // {"abc":"test","def":123}

출력은 문자열로 나오게 됩니다.

값을 참조하기 위해서는 문자열을 맵형으로 변경해야합니다.(String to Map)

String to map

final v = jsonDecode(r.body);
print(v); // {abc: test, def: 123}
print(v['abc']); // test
print(v['xxx']); // null

jsonDecode 함수로 간단하게 맵형 자료가 되었습니다.

복잡한 데이터

사실 json의 장점은 유연함입니다. 배열형도 있을 수 있고 하위로 다른 오브젝트가 올수 있습니다.

만들어 놓고 테스트를 해봅니다.

server

.get('/', (ctx, next) => {
  const user = {
    email: 'a@a.aaa',
    name: 'namesss',
    address: {
      city: 'seoul',
      no: 333
    },
    friends: ['aaa', 'bbb']
  }
  ctx.body = user
})

client

final r = await http.get(url);
final v = jsonDecode(r.body);
print(v);
print(v['address']['city']); // seoul
print(v['friends'][1]); // bbb

예상했던 대로 잘 나옵니다.

하지만 상당히 가독성도 떨어지고 예외처리를 많이해야할 코드 입니다.

없는 주소를 사용하면 print(v['friends'][3]); 바로 에러가 납니다.

그래서 보통 이럴 때는 클래스를 만들어 관리하는 것이 좋습니다.

클래스로 테스트하기 만들기

위의 내용으로 클래스를 만들면..

class

class Address {
  String city;
  int no;
  Address(this.city, this.no);
}

class User {
  String email;
  String name;
  Address address;
  List<dynamic> friends;
  User(this.email, this.name, this.address, this.friends);
}

이렇게 만들 수 있습니다.

friends가 dynamic인 이유는 기본적으로 jsonDecode할 때 <String, dynamic> 으로 값들이 구성되기 때문입니다.

cient

Address address = Address(v['address']['city'], v['address']['no']);
User user = User(v['email'], v['name'], address, v['friends']);
print(user.email);
print(user.address.city);
print(user.friends[1]);

사용은 위와 같이 사용할 수 있습니다.

고작 쩜 하나 찍자고 요란합니다.

class 변환 로직 추가하기

저런 데이터를 다루는 클래스를 보통 모델이라 하는데 간단한 양방향 변환이 가능한 함수를 넣어서 관리하는 것이 편합니다.

class Address {
  String city;
  int no;
  Address(this.city, this.no);

  Address.fromJson(Map<String, dynamic> json)
      : city = json['city'],
        no = json['no'];

  Map<String, dynamic> toJson() =>
    {
      'city': city,
      'no': no,
    };
}

class User {
  String email;
  String name;
  Address address;
  List<dynamic> friends;
  User(this.email, this.name, this.address, this.friends);

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

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

사실 지저분한 코드를 정돈한 느낌으로만 보이지만 사용할때 편합니다.

client

User user = User.fromJson(v);

print(user.email);
print(user.address.city);
print(user.friends[1]);

사용하니 편리합니다~

JSON 직렬화 사용하기

위의 예제까지는 그럭저럭 쓸만합니다.

하지만 실제 운용해보면 훨씬 더 많은 필드들이 필요합니다.

그 때마다 저 반복적인 바보짓을 계속하는 것은 가슴이 답답해집니다.

class User {
  String email;
  String name;
  Address address;
  List<dynamic> friends;
  int age;
  List<dynamic> parents;
  int like;
  // more more

  User(this.email, this.name, this.address, this.friends, this.age, this.parents, this.like);

  User.fromJson(Map<String, dynamic> json)
      : name = json['name'],
        email = json['email'],
        address = Address.fromJson(json['address']),
        friends = json['friends'],
        age = json['age'],
        parents = json['parents'],
        like = json['like']; //,more

  Map<String, dynamic> toJson() =>
    {
      'name': name,
      'email': email,
      'address': address,
      'friends': friends,
      'age': age,
      'parents': parents,
      'like': like
      // more
    };
}

필드 하나 늘어날 때 마다 3줄은 기본이며.. 오타도 유심히 봐야하죠..

그래서 직렬화 유틸리티가 있습니다.

클래스 파일 만들기

직렬화 유틸리티를 사용하기 전에 모델들을 파일로 정리해야합니다.

User, Address 클래스를 각각의 파일로 저장합니다.

  • models/user.dart
  • models/address.dart

main.dart 상단에

import 'models/address.dart';
import 'models/user.dart';

해주고 잘 되는 지 확인하면 준비 끝입니다.

자스에 비해 불편한 점이 많지만, 이런 면(코드 재활용)은 상당히 편리합니다.

설치

pubspec.yaml

dependencies:
  # 다른 의존성들
  json_annotation: ^2.0.0

dev_dependencies:
  # 다른 개발 의존성들
  build_runner: ^1.0.0
  json_serializable: ^2.0.0

요렇게 3가지만 추가해줍니다.

직렬화 클래스 만들기

models/address.dart

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

@JsonSerializable()

class Address {
  String city;
  int no;
  Address(this.city, this.no);

  factory Address.fromJson(Map<String, dynamic> json) => _$AddressFromJson(json);
  Map<String, dynamic> toJson() => _$AddressToJson(this);
}

models/user.dart

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

@JsonSerializable()

class User {
  String email;
  String name;
  Address address;
  List<dynamic> friends;
  int age;
  List<dynamic> parents;
  int like;

  User(this.email, this.name, this.address, this.friends, this.age, this.parents, this.like);

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

클래스 명 기반으로 이제 자동으로 변환이 가능한 클래스입니다.

직렬화를 하고 나면 이제 필드값만 추가해도 알아서 코드를 작성해줍니다.(*.g.dart)

빌드하기

아직 사용이 안됩니다.

*.g.dart로 만들려면 명령어를 사용해야합니다.

한번만 하기

$ flutter pub run build_runner build

자동으로 추가 즉시 변환하기

$ flutter pub run build_runner watch

빌드를 매번 해야하는 번거로움이 있지만 필드 추가는 훨씬 간단해졌습니다.

요약

  • 변환: 문자(String) > 맵(Map<String, dynamic>) > 클래스(class)
  • 방법: jsonDecode -> fromJson

영상

댓글남기기