クラウド債務支払開発チーム でバックエンドエンジニアをしているいいねです。
みなさんGraphQLは使っていますか?
自分は普段バックエンドの開発を行っているのですが、最近クライアント実装の機会がありました。 そこで、バックエンド開発との特性の違いや、開発において意識すべきポイントが結構あるなと感じました。 それに伴って近年GraphQLが求められるようになった理由を実感したので記事を書いてみました。
「普段バックエンドの開発をしているけど、REST APIをGraphQLに変えることで何が嬉しくなるのかがイメージできない」 という悩みを抱えている方にとって、クライアント実装時の課題とGraphQLが解決するものを理解する手助けとなれば幸いです。
クライアント実装側に立って分かった3つの課題
バックエンドエンジニアの自分がクライアント側の開発を行った際に、クライアント実装時の特性として3つの課題を強く感じました。 これらはGraphQLが何故使われるようになったかを理解するために重要なポイントとなります。 一つずつ詳しく見ていきましょう。
1. 通信処理はなるべく減らしたい
通信処理が増えるたびに考慮すべきことが増加します。
通信処理を行う場合、多くは「画面に表示する」などの何かしらの目的が伴います。 ただリクエストを実行すれば良いという訳ではなく、付随して状態管理への反映などが必要になります。
以下のよくあるクリックイベントを例にします。
# ユーザーのクリック時にデータを取得して、取得内容を表示する機能 1. ユーザークリックをハンドルする関数を実行する ・ handleClick()を実行する 2. HTTPリクエストを実行してデータを取得する ・ fetch() を使ってHTTPリクエストを発行する 3. データを状態管理オブジェクトにマッピング ・ 取得したデータを表示先のコンポーネントにバインドされている状態管理オブジェクトに反映する 4. 状態管理オブジェクトを表示に反映 ・ データ表示先のコンポーネントを再描画して状態管理オブジェクトの変更を反映する
このように書くとシンプルな機能に見えますが、 実際はそれぞれの項目で考慮すべきことがあり、以下のようになります。
# ユーザーのクリック時にデータを取得して、取得内容を表示する機能 1. ユーザークリックをハンドルする関数を実行する → 処理中にユーザーが他の操作を行っても構わないか 2. HTTPリクエストを実行してデータを取得する → HTTPリクエスト中に他の処理を行うことができるか 3. レスポンスを状態管理オブジェクトにマッピング → 状態管理オブジェクトの変更時のhookは実行されても構わないか 4. 状態管理オブジェクトを表示に反映 → 表示に反映させるタイミングは即時か、それともその他の処理を待ってからか
単純に「 ユーザーのクリック時にデータを取得して、取得内容を表示する機能」というだけでもこれだけの要素の複合を考慮する必要があります。 もしデータ取得を複数回のリクエストで実現しなければならない場合、その回数分だけ複雑さが更に掛け算されていくでしょう。
これらの複雑さは認知負荷が大きく、思わぬ不具合の原因にもなります。 可能な限り通信処理を減らしてシンプルな処理フローに抑えた方がいいでしょう。
また、リクエストが1回増えるということは通信待ち時間が発生することでもあります。 ユーザーインタラクションを損なわないためにも、通信処理を最小限にすることが望ましいでしょう。
実装の点で見ると、通信処理があると単体テストを書く難易度が上がります。 通信処理をそれぞれMockしたりすることも結構な労力となります。
2. 状態管理の複雑さを減らしたい
クライアント実装では「ユーザーが何の操作を行ったか」「各コンポーネントがどのような状態であるか」などを把握する必要があり、それは「状態管理」と呼ばれます。
バックエンド実装における状態管理は「ログイン処理」や「トランザクションの永続化」など1つのビジネスロジックの粒度で行うのに対して、 クライアント実装における状態管理は前述の通り、「1つのビジネスロジックを成立させるまでの状態の変化」を取り扱う為に、とても粒度が細かいです。 したがって、状態変更の数自体がバックエンド実装に対して非常に多く、それをどのようにハンドリングするかはクライアント実装における大きな課題の一つです。
また、「状態Aの変更を検知して、状態Bのイベントを発火させる」というように複数の状態間に依存関係を持つこともあります。 「明細の金額を変更した際に、ヘッダーの総計・小計を変更する」と読み替えてもらえると分かりやすいかと思います。 このような状態同士の依存関係が増えると機能の把握が難しくなります。 クライアント実装においては、状態同士をなるべく疎結合にして依存関係をシンプルに抑えることが非常に重要になります。
しかし、通信処理を増やすということは管理するべき状態が増えるということでもあります。
「金額取得APIで取得した金額を画面に表示する」場合をイメージしてください。 API呼び出しでエラーが発生したらどうしますか? 既に取得済かどうかを気にする必要はありませんか?
前項目でも触れた、通信処理が増えることによる複雑さの爆発は状態管理においても発生します。
3. APIエンドポイントが増える度に発生する実装を減らしたい
バックエンドに新しいエンドポイントを提供した時に、クライアント側はただエンドポイントを呼び出すロジックを書けばいいという訳ではありません。 多くの場合、専用のリクエスト用クラスとレスポンスの型定義を実装することになるでしょう。
これについてはバックエンドの文脈でもよくあるので想像しやすいかもしれません。 別プロダクトとの連携をするシーンなどを思い浮かべてください。
しかし、クライアント実装において必要なエンドポイントが増える、というシーンはバックエンドより頻繁に発生します。 クライアント側に1つ機能を増やすたびに1つ以上のエンドポイントが増える、ということも珍しくありません。 エンドポイントが増える度に、エンドポイントのリクエスト用クラスとレスポンスの型定義、そして自動テストを用意するのは大きな手間です。 できる限りそのような手間は減らして、より迅速なリリースを目指したいはずです。
GraphQLが解決する課題
前述のクライアントの課題に対して、GraphQLの以下の特性がうまく機能します。
1. 共通のエンドポイントを用いることでリクエストを1回にまとめることができる
図書館のポータルサイトを実装する場合を例にします。 一つのページに「利用ユーザーの予約状況」「本の最新入荷リスト」を表示させたい場合をイメージしてください。
この要件の実現の仕方について、REST APIとGraphQLのそれぞれで見ていきましょう。
REST API:リソース毎にエンドポイントを用意するパターン
「利用ユーザーの予約状況」エンドポイント と 「本の最新入荷リスト」エンドポイントをそれぞれ用意して、クライアントはそれぞれに対してリクエストを行う # 「利用ユーザーの予約状況」 GET /reservations/{user_id} # 「本の最新入荷リスト」 GET /latest_books
このパターンはリソース毎にエンドポイントが分かれているので、その他の画面でもエンドポイントを再利用できることがメリットです。
しかし、エンドポイントの取得内容を増やした場合に、利用している全ての箇所で不要な取得内容を取得しなければならなくなることが課題となります。
また、クライアントが2回リクエストを行う必要があり、
クライアント側の課題 1. 通信処理はなるべく減らしたい
で言及した問題が発生します。
REST API:機能に特化したエンドポイントを用意するパターン
「利用ユーザーの予約状況と本の最新入荷リスト」エンドポイントを用意して、クライアントは1回リクエストを行う # 「利用ユーザーの予約状況と本の最新入荷リスト」 GET /reservations_and_latest_books/?user_id={user_id}
二つ目のこのパターンはクライアントのリクエスト回数は1回で済みますが、 エンドポイントの機能を特化しすぎている為に、エンドポイントを他の画面で再利用することは難しそうです。
GraphQLを用いる場合
GraphQLを用いる場合、REST APIの前述の2つのパターンのいいとこ取りをすることができます。
GraphQLサーバーを提供する場合、リクエストを受け付けるエンドポイントを共通にすることがきます。(スキーマ毎にエンドポイントを分けることもできますが) そして、以下のように一度のリクエストで複数のリゾルバに対して問い合わせを行うことが可能です。
query { # 利用ユーザーの予約状況 reservations(userId: "***") { id, reservedAt, book { id, name } }, # 本の最新入荷リスト latestBooks { id, name, arrivedAt } }
query
に reservations
と latestBooks
という二つのリゾルバ(GraphQLサーバーに定義された関数)が記載されています。
1度のリクエストで「利用ユーザーの予約状況」と「本の最新入荷リスト」を取得する、という点では 「REST API:機能に特化したエンドポイントを用意するパターン」
と同じですが、
リゾルバが reservations
と latestBooks
とそれぞれ独立しているため、他の画面でもreservations
のみ利用する、ということが可能です。
「クライアント実装側に立って分かった3つの課題 > 1. 通信処理はなるべく減らしたい」
で言及した通り、リクエストの回数が増えるたびに複雑さが増していく為、このようにリクエストをまとめることは処理をシンプルにする助けとなります。
加えて、GraphQLではリゾルバが返す内容を必要なものだけに絞らせることができます。
上記ではreservations
で取得する内容をid
, receivedAt
, books
と明確に指定しています。
たとえ別の機能の為にreservations
の取得できる項目を拡張したとしても、この形でリクエストを実行する限りは取得内容は変わらないため、「REST API:リソース毎にエンドポイントを用意するパターン」
で問題となっていた不要な取得内容の取得は起きません。
2. Apollo Clientにリクエスト・レスポンス・状態管理を委譲することができる
GraphQLをクライアントが利用する際に、Apollo Clientという便利なGraphQLのクライアントライブラリを利用することができます。
GraphQLはエンドポイントに決まった形でリクエストを投げると利用できるので必ずしもApollo Clientを用いる必要はないのですが、後述の機能が課題と向き合う上で便利だと感じたのでご紹介します。
リクエスト・レスポンス解析の役割としてのApollo Client
Apollo ClientはGraphQLのクライアントとしての役割があります。 その為、Apollo Clientを利用するとエンドポイントにリクエストを投げる為のクラスを自前で用意する必要はありません。
また、サーバー側がSchema情報を提供していれば、GraphQLのリクエスト・レスポンスの型定義クラスはApollo Clientが自動生成してくれます。
「クライアント実装側に立って分かった3つの課題 > 3. APIエンドポイントが増える度に発生する実装を減らしたい」
で言及した、エンドポイントを増やすたびに増える追加実装を省略できます。
状態管理ライブラリとしてのApollo Client
Apollo Clientは単なるGraphQLのクライアントではなく、状態管理ライブラリでもあります。
- クライアントがローカルに持つ情報
- リモートから取得する必要のある情報
- リモートから取得した情報のキャッシュ
を一元的にApollo Clientは管理することができます。 そして、ローカルの状態とリモートの状態は共通してGraphQLのクエリ構文で取り出すことができます。
query { # ログイン状況(クライアントがローカルに持つ状態) isLoggedIn, @client # 利用ユーザーの予約状況 reservations(userId: "***") { id, reservedAt, book { id, name } }, # 本の最新入荷リスト latestBooks { id, name, arrivedAt } }
これによって、クライアント実装時にローカルとリモートの情報を一括で取得することができ、個別に取り出す処理を実装することを避けられます。
また、GraphQLのSchemaを元にした型定義をローカルの情報においても用いることができるため、ローカルとリモートで別々に型定義をする必要はありません。
直接的に状態管理の複雑さを減らしてくれるわけではありませんが、前述の 「クライアント実装側に立って分かった3つの課題 > 2. 状態管理の複雑さを減らしたい」
で触れた通り状態管理というのはとにかく複雑化しやすいため、状態の取り回しが良くなることは複雑な状態管理の助けになります。
まとめ
バックエンドエンジニアが感じたクライアント実装のポイントとGraphQLのニーズ、いかがだったでしょうか。 GraphQLはREST APIの延長線上ではなく、クライアント実装の課題を解決するためのアプローチであることが伝わりましたでしょうか。 クライアント実装側の抱える課題を意識することでGraphQLが広く採用されている理由が理解しやすくなります。
この記事がバックエンドとクライアントの連携について見直すきっかけになると幸いです。
今回の内容をより深く学びたい方はGraphQL入門書として評価の高い「Production Ready GraphQL」をお薦めいたします。 マネーフォワード福岡拠点でもエンジニアで輪読会を行っており、GraphQLについての知識が網羅的に学べるためとても良い本です。
マネーフォワード福岡拠点ではエンジニアを募集しています! 一緒にGraphQLを使ってより良いサービスを作りませんか?
カジュアル面談もお気軽にご応募ください!