[Flutter] Flutter에서 GraphQL을 잘 사용해보자!
안녕하세요, 개발팀 톡기입니다. 오늘은 Flutter에서 GraphQL을 사용하면서 알게 된 것들을 글로 정리해 볼 거에요. 👀 코드들의 기본 틀은 graphql_flutter 패키지 문서에서 가져왔습니다.
패키지 가져오기 & 설정하기
우선 GraphQL API를 사용하기 위해 https://pub.dev/packages/graphql_flutter 패키지를 가져옵니다.
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 위젯은 options
와 builder
라는 두 개의 인자를 필수로 갖고 있습니다.
result.isLoading
으로 현재 쿼리가 로딩중인지 아닌지 판별하여 로딩 중 UI를 표시할 수 있습니다.
options
options
는 QueryOptions
타입으로,
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 시 캐시에 대한 정책을 정합니다.
FetchPolicy.cacheFirst
: 기본적으로 캐시에서 결과를 가져옵니다. 캐시한 결과를 사용할 수 없을 때(요청한 데이터가 모두 캐시에 없을 때)만 네트워크에서 fetch합니다. (기본)FetchPolicy.cacheAndNetwork
: 캐시에서 먼저 결과를 반환한 다음(있는 경우), 사용 가능한 네트워크 결과를 반환합니다.FetchPolicy.cacheOnly
: 캐시에서 결과를 반환하고 불가능하면 실패합니다.FetchPolicy.noCache
: 네트워크에서 fetch한 결과를 반환하고, 네트워크 호출에 실패하면 실패합니다. 네트워크에서 fetch한 결과는 캐시에 저장하지 않습니다.FetchPolicy.networkOnly
: 네트워크에서 fetch한 결과를 반환하고, 네트워크 호출에 실패하면 실패합니다. 네트워크에서 fetch한 결과는 캐시에 저장합니다.
errorPolicy
: Error 시 어떻게 처리할 것인지에 대한 정책을 정합니다.
ErrorPolicy.none
: 응답에 GraphQL 오류가 포함된 경우 네트워크 오류와 똑같이 취급됩니다. 응답에 데이터가 포함되어 있더라도 무시합니다. (기본)ErrorPolicy.ignore
: 에러를 무시합니다. GraphQL 오류와 함께 반환되는 모든 데이터를 읽을 수 있고 캐시에 저장할 수도 있습니다. 에러를 저장하거나 UI에 표시하지 않습니다.ErrorPolicy.all
: 가져온 데이터와 에러를 모두 캐시에 저장합니다. 데이터를 UI에 보여주면서 에러에 대한 정보 또한 함께 보여줄 수 있습니다.
pollInterval
: 쿼리를 계속해서 특정 시간마다 refetch해야 할 경우 그 기간을 쓰면 됩니다. 쓰지 않으면 refetch하지 않습니다.
builder
builder
에서는 쿼리 결과인 QueryResult
및 Future<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 정책이 noCache
나 networkOnly
가 아닌 경우, 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
은 뮤테이션을 실행하는 함수입니다. 안에 인자로 데이터를 넣어줍니다.
그래프큐엘 정말 재미가 있네요~