본문 바로가기
Flutter

[Flutter] JSON 다루기 (리스트, 중첩)

by 아마도개발자 2024. 7. 27.

 

먼저 JSON이 무엇인지 알아보자.

 

JSON 이란

- JavaSCript Object Notation(JSON)은 JavaScript 객체 문법으로 구조화된 데이터를 표현하기 위한 문자 기반의 표준 포맷

- 서버와 클라이언트 간의 통신에서 일반적으로 사용

- 자바스크립트의 문법과 굉장히 유사하지만 텍스트 형식

 

JSON 구조

{
  "squadName": "Super hero squad",
  "homeTown": "Metro City",
  "formed": 2016,
  "secretBase": "Super tower",
  "active": true,
  "members": [
    {
      "name": "Molecule Man",
      "age": 29,
      "secretIdentity": "Dan Jukes",
      "powers": ["Radiation resistance", "Turning tiny", "Radiation blast"]
    },
    {
      "name": "Madame Uppercut",
      "age": 39,
      "secretIdentity": "Jane Wilson",
      "powers": [
        "Million tonne punch",
        "Damage resistance",
        "Superhuman reflexes"
      ]
    },
    {
      "name": "Eternal Flame",
      "age": 1000000,
      "secretIdentity": "Unknown",
      "powers": [
        "Immortality",
        "Heat Immunity",
        "Inferno",
        "Teleportation",
        "Interdimensional travel"
      ]
    }
  ]
}

 

 

그럼 Flutter에서는 JSON을 어떻게 다룰 수 있을까? 앞서 말했듯 JSON은 텍스트 형태로 이루어져 있다. 아래 예시는 JSON형태의 텍스트로, JsonString이라고 불린다.

 

 

만약 서버로부터 이 jsonString을 받았다고 가정했을 때, 이것을 바로 사용할 수 있을까? 아마 매우 힐듯 것이다. 내가 필요한 데이터들을 읽을 수는 있지만, 객체가 아닌 String타입이기 때문에 원하는대로 데이터를 조작할 수가 없다. 그렇기 때문에 우선 이 String을 Map객체로 변환시키는 작업이 필요하다. 이것을 serialization 즉, 직렬화라고 한다.

 

직렬화를 하는 방법은 크게 2가지가 있다.

 

첫 번째는 dart의 Convert라이브러리를 이용해 String을 곧바로 직렬화하는 방법이다. 이 방법을 사용하면 jsonDecode를 사용해 손쉽게 직렬화를 할 수가 있다. 

 

 

아래 print의 결과를 보면, String타입에서 JsonMap타입으로 직렬화가 된 것을 확인할 수 있고, 실제로 user객체에서 Map과 같이 name을 키 값으로 넣었을 때 정상적으로 value가 출력되는 것을 확인할 수 있다.

이것만 보면 직렬화가 아주 간단하다고 생각하기 쉽다. 하지만 이 방법의 문제점은 user객체의 타입추론이 불가능해 오류 추적, 디버깅에 큰 어려움이 따른다는 것이다. 또한 컴파일 시 오류가 뜨지 않는다는 크리티컬한 문제로 인해, 큰 프로젝트 일 수록 버그를 유발하는 요인이 되기 쉽상이다. 결과적으로 정적 타입 언어의 장점을 잃어버리는 결과가 된다.

 

직렬화를 하기 위한 두 번째 방법은 이러한 문제를 해결해준다.

 

class User {
  final String name;
  final String email;

  User(this.name, this.email);

  factory User.fromJson(Map<String, dynamic> json) {
    return User(json["name"], json["email"]);
  }

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

 

두 번째 방법은 모델 클래스 내부에서 직렬화와 역직렬화(Deserialization)을 처리하는 것이다. 이렇게 하면 컴파일 시 에러를 캐치할 수 있고, 타입 추론이 가능해져 좀 더 안정성을 가질 수 있다.

 

- fromJson 생성자로 Map객체를 User 인스턴스로 변환할 수 있다.

- toJson으로 User인스턴스에서 Map으로 변환할 수 있다. 

 

 

이렇게 jsonString => Map(String, dynamic) => User 로 형변환을 하여 json데이터를 손쉽게 처리를 할 수 있는 것이다.

만약 지금까지와 반대로, 인스턴스를 jsonString으로 만들고 싶다면 toJson을 활용할 수 있다.

 

 

지금까지 단순한 타입의 jsonString을 다루는 방법을 알아보았다. 하지만 jsonDecode를 사용할 줄 안다고 모든 데이터를 처리할 수 있는 것은 아니다. 서버와 통신을 하다보면 아래와 같은 형태의 데이터들을 만나게 되는 경우가 있다.

 

 

1. 중첩 JSON (nested JSON)

{
  "name": "John Doe",
  "email": "johndoe@example.com",
  "address": {
    "street": "123 Main St",
    "city": "Anytown",
    "zipcode": "12345"
  }
}

 

만약 위와같은 JSON 데이터가 있다면 어떨까? JSON이 2중으로 중첩되어 있어도 jsonDecode를 사용하면 Map객체를 만들 수 있다.

 

print의 결과를 보면, 중첩되어있는 json도 객체로 변환이 된 것을 확인할 수 있다. 하지만 앞서 말했듯이 이런식으로 jsonString을 객체로 변환한 뒤 바로 사용하는것은 상당히 위험할 수 있다. 때문에 모델을 만들어 처리를 해주는 방법을 필수적으로 알고 있어야 한다.

 

우선 위 데이터에는 두 가지 모델이 필요한 것을 알 수 있다. json 전체를 나타내는 User모델과 adress내부의 값들을 정의할 Address모델이 필요하다.

 

우선 Address모델을 정의해보자. Address는 street, city, zipcode로 이루어진 단순한 모델로 만들 수 있다.

class Address {
  final String street;
  final String city;
  final String zipcode;

  Address({required this.street, required this.city, required this.zipcode});

  factory Address.fromJson(Map<String, dynamic> json) {
    return Address(
      street: json['street'],
      city: json['city'],
      zipcode: json['zipcode'],
    );
  }

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

 

이번에는 adress를 포함하는 User 모델을 정의해보자. 앞서 만들었었던 User클래스와의 차이점은 address가 단순한 String이 아닌 객체라는 것이다. 그렇기 때문에 아래와 같이 User객체의 인스턴스를 만들 때에는 Address의 fronJson생성자를 사용하여 객체를 담을 수 있게 만들고, json객체를 만들 때에는 toJson으로 똑같이 address의 객체를 담아줘야 한다.

 

class User {
  final String name;
  final String email;
  final Address address;

  User({required this.name, required this.email, required this.address});

  factory User.fromJson(Map<String, dynamic> json) {
    return User(
      name: json['name'],
      email: json['email'],
      address: Address.fromJson(json['address']),
    );
  }

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

 

코드로 만들어보자.

 

 

 

2. list를 가지고 있는 중첩 JSON

 

만약 address가 하나의 값이아니라 리스라고 가정하면 어떻게 해야할까? 우선 address에는 변화가 필요 없을 것이다. 하지만 User에서는 address가 리스트로 들어올 수 있도록 수정이 필요하다

class User {
  final String name;
  final String email;
  final List<Address> addresses;

  User({required this.name, required this.email, required this.addresses});

  factory User.fromJson(Map<String, dynamic> json) {
    var addressesFromJson = json['addresses'];
    List<Address> addressList = addressesFromJson.map((addressJson) => Address.fromJson(addressJson)).toList();

    return User(
      name: json['name'],
      email: json['email'],
      addresses: addressList,
    );
  }

  Map<String, dynamic> toJson() => {
        'name': name,
        'email': email,
        'addresses': addresses.map((address) => address.toJson()).toList(),
      };
}

 

리스트로 바뀌면서 발생한 차이는 json["addresses"]라는 리스트의 map함수를 활용해 각각의 요소들을 모두 fromJosn 혹은 toJson으로 형변화를 시킨뒤, 그 집합을 리스트로 만들어준다는 것이다. 만약 이 과정을 거치지 않고 단순히 toList를 만들어준다면 타입 캐스팅이 실패하여 에러가 발생하게 될 것이다.

 

3. 리스트로 들어오는 JSON

 

모델에서의 변화는 없다. 하지만 처음 데이터를 받을 때, 리스트를 처리해주어야 한다.

 

void main() {
  String jsonString = '''
  [
    {
      "name": "John Doe",
      "email": "johndoe@example.com",
      "addresses": [
        {
          "street": "123 Main St",
          "city": "Anytown",
          "zipcode": "12345"
        },
        {
          "street": "456 Elm St",
          "city": "Othertown",
          "zipcode": "67890"
        }
      ]
    },
    {
      "name": "Jane Smith",
      "email": "janesmith@example.com",
      "addresses": [
        {
          "street": "789 Oak St",
          "city": "Somewhere",
          "zipcode": "54321"
        }
      ]
    }
  ]
  ''';


  List<dynamic> jsonList = jsonDecode(jsonString);
  List<User> users = jsonList.map((userJson) => User.fromJson(userJson)).toList();
  
  users.forEach((user) {
    print('Name: ${user.name}');
    print('Email: ${user.email}');
    user.addresses.forEach((address) {
      print('Address: ${address.street}, ${address.city}, ${address.zipcode}');
    });
  });

  String encodedJson = jsonEncode(users.map((user) => user.toJson()).toList());

  print(encodedJson);
}

 

A. JSON 문자열을 List<User> 객체로 인라인으로 디코딩
B. 디코딩된 리스트를 map에서 User클래스로 형변환

 

순서로 처리를 하면 된다.

 

 

결국, JSON데이터가 value가 리스트인 key를 갖고 있거나 중첩이든 복잡하게 생각하지 않고 하나씩 처리를 해주면 어렵지 않게 JSON을 처리할 수가 있다.