달달한 스토리

728x90
반응형

 

오늘은 api 서버통신에 대한 글을 작성하려 한다.

 

참고 자료는

 

코딩 팩토리님의 영상과

 

아래 공식 문서를 참고했다.

 

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

 

JSON과 직렬화

어느 시점부터 웹 서버와 통신하지 않거나 구조화된 데이터를 적절하게 보관하지 않는 모바일 앱을생각하기 어려워졌습니다. 네트워크와 연결된 앱을 제작할 때, 결국에는 제법 괜찮은 JSON을사

flutter-ko.dev

우선 이 직렬화를 자동으로 쓰지 않으면

 

생길 수 있는 문제점에 대해서 설명하겠다.

 

보통 우리가 서버에서 api를 통해 json 데이터를 받아오는데,

 

모델 클래스를 만들어 받는 경우가 흔하다.

 

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,
    };
}

서버에서 데이터를 인코딩(encode)에서 보내주면

 

우리는 그 데이터를 디코딩(decode)해서 사용하게 된다.

 

Map userMap = jsonDecode(jsonString);
var user = User.fromJson(userMap);

print('안녕하세요, ${user.name}님!');
print('${user.email}으로 인증 링크를 보냈습니다.');

위에 코드가 기존에 서버에서 받은 데이터를 모델 값에 넣어주어

 

사용하던 방법이다.

 

받아온 파일은 디코딩한 후 fromJson으로,

 

서버에 데이터를 보낼 땐 엔코딩 하여

 

String json = jsonEncode(user);

toJson으로 보내주는 것이다.

 

이것이 저희들이 흔히 말하는 (직렬화) Serialiazable이라고 합니다.

 

아울러서 반대로 다시 변환하는 역직렬 화도 아울러서 이야기한답니다.

 

하지만 이 방법에서 문제점은

 

1. 받아올 데이터가 많고 모델 클래스가 여러 개 필요할 때는 이 과정이 번거롭고, 오래 걸린다.

 

2. json에 키값을 넣을 때는 (ex) json ['test]) 오타가 날 확률이 있다.

 

3. 2번에서 키값에 오타가 나면 컴파일 때 걸러지지 않고 런타임 시(앱 실행 시)에 걸러진다.

 

이러한 문제점 때문에 이 객체들을 자동으로 완성해 주는 코드를 사용하면

 

훨씬 더 코드 안전성이나 효율면에서 좋다고 생각할 수 있습니다.

 

공식 문서에도 소규모 프로젝트에서는 일반적으로 사용하는 직렬화를

 

사용해도 되지만, 오류나 안정성을 위해서 중대형 프로젝트에서는

 

자동 코드 생성기를 사용하라고 권장하고 있습니다.

 

그렇다면 json 직렬화를 자동화하는 코드를 짜 봅시다.

 

우선 pubspec.yaml에 다음과 같이 패키지를 넣어줍니다.

 

version: 1.0.0+1

environment:
  sdk: ">=2.12.0 <3.0.0" //이거

# Dependencies specify other packages that your package needs in order to work.
# To automatically upgrade your package dependencies to the latest versions
# consider running `flutter pub upgrade --major-versions`. Alternatively,
# dependencies can be manually updated by changing the version numbers below to
# the latest version available on pub.dev. To see which dependencies have newer
# versions available, run `flutter pub outdated`.
dependencies:
  flutter:
    sdk: flutter

  retrofit: ^2.0.1 //이거
  logger: ^1.0.0
  dio: ^4.0.0 //이거

  # The following adds the Cupertino Icons font to your application.
  # Use with the CupertinoIcons class for iOS style icons.
  cupertino_icons: ^1.0.2

dev_dependencies:
  flutter_test:
    sdk: flutter

  retrofit_generator: ^2.0.1+1 //이거
  build_runner: ^2.0.1 //이거
  json_serializable: ^5.0.0 //이거

주석으로 "이거"라고 써져있는 부분을 넣어주면 됩니다.

 

여기서 주의하셔야 될 부분이 있습니다.

 

각 플러그인의 버전의 호환성이 정말 예민하기 때문에

 

하나의 버전이라도 안 맞으면 오류가 날 가능성이 큽니다.

 

특히 retrofit에서 사용하는 source_gen의 버전이

 

json_serializable에서 사용하는 source_gen 버전과 차이가

 

컸기 때문입니다.

 

고로, 이 5가지(retrofit, dio, retrofit_generator, build_runner, json_serializable)

은 위에 기재된 버전을 맞춰주시는 것을 권장합니다.

 

저도 모두 다 최근 버전으로 맞추어 사용했지만, 

 

종종 오류가 뜨는 바람에.. 엄청난 삽질을 했습니다.

 

각 플러그인에서 제공하는 기능이 서로 호환이 안 맞는 부분들이 많았으며,

 

위에 버전들은 제가 최대한 맞춰놓은 버전들입니다.

 

 

저도 무턱대고 최신 버전으로 다 올려놓으니 다 에러가 뜨는 바람에..

 

아 그리고, environment도 

 

sdk 버전을 ">=2.12.0 <3.0.0"로 맞추어 주시길 바랍니다.(이 부분도 호환 문제입니다.)

 

이 플러그인들의 링크들은 아래의 제공하겠습니다.

 

https://pub.dev/packages/retrofit

 

retrofit | Dart Package

retrofit.dart is an dio client generator using source_gen and inspired by Chopper and Retrofit.

pub.dev

https://pub.dev/packages/retrofit_generator

 

retrofit_generator | Dart Package

retrofit generator is an dio client generator using source_gen and inspired by Chopper and Retrofit.

pub.dev

https://pub.dev/packages/dio

 

dio | Dart Package

A powerful Http client for Dart, which supports Interceptors, FormData, Request Cancellation, File Downloading, Timeout etc.

pub.dev

https://pub.dev/packages/build_runner

 

build_runner | Dart Package

A build system for Dart code generation and modular compilation.

pub.dev

https://pub.dev/packages/json_serializable

 

json_serializable | Dart Package

Automatically generate code for converting to and from JSON by annotating Dart classes.

pub.dev

 

보통은 retrofit 대신 "json annotation"을 쓰는 경우가 많으나,

 

저 같은 경우 retrofit와 retrofit_generator를 사용했습니다.

 

retrofit에서 사용되는 annotation이 더 보기 좋고 쓰기 간편했기 때문입니다.

 

게다가 build_runner에 json annotation이 내장되어 굳이 따로,

 

패키지를 추가하지 않아도 됩니다.

 

 

위에 패키지를 간단히 설명드리면,

 

retrofit: 소스 코드 생성 빌 및 유틸리티인 source_gen을 사용하여

dio 클라이언트를 생성해주는 생성기입니다.

 

dio: 인터셉터, FormData. 요청 취소 등 http용 서버 연동 클라이언트입니다.


retrofit_generator: retrofit과 함께 쓰는 generator입니다. retroifit과 마찬가지로

dio 클라이언트의 생성의 보조 역할을 합니다.


build_runner: 자동 코드 생성의 핵심인 모듈식 컴파일을 위한 빌드 시스템입니다.


json_serializable: 이 플러그인도 serializable을 추가하여 json직력화를 위한

자동 코드 생성을 도와주는 플러그인입니다.

 

한마디로 이 dio를 제외한 네 가지 플러그인은 거의 세트로 사용된다고 생각하시면

 

될 것 같습니다.

 

우선 retrofit폴더를 만들고,

 

안에 임의로 rest.dart 파일을 만들어 보겠습니다.

 

 

그리고 다음과 같이 작성합니다.

 

import 'package:json_annotation/json_annotation.dart';
import 'package:retrofit/retrofit.dart';
import 'package:dio/dio.dart';


part 'rest.g.dart';


@RestApi(baseUrl: 'https://hacker-news.firebaseio.com/v0')
abstract class RestClient {


  factory RestClient(Dio dio, {String baseUrl}) = _RestClient;

  @GET('/topstories.json')
  Future<List<int>> getTopNews();

}

@JsonSerializable()
class News {
  int id;
  String title;
  String type;
  String url;

  News({
    required this.id,
    required this.title,
    required this.type,
    required this.url,
  });

  factory News.fromJson(Map<String, dynamic> json) => _$NewsFromJson(json);

  Map<String, dynamic> toJson() => _$NewsToJson(this);
}

코드를 하나하나 살펴보겠습니다.

 

우선 첫 번째로 코드를 작성하시면서

 

빨간색의 오류가 뜨실 텐데(import가 안될 때)

 

오히려 이것이 정상이므로, 오류가 떠도 그대로 코드를 작성하시면 됩니다.

세 플러그인의 import를 해주시고,

 

아래 보시면

 

part 'rest.g.dart'라고 써준 부분이 있습니다.

 

이 코드는 자동으로 코드를 생성하는 규칙 중 하나이므로,

 

다음과 같은 형식으로 지켜야 합니다.

 

'자동 코드를 생성할 dart이름'. g.dart

 

 

그리곤 아래 RestApi 어노테이션 안에 기본적으로 사용될

 

api주소를 넣어줍니다.

 

클래스는 추상 클래스로 명시하는 abstract로 만들어 주시고,

 

클래스 이름은 원하는 클래스명으로 해주시면 됩니다.

 

factory로 되어있는 생성자를 다음과 같이 작성해주시고,

 

factory RestClient(Dio dio, {String baseUrl}) = _RestClient;

 

아래 GET어노테이션을 쓸 것인지 아니면 POST를 쓸 것인지..

 

여러 어노테이션이 있기에 이 부분은 글 맨 아래에 제가 예제를 둘 테니

 

참고하시고 적용하셔도 될 것 같습니다.

 

그리고 아래에는

 

@JsonSerializable()
class News {
  int id;
  String title;
  String type;
  String url;

  News({
    required this.id,
    required this.title,
    required this.type,
    required this.url,
  });

  factory News.fromJson(Map<String, dynamic> json) => _$NewsFromJson(json);

  Map<String, dynamic> toJson() => _$NewsToJson(this);
}

News클래스에서 받을 멤버 변수를 생성자와 함께 넣어주시고,

 

아래 코드는 정해진 형식에 따라

 

다음과 같이 작성합니다.

 

자 어떠신가요?

 

여기까지 하나의 직렬화가 완성된 것입니다. 

 

자 그러면 서버와 연동되는 코드를 자동으로 생성해봅시다.

 

반응형

 

터미널을 열어주고,

 

다음과 같이 입력합니다.

 

flutter pub run build_runner build

 

이 명령어를 수행하시고 succeed가 뜨면,

 

프로젝트 창에 다음과 같은 파일이 하나 생성됩니다.

// GENERATED CODE - DO NOT MODIFY BY HAND

part of 'rest.dart';

// **************************************************************************
// JsonSerializableGenerator
// **************************************************************************

News _$NewsFromJson(Map<String, dynamic> json) => News(
      id: json['id'] as int,
      title: json['title'] as String,
      type: json['type'] as String,
      url: json['url'] as String,
    );

Map<String, dynamic> _$NewsToJson(News instance) => <String, dynamic>{
      'id': instance.id,
      'title': instance.title,
      'type': instance.type,
      'url': instance.url,
    };

// **************************************************************************
// RetrofitGenerator
// **************************************************************************

class _RestClient implements RestClient {
  _RestClient(this._dio, {this.baseUrl}) {
    baseUrl ??= 'https://hacker-news.firebaseio.com/v0';
  }

  final Dio _dio;

  String? baseUrl;

  @override
  Future<List<int>> getTopNews() async {
    const _extra = <String, dynamic>{};
    final queryParameters = <String, dynamic>{};
    final _data = <String, dynamic>{};
    final _result = await _dio.fetch<List<dynamic>>(_setStreamType<List<int>>(
        Options(method: 'GET', headers: <String, dynamic>{}, extra: _extra)
            .compose(_dio.options, '/topstories.json',
                queryParameters: queryParameters, data: _data)
            .copyWith(baseUrl: baseUrl ?? _dio.options.baseUrl)));
    final value = _result.data!.cast<int>();
    return value;
  }

  RequestOptions _setStreamType<T>(RequestOptions requestOptions) {
    if (T != dynamic &&
        !(requestOptions.responseType == ResponseType.bytes ||
            requestOptions.responseType == ResponseType.stream)) {
      if (T == String) {
        requestOptions.responseType = ResponseType.plain;
      } else {
        requestOptions.responseType = ResponseType.json;
      }
    }
    return requestOptions;
  }
}

이런 식으로 api에서 연동할 수 있는 번거로운 코드 작업을

 

자동으로 만들어주는 놀라운 기능입니다..!

 

자 그럼 사용은 어떻게 할까요?

 

사실 이 두 파일은 part로 연결되어 있기 때문에

 

둘 다 삭제를 하시면 안 됩니다.

 

저희가 호출해서 사용할 부분은 rest.dart입니다.

@RestApi(baseUrl: 'https://hacker-news.firebaseio.com/v0')
abstract class RestClient {


  factory RestClient(Dio dio, {String baseUrl}) = _RestClient;

  @GET('/topstories.json')
  Future<List<int>> getTopNews();

}

@JsonSerializable()
class News {
  int id;
  String title;
  String type;
  String url;

  News({
    required this.id,
    required this.title,
    required this.type,
    required this.url,
  });

  factory News.fromJson(Map<String, dynamic> json) => _$NewsFromJson(json);

  Map<String, dynamic> toJson() => _$NewsToJson(this);
}

News 정보를 서버에서 가져오기 위해서는 getTopNews()

 

사용하시면 되고,

 

그에 따른 직렬화를 위한 모델은

 

아래 뉴스 클래스의 fromJson과 toJson을 이용하시면 됩니다.

 


보너스

api 연동 시에 header가 필요하실 겁니다.

 

dio import에 hide 하고 Header를 넣어주신 다음,

 

해당 api를 호출하는 메서드 위에

 

Headers어노테이션을 달아 주시면 됩니다.

 

헤더는 이런 식으로 넣어주시고,

 

flutter pub run build_runner build를 하면 됩니다.

 

그러면 g.dart에 Header가 넣어진 상태로 코드가 만들어집니다.

 

다른 기능도 보고 싶으신 분들은 아래 링크를 참고해 주세요.

 

https://github.com/trevorwang/retrofit.dart/blob/master/example/lib/example.dart

 

GitHub - trevorwang/retrofit.dart: retrofit.dart is an dio client generator using source_gen and inspired by Chopper and Retrofi

retrofit.dart is an dio client generator using source_gen and inspired by Chopper and Retrofit. - GitHub - trevorwang/retrofit.dart: retrofit.dart is an dio client generator using source_gen and in...

github.com

 

그리고 한 가지가 더 있습니다.

 

이런 생각이 드실 수도 있습니다.

 

코드를 생성할 때마다 

 

flutter pub run build_runner build를 호출해 주어야 할까?

 

전혀 아닙니다.

 

이 방법은 비효율적입니다.

 

코드를 수정하면 flutter pub run build_runner build를 실행하지 않아도,

 

자동으로 만들어주는 명령어가 있습니다.

 

바로 flutter pub run build_runner watch입니다.

 

이 명령어를 사용하면

이런 식으로 만약에 rest.dart 안에 News2라는 클래스를 생성하고,

 

나머지 부분도 변경한 후 (컨트롤 + s)를 누르면,

rest.g.dart안에 새로운 api호출 메서드가 자동으로 생성이 됩니다.

 

엄청나죠??

 

클래스를 지우고 컨트롤 + s를 누르면 다시 원래대로 돌아갑니다.

 

이 코드는 백그라운드에서 watcher코드가 돌아가면서 코드의 수정사항을

 

확인 후 그대로 적용해주어, 일일이 flutter pub run build_runner build를

 

호출하지 않아도 자동 적용될 수 있게 지속성을 부여해줍니다.

 

대신 안드로이드 스튜디오를 종료하면 백그라운드 실행이 끝나기 때문에

 

사용을 원하실 때만 이 기능을 실행해 주시기 바랍니다.

 

여기까지가 끝입니다.

 

전체 코드는 깃헙에 올려두었고, retrofit2 폴더에 위치합니다.

 

https://github.com/qjsqjsaos/retrofit_flutter

 

GitHub - qjsqjsaos/retrofit_flutter: 플러터 레트로핏 예제

플러터 레트로핏 예제. Contribute to qjsqjsaos/retrofit_flutter development by creating an account on GitHub.

github.com

 

728x90
반응형

공유하기

facebook twitter kakaoTalk kakaostory naver band
loading