[Flutter] Flutter에서 GraphQL을 잘 사용해보자!

[Flutter] Flutter에서 GraphQL을 잘 사용해보자!

안녕하세요, 개발팀 톡기입니다. 오늘은 Flutter에서 GraphQL을 사용하면서 알게 된 것들을 글로 정리해 볼 거에요. 👀 코드들의 기본 틀은 graphql_flutter 패키지 문서에서 가져왔습니다.

패키지 가져오기 & 설정하기

우선 GraphQL API를 사용하기 위해 https://pub.dev/packages/graphql_flutter 패키지를 가져옵니다.

dependencies:
  graphql_flutter: ^4.0.0-beta
pubspec.yaml

pubspec.yaml에 직접 dependencies를 추가하거나 flutter pub add graphql_flutter를 실행해 최신 버전을 가져온 후 flutter pub get으로 설치해줍니다.

...

//패키지를 import 해옵니다.
import 'package:graphql_flutter/graphql_flutter.dart';

void main() async {

  //헤더에 authorization 토큰이 필요하다면 넣어줍니다
  final link = AuthLink(getToken: () async {
    return "bearer ${토큰}";
  }).concat(HttpLink('${graphql api 링크}'));
  
  //AuthLink가 필요하지 않다면 link = HttpLink('${graphql api 링크}');

  ValueNotifier<GraphQLClient> client = ValueNotifier(
    GraphQLClient(
      link: link,
      cache: GraphQLCache(),
    ),
  );
  
  ...
}

...

여기서 main함수 내의 코드를 따로 class로 분리해서 사용해도 됩니다.

  ...

  return GraphQLProvider(
    client: client,
    child: MaterialApp(
      title: 'Flutter Demo',
      ...
    ),
  );

  ...

GraphQL을 사용할 위젯을 GraphQLProvider의 자식으로 두고 위에서 만든 client를 넣어주면 됩니다.

Query

static String getPostByID = r"""
  query($id: ID!) {
    post(id: $id) {
      title
      content
    }
  }
""";

GraphQL 쿼리문을 String으로 만들어 주고

Widget build(BuildContext context) {
  return Query(
      options: QueryOptions(document: gql(getPostByID), variables: {"id": "${검색할 ID}"}),
      builder: (result, {refetch, fetchMore}) {
        if (result.isLoading) {
          return CircularProgressIndicator();
        }
        final post = result.data!['post'];
        return Scaffold(
          appBar: AppBar(title: Text(post['title'])),
          body: Container(
            child: Text(post['content']),
          ),
        );
      });
}

쿼리를 쏠 때에는 Query 위젯을 이용합니다. Query 위젯은 optionsbuilder라는 두 개의 인자를 필수로 갖고 있습니다.

result.isLoading 으로 현재 쿼리가 로딩중인지 아닌지 판별하여 로딩 중 UI를 표시할 수 있습니다.

options

optionsQueryOptions 타입으로,

required DocumentNode document,
String? operationName,
Map<String, dynamic> variables = const {},
FetchPolicy? fetchPolicy,
ErrorPolicy? errorPolicy,
CacheRereadPolicy? cacheRereadPolicy,
Object? optimisticResult,
Duration? pollInterval,
Context? context

위와 같은 인자를 가질 수 있습니다.

  • document : 쿼리문을 여기에 넣어줍니다. 필수 인자입니다.
  • variables : 쿼리문에 필요한 변수들을 넣어주면 됩니다.
  • fetchPolicy : Fetch 시 캐시에 대한 정책을 정합니다.
  1. FetchPolicy.cacheFirst : 기본적으로 캐시에서 결과를 가져옵니다. 캐시한 결과를 사용할 수 없을 때(요청한 데이터가 모두 캐시에 없을 때)만 네트워크에서 fetch합니다. (기본)
  2. FetchPolicy.cacheAndNetwork : 캐시에서 먼저 결과를 반환한 다음(있는 경우), 사용 가능한 네트워크 결과를 반환합니다.
  3. FetchPolicy.cacheOnly : 캐시에서 결과를 반환하고 불가능하면 실패합니다.
  4. FetchPolicy.noCache : 네트워크에서 fetch한 결과를 반환하고, 네트워크 호출에 실패하면 실패합니다. 네트워크에서 fetch한 결과는 캐시에 저장하지 않습니다.
  5. FetchPolicy.networkOnly  : 네트워크에서 fetch한 결과를 반환하고, 네트워크 호출에 실패하면 실패합니다. 네트워크에서 fetch한 결과는 캐시에 저장합니다.
  • errorPolicy : Error 시 어떻게 처리할 것인지에 대한 정책을 정합니다.
  1. ErrorPolicy.none : 응답에 GraphQL 오류가 포함된 경우 네트워크 오류와 똑같이 취급됩니다. 응답에 데이터가 포함되어 있더라도 무시합니다. (기본)
  2. ErrorPolicy.ignore : 에러를 무시합니다. GraphQL 오류와 함께 반환되는 모든 데이터를 읽을 수 있고 캐시에 저장할 수도 있습니다. 에러를 저장하거나 UI에 표시하지 않습니다.
  3. ErrorPolicy.all : 가져온 데이터와 에러를 모두 캐시에 저장합니다. 데이터를 UI에 보여주면서 에러에 대한 정보 또한 함께 보여줄 수 있습니다.
  • pollInterval : 쿼리를 계속해서 특정 시간마다 refetch해야 할 경우 그 기간을 쓰면 됩니다. 쓰지 않으면 refetch하지 않습니다.

builder

builder에서는 쿼리 결과인 QueryResultFuture<QueryResult> 를 반환하는 refetch, fetchmore 함수를 가지고 이렇게 저렇게 잘 UI에 쿼리 결과를 표현하거나 새로고침, 페이지네이션 등등을 구현할 수 있습니다.

refetch

refetch 는 말 그대로 쿼리를 다시 실행합니다. 무조건 네트워크에서 fetch한 결과를 반환합니다. 새로고침 시에 실행하면 됩니다.

fetchmore

fetchMore를 사용하면 쿼리의 options 를 변경해 쿼리를 쏠 수 있습니다. 그리고 가져온 쿼리 결과를 원래의 쿼리 결과와 합쳐서 return할 수 있어서 페이지네이션을 할 때 쓰기 아주 좋습니다.

pagination

int start = 0, limit = 10;

FetchMoreOptions opts = FetchMoreOptions(
  variables: {'start': start, 'limit': limit},
  updateQuery: (previousResultData, fetchMoreResultData) {
    // 이렇게 하면 fetchMore한 결과와 원래의 쿼리 결과를 합쳐서 가져올 수 있습니다.
    final List<dynamic> allPosts = [
      ...previousResultData['posts'] as List<dynamic>,
      ...fetchMoreResultData['posts'] as List<dynamic>
    ];
    fetchMoreResultData['posts'] = allPosts;
    return fetchMoreResultData;
  },
);

...

RaisedButton(
  child: Row(
    mainAxisAlignment: MainAxisAlignment.center,
    children: <Widget>[
      Text("Load More"),
    ],
  ),
  onPressed: () {
    start += limit;
    fetchMore(opts);
  },
)

updateQuery에서 이전 결과와 새로 가져온 결과를 합쳐서 처리해줍니다.

더 자세한 내용은 https://graphql.org/learn/pagination/ 를 참고하세요.

참고: 위 코드처럼 인덱스 기반으로 페이지네이션을 하면 도중에 새 데이터가 추가되었을 때 인덱스가 밀리므로 Auto Increment Primary Key 같은 걸로 해야 해요^^

cache

제가 잘 몰라서 고생했던 부분인데요. 쿼리의 cache에 저장되는 결과에는 fetchmore로 가져온 결과도 포함됩니다.

그래서 쿼리의 cache 정책이 noCachenetworkOnly 가 아닌 경우, pagination을 할 때 return값에 이전 쿼리의 결과와 fetchmore로 받아온 결과를 합쳐 주지 않으면 나중에 결과를 cache에서 불러올 때 문제가 생깁니다. 지금 1-5페이지의 데이터가 가져와져 있고, fetchmore로 6-10페이지의 데이터를 가져온 후 updateQuery 부분 밖에서 이전 데이터와 붙여버리, 캐시된 값을 불러올 때는 6-10페이지만 보이게 되는 것입니다. 🤨

updateQuery 안에서 1-5페이지의 결과와 6-10페이지의 결과를 잘 합쳐서 처리한 후 fetchMoreResultData로 리턴해주면, 1-10페이지가 잘 캐시되어 나갔다가 들어와도 이전에 불러온 1-10페이지가 잘 보입니다.

기쁨의 엉덩이춤

Mutation

static String createPost = r"""
  mutation($post: createPostInput) {
    createPost(input: $post) {
      post {
        title
        description
      }
    }
  }
""";

마찬가지로 Mutation도 document를 String으로 만들어줍니다.

Widget build(BuildContext context) {
  return Mutation(
      options: MutationOptions(
        document: gql(createPost),
        update: (cache, result) => result,
        onCompleted: (data) {
          //mutation이 완료되었을 때 실행되는 부분
        },
        onError: (e) {
          //error시 실행되는 부분
        }),
      builder: (runMutation, result) => IconButton(
            onPressed: () async {
              runMutation({
                "post": {
                  "data": {
                    "title": /*내용*/,
                    "description": /*내용*/,
                    }
                  }
                });
            },
            icon: result!.isLoading
                ? CircularProgressIndicator()
                : Icon(Icons.upload_rounded),
          ));
}

이런 식으로 runMutation 함수를 통해 뮤테이션을 실행시켜주면 됩니다.

options

MutationOptions 타입인데요, QueryOptions 와 비슷합니다. 특별히 봐야 할 것만 적어보겠습니다.

  • onCompleted : 뮤테이션이 완료되었을 때 실행되는 함수를 넣어줍니다.
  • update : 결과에 따라서 cache를 업데이트 할 수 있다는데 잘 모르겠습니다 😅
  • onError : 에러가 났을 때 실행되는 함수를 넣어줍니다.

builder

(runMutation, result) 를 인자로 갖는 함수입니다. result 는  query에서의 result처럼 사용하면 됩니다. runMutation 은 뮤테이션을 실행하는 함수입니다. 안에 인자로 데이터를 넣어줍니다.

그래프큐엘 정말 재미가 있네요~