본문 바로가기

개발 이야기/flutter

[flutter] graphql code generator | artemis 사용 방법과 장단점

model, fromJson 노가다

flutter에서 model 클래스 정의하고, json serializer를 일일이 만들어줘야하는 게 불편했다. 데이터를 가져올 때마다 매번 이래야하니 귀찮아 죽을 것 같았다. 🤯

 

두번째 모델을 만들려다가 flutter에도 왠지 graphql-code-generator 같은 비슷한 라이브러리가 있을 것 같았다.아니나 다를까 3개 정도가 있었는데 그 중 artemis가 document도 잘 정리되어있고 star 수도 많아 쓸만해보였다.

 


 

Artemis

아르테미스는 graphql 파일을 찾아 dart 파일을 생성해주는 code generator 기능이 있고 ArtemisClient로 graphql 쿼리도 날릴 수 있다. artemis는 아폴론(apollo)의 쌍둥이 여동생인데 graphql Apollo와 비슷하다고 하여 artemis로 지었다고 한다.

 

source: github 'comigor/artemis' repo

 

사용법

1. 스키마 파일 가져오기

flutter, firebase를 같이 써서 모든 스키마 파일이 플러터 프로젝트 안에 있다면 그냥 스키마를 한 파일에 넣으면 된다.

 

나의 경우에는 nodejs를 server로 사용하고 있어서 graphql-code-generator로 스키마 파일을 한 파일에 모아서 generate했다.

 

yarn add @graphql-codegen/schema-ast

 

codegen.yml

generates:
  schema.graphql:
    plugins:
      - 'schema-ast'

 

code generate를 하면 서버에 schema.graphql 파일이 생성된다. 이 파일을 flutter 프로젝트 폴더로 복사한다. (artemis에서는 서버 엔드포인트나 상대 경로로 schema를 가져올 수 없어서 이렇게 했다.)

 

 

 

pubspec.yaml

아래 패키지를 모두 설치해준다. 권장 버전은 바뀔 수 있으니 버전은 artemis 문서를 확인한다.

 

dependencies:
  # graphql code generator
  artemis: '>=6.0.0 <7.0.0' # only if you're using ArtemisClient!
  json_annotation: ^3.1.0
  equatable: ^1.2.5
  meta: '>=1.0.0 <2.0.0' # only if you have non nullable fields
  gql: '>=0.12.3 <1.0.0'
  intl: ^0.15.8

dev_dependencies:
  build_runner: ^1.10.4
  json_serializable: ^3.5.0

 

 

build.yaml 설정

options 종류는 artemis 도큐먼트에 나와있다.

 

- naming_schema: simpe : generated 되는 클래스 이름들을 간단하게 바꿔준다. default 옵션은 상위 클래스의 이름이 증조클래스$부모클래스$자식클래스 이렇게 prefix로 계속 붙어서 가독성이 떨어진다. 대신 이름이 겹치지 않아서 안정적이다.

 

- schema_mapping : output 파일은 generated된 파일 위치 / schema는 맨 처음 얘기했던 스키마 파일 위치 / queries_glob은 query나 mutation의 쿼리문 파일의 위치를 적는다.

 

- custom_parser_import : artemis가 파싱할 수 없는 타입은 직접 파서를 만들어야 한다. 다행히 흔히 쓰이는 Date같은 타입은 artemis에 예시가 있었다.

 

- scalar_mapping : graphql_type은 graphql 파일 안에 있는 타입, 그리고 dart_type은 graphql 타입을 generate했을 때 dart의 어떤 타입과 매핑할 건지 써주면 된다. custom_parser를 써야하면 use_custom_parser: true 를 사용한다.

 

targets:
  $default:
    builders:
      artemis:
        options: #1 options
          naming_scheme: simple
          schema_mapping:
            - output: lib/generated/graphql-api.dart
              schema: lib/schema.graphql
              queries_glob: lib/logic/graphql/operations/*.graphql
          custom_parser_import: "package:bookbook/lib/utils/coercers.dart"
          scalar_mapping:
            - graphql_type: GraphQLDate
              dart_type: DateTime
              use_custom_parser: true
            - graphql_type: GraphQLDateTime
              dart_type: DateTime
              use_custom_parser: true
            - graphql_type: JSONObject
              dart_type: Map<String, dynamic>
    sources: 
      - lib/**
      - graphql/**

 

 

generate

아래 명령어를 입력해서 build_runner로 build.yaml에 있는 내용을 실행한다.

flutter pub run build_runner build

output 파일이 lib/generated/graphql-api.dart 에 생긴다. (3개의 파일이 생길 것이다)

 

 

 

코드 단축

model 클래스나 fromJson같은 serializer를 만들어주지 않아도 된다. 알아서 만들어준다. 또한, graphql document도 만들어줘서 string으로 따로 만들 필요 없이 graphql-api.dart 파일만 import 해서 사용하면 된다.

 

보통은 final search_books = r'''query { ... } ''' 처럼 string으로 작성한 후, gql(search_books)로 graphql document를 만들지만, artemis를 사용하면 그냥 아래처럼 graphql 파일로 작성한다.

 

query search_books($q: String!, $page: Int) {
  searchBooks(q: $q, page: $page) {
    books {
      title
      description
      isbn
      thumbnail
      __typename
    }
  }
}

 

 

그리고 generated 파일만 import 해서 document를 갖다쓰면 된다.

import 'example/generated/graphql-api.graphql.dart';

QueryOptions(
	document: SearchBooksQuery().document,
	variables: {'q': q, 'page': page}
);

 

 

fromJson도 generated된 클래스의 메서드를 사용하면 된다.

fromJson(QueryResult result) {
    final books =
        SearchBooks$Query$SearchBookPayload.fromJson(result.data['searchBooks'])
            .books;
    return books;
  }

 

 

만약에 artemis를 쓰지 않았다면 아래 같은 모델을 만들어줘야 한다.

class SearchBook {
  final String title;
  final String description;
  final String isbn;
  final String thumbnail;

  SearchBook({this.title, this.description, this.isbn, this.thumbnail});

  factory SearchBook.fromJson(Map<String, dynamic> json) { 
    return SearchBook(
      title: json['title'],
      description: json['description'],
      isbn: json['isbn'],
      thumbnail: json['thumbnail'],
    );
  }
}

 

 

또한, fromJson도 아래처럼 복잡해진다.

List<SearchBook> fromJson(QueryResult result) {
    final booksData = result.data['searchBooks']['books'];
    final books = List<SearchBook>.from(
        booksData.map((book) => SearchBook.fromJson(book)));
    return books;
  }

 

 


 

Artemis의 장단점

artemis는 노가다할 때보다는 확실히 편했다. 하지만 graphql-code-generator와 비교해봤을 때 아직은 불편한 점도 있었다.

 

장점

 

- model 클래스 정의, fromJson 노가다 안해도 된다.

- graphql_flutter, model 파일 등 import 안하고 generated 된 파일 하나만 import 하면 된다.

 

서버 graphql endpoint에서 graphql schema를 못가져온다는 게 아쉬웠다. 하지만 model 클래스나 Json serializer 를 따로 정의해주지 않아도 되고, graphql document도 gql(document) 로 안쓰고 generated된 타입에서 가져다 써서 graphql_flutter를 따로 Import 해주지 않아도 되서 편했다.

 

 

단점

 

- watch가 안되서 graphql 파일에 변경이 생길 때 마다 매번 generate 해줘야 한다.

- 서버 graphql endpoint에서 graphql schema를 읽지 못한다. ../server/schema.graphql 처럼 상대경로로 읽어 오는 것도 안된다 (glob 패턴으로 프로젝트 폴더 안에서 파일을 찾아 그런 것 같다).

- ArtemisClient로도 query, mutation 등의 operation을 실행할 수 있는데, graphql_flutter에 비해 지원되는 기능이 적은 듯하다. code generator 정도로만 사용하면 될 듯하다.

 

그리고 클래스의 이름이 너무 길다. Payload 타입을 여러 곳에서 써야하는데 클래스명이 BooksData$Query$BookPayload$Book 이렇게 나온다. naming_scheme: simple 옵션이 있긴 한데 이 옵션은 query, mutation의 네이밍을 간단하게 만들어준다. 그러나 query명에 적용되는 것 같지는 않다. 모델을 새로 만들어 사용하는 대신 해당 클래스명을 사용할 수 있는데, 여러 군데서 사용하려니 가독성이 떨어진다.

 

예를 들어 아래 같은 코드가 있다고 해보자. default 옵션에서는 상위 클래스가 계속 prefix로 붙어 BigQuery$Query$AliasOnThing$AliasOnNextThing 처럼 클래스명이 길어진다. 반면 simple 옵션은 AliasOnNextThing 으로 쓸 수 있다. 그러나 query 명인 BigQuery는 여전히 prefix가 계속 붙는다.

 

query big_query($input: Input!) {
  aliasOnThing: thing(input: $input) {
    e
    ...parts
    nextThing {
      id
    }
    aliasOnNextThing: nextThing {
      id
    }
  }
}

 

 


 

references