JSON(JavaScript Object Notation)은 Dart에서 비동기식 데이터 처리나 HTTP 통신에서 자주 사용되는 형식이다. 이 섹션에서는 JSON 데이터의 구조, 처리 방식, 그리고 Dart에서 JSON 데이터를 활용하는 방법에 대해 다룬다. 또한, JSON 데이터를 변환하고, 직렬화 및 역직렬화를 통해 Dart 객체와 JSON 간의 상호 변환하는 방법을 설명한다.

1. JSON의 기본 구조

JSON은 데이터를 키-값 쌍으로 저장하는 경량 데이터 형식으로, 다음과 같은 기본 형식을 갖는다:

{
  "name": "John",
  "age": 30,
  "city": "New York"
}

Dart에서는 JSON 데이터를 주로 문자열로 취급하며, JSON 데이터를 객체로 변환하거나 객체를 JSON으로 변환하는 과정을 제공한다.

2. Dart에서 JSON 데이터 처리

Dart에서 JSON 데이터를 처리할 때는 주로 dart:convert 라이브러리를 사용한다. 이 라이브러리는 JSON을 직렬화(객체 -> JSON) 및 역직렬화(JSON -> 객체)하는 기능을 제공한다.

2.1 역직렬화 (JSON -> Dart 객체)

JSON 문자열을 Dart 객체로 변환하려면 jsonDecode 함수를 사용한다. 예를 들어, 아래 JSON 문자열을 Dart 객체로 변환하는 코드를 살펴보자:

import 'dart:convert';

void main() {
  String jsonString = '{"name": "John", "age": 30, "city": "New York"}';
  Map<String, dynamic> user = jsonDecode(jsonString);

  print('Name: ${user['name']}');
  print('Age: ${user['age']}');
  print('City: ${user['city']}');
}

위 코드에서 JSON 문자열 jsonStringjsonDecode 함수를 통해 Dart의 Map<String, dynamic> 객체로 변환되었다. 여기서 dynamic 타입은 JSON의 값이 다양한 형태(숫자, 문자열, 객체 등)를 가질 수 있음을 의미한다.

2.2 직렬화 (Dart 객체 -> JSON)

반대로, Dart 객체를 JSON 문자열로 변환하려면 jsonEncode 함수를 사용한다. 예를 들어, 아래 Dart 객체를 JSON 문자열로 변환하는 코드를 보겠다:

import 'dart:convert';

void main() {
  Map<String, dynamic> user = {
    'name': 'John',
    'age': 30,
    'city': 'New York'
  };

  String jsonString = jsonEncode(user);

  print(jsonString);
}

이 코드에서는 Map 객체가 jsonEncode 함수를 통해 JSON 문자열로 변환되었다.

3. 중첩된 JSON 데이터 처리

때때로 JSON 데이터는 중첩된 형태로 나타난다. 즉, JSON 데이터 내에 또 다른 JSON 객체가 포함된 경우이다. 예를 들어, 아래와 같은 구조를 갖는 JSON을 처리할 수 있다:

{
  "name": "John",
  "age": 30,
  "address": {
    "city": "New York",
    "zipcode": "10001"
  }
}

이 경우 Dart에서 JSON 데이터를 처리하는 방식은 동일하며, 중첩된 데이터를 접근할 때는 키를 체인 방식으로 사용한다. 아래 예제를 살펴보자:

import 'dart:convert';

void main() {
  String jsonString = '''
  {
    "name": "John",
    "age": 30,
    "address": {
      "city": "New York",
      "zipcode": "10001"
    }
  }
  ''';

  Map<String, dynamic> user = jsonDecode(jsonString);

  print('City: ${user['address']['city']}');
  print('Zipcode: ${user['address']['zipcode']}');
}

위 코드에서 중첩된 address 객체의 cityzipcode에 접근하기 위해, user['address']['city']와 같은 방식으로 키 체인을 사용하였다.

4. JSON 배열 처리

JSON 데이터는 배열 형태로 나타날 수도 있다. 예를 들어, 사용자가 여러 명인 경우 다음과 같은 JSON 데이터가 있을 수 있다:

[
  {
    "name": "John",
    "age": 30
  },
  {
    "name": "Jane",
    "age": 25
  }
]

이 경우, Dart에서는 JSON 배열을 List 객체로 변환할 수 있으며, 각 요소는 Map으로 처리된다. 예를 들어, 아래와 같이 JSON 배열 데이터를 처리할 수 있다:

import 'dart:convert';

void main() {
  String jsonString = '''
  [
    {"name": "John", "age": 30},
    {"name": "Jane", "age": 25}
  ]
  ''';

  List<dynamic> users = jsonDecode(jsonString);

  for (var user in users) {
    print('Name: ${user['name']}, Age:${user['age']}');
  }
}

위 코드에서는 JSON 배열을 jsonDecode 함수를 통해 List로 변환하였으며, 각 user 객체에 반복문을 사용해 접근하고 있다.

5. JSON 데이터와 Dart 클래스 간의 변환

Dart에서 JSON 데이터를 처리할 때, 일반적으로는 Map 객체를 사용하지만, 더 복잡한 구조의 데이터를 다룰 때는 Dart 클래스로 변환하는 것이 유용하다. 이 과정에서는 JSON 데이터를 Dart 클래스의 인스턴스로 역직렬화하거나, Dart 클래스의 인스턴스를 JSON으로 직렬화하는 방법이 필요하다.

5.1 JSON -> Dart 클래스

우선 JSON 데이터를 Dart 클래스의 인스턴스로 변환하는 방법을 알아보겠다. 예를 들어, 다음과 같은 User 클래스를 정의하고 JSON 데이터를 이를 바탕으로 처리할 수 있다:

class User {
  String name;
  int age;
  String city;

  User({required this.name, required this.age, required this.city});

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

여기서 fromJson이라는 팩토리 생성자를 사용하여 JSON 데이터를 Dart 객체로 변환한다. 다음은 JSON 문자열을 User 객체로 변환하는 예제이다:

import 'dart:convert';

void main() {
  String jsonString = '{"name": "John", "age": 30, "city": "New York"}';
  Map<String, dynamic> json = jsonDecode(jsonString);

  User user = User.fromJson(json);

  print('Name: ${user.name}, Age:${user.age}, City: ${user.city}');
}

이 코드는 JSON 데이터를 User 객체로 변환한 후, 각 필드를 출력한다.

5.2 Dart 클래스 -> JSON

반대로 Dart 객체를 JSON으로 변환하려면, 클래스에 toJson 메소드를 추가해야 한다. 아래는 User 클래스에 toJson 메소드를 추가한 예제이다:

class User {
  String name;
  int age;
  String city;

  User({required this.name, required this.age, required this.city});

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

  Map<String, dynamic> toJson() {
    return {
      'name': name,
      'age': age,
      'city': city
    };
  }
}

toJson 메소드는 Dart 객체를 JSON으로 변환할 때 사용된다. 이제 User 객체를 JSON으로 변환하는 예제를 살펴보자:

import 'dart:convert';

void main() {
  User user = User(name: 'John', age: 30, city: 'New York');
  String jsonString = jsonEncode(user.toJson());

  print(jsonString);
}

이 코드에서 User 객체는 toJson 메소드를 통해 Map으로 변환된 후, jsonEncode 함수를 사용하여 JSON 문자열로 직렬화된다.

6. 중첩된 클래스와 JSON 처리

중첩된 JSON 데이터를 다룰 때, 클래스 내에 또 다른 클래스를 정의해야 한다. 예를 들어, 사용자 정보에 주소를 포함하는 경우, Address 클래스를 별도로 정의하고 User 클래스 내에 이를 포함시킬 수 있다.

class Address {
  String city;
  String zipcode;

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

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

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

class User {
  String name;
  int age;
  Address address;

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

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

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

이제 중첩된 JSON 데이터를 Dart 클래스 객체로 변환하고, 다시 JSON으로 직렬화할 수 있다. 아래 예제를 보겠다:

import 'dart:convert';

void main() {
  String jsonString = '''
  {
    "name": "John",
    "age": 30,
    "address": {
      "city": "New York",
      "zipcode": "10001"
    }
  }
  ''';

  // JSON을 User 객체로 변환
  Map<String, dynamic> json = jsonDecode(jsonString);
  User user = User.fromJson(json);

  // 변환된 User 객체 출력
  print('Name: ${user.name}, Age:${user.age}, City: ${user.address.city}, Zipcode:${user.address.zipcode}');

  // User 객체를 JSON으로 직렬화
  String jsonStringFromUser = jsonEncode(user.toJson());
  print(jsonStringFromUser);
}

위 예제에서는 중첩된 Address 객체를 포함한 User 객체를 생성하고, 이를 JSON으로 변환하는 과정을 확인할 수 있다.

7. JSON 데이터의 유효성 검증

JSON 데이터를 처리할 때, 데이터의 유효성을 검증하는 것이 중요하다. 특히 외부에서 전달되는 JSON 데이터는 예상하지 못한 값이나 구조를 가질 수 있으므로, Dart 코드에서 이를 처리할 수 있는 예외 처리가 필요하다.

7.1 필수 필드 검증

Dart에서는 JSON 데이터에서 특정 필드가 존재하는지 여부를 검사할 수 있다. 아래는 특정 필드가 존재하지 않을 경우 예외를 던지는 예제이다:

import 'dart:convert';

class User {
  String name;
  int age;

  User({required this.name, required this.age});

  factory User.fromJson(Map<String, dynamic> json) {
    if (!json.containsKey('name') || !json.containsKey('age')) {
      throw FormatException('Missing required fields: name or age');
    }
    return User(
      name: json['name'],
      age: json['age']
    );
  }
}

void main() {
  String jsonString = '{"name": "John"}'; // age 필드가 없음

  try {
    Map<String, dynamic> json = jsonDecode(jsonString);
    User user = User.fromJson(json);
  } catch (e) {
    print('Error: $e');
  }
}

위 코드는 nameage 필드가 모두 존재하는지 확인하며, 만약 해당 필드가 없다면 FormatException을 던진다. age 필드가 없으므로, 예외가 발생하여 오류 메시지가 출력된다.

7.2 데이터 타입 검증

JSON 데이터의 필드가 예상한 데이터 타입인지 확인하는 것도 중요하다. Dart에서는 is 연산자를 사용하여 데이터 타입을 확인할 수 있다. 아래는 JSON 필드가 예상한 데이터 타입이 아닐 경우 예외를 던지는 예제이다:

import 'dart:convert';

class User {
  String name;
  int age;

  User({required this.name, required this.age});

  factory User.fromJson(Map<String, dynamic> json) {
    if (!json.containsKey('name') || !json.containsKey('age')) {
      throw FormatException('Missing required fields: name or age');
    }

    if (json['name'] is! String || json['age'] is! int) {
      throw FormatException('Invalid data types for fields: name or age');
    }

    return User(
      name: json['name'],
      age: json['age']
    );
  }
}

void main() {
  String jsonString = '{"name": "John", "age": "thirty"}'; // age의 타입이 잘못됨

  try {
    Map<String, dynamic> json = jsonDecode(jsonString);
    User user = User.fromJson(json);
  } catch (e) {
    print('Error: $e');
  }
}

이 예제에서는 name 필드가 문자열인지, age 필드가 정수인지 확인하며, 예상한 타입이 아닌 경우 FormatException을 던진다. JSON의 age 값이 문자열이므로, 예외가 발생하여 오류 메시지가 출력된다.

8. JSON 직렬화와 역직렬화의 성능 고려

Dart에서 JSON 데이터를 직렬화 및 역직렬화하는 작업은 빈번하게 사용되지만, 성능을 고려한 최적화가 필요할 때도 있다. 특히 대용량 JSON 데이터를 다룰 때, 데이터 크기와 처리 시간에 대한 고려가 필요하다.

8.1 JSON 데이터 압축

JSON 데이터의 크기를 줄이기 위해 압축 기법을 사용할 수 있다. Dart에서는 dart:convertgzip 함수를 사용하여 데이터를 압축하거나, jsonEncode 결과를 압축할 수 있다. 예를 들어, 아래와 같이 JSON 데이터를 gzip으로 압축할 수 있다:

import 'dart:convert';
import 'dart:io';

void main() {
  Map<String, dynamic> data = {
    'name': 'John',
    'age': 30,
    'city': 'New York'
  };

  // JSON 직렬화
  String jsonString = jsonEncode(data);

  // gzip으로 압축
  List<int> compressed = gzip.encode(utf8.encode(jsonString));

  print('Compressed size: ${compressed.length} bytes');
}

압축된 데이터는 네트워크 전송 시 데이터 크기를 줄일 수 있어 성능 최적화에 기여할 수 있다.

8.2 비동기 JSON 처리

Dart의 비동기 처리 기능을 활용하면, 대규모 JSON 데이터를 처리할 때도 메인 스레드의 성능에 영향을 주지 않도록 할 수 있다. Dart에서는 Futureasync/await를 사용하여 비동기식으로 JSON 데이터를 처리할 수 있다.

import 'dart:convert';
import 'dart:io';

Future<void> main() async {
  // 대규모 JSON 파일 읽기
  File file = File('large_data.json');
  Stream<List<int>> inputStream = file.openRead();

  // JSON 데이터를 비동기적으로 디코딩
  await for (var chunk in inputStream.transform(utf8.decoder).transform(json.decoder)) {
    print(chunk);
  }
}

이 예제에서는 대규모 JSON 파일을 비동기적으로 읽고 처리하여, 대용량 데이터를 다룰 때 메모리 사용량을 최소화하고 성능을 유지할 수 있다.