とろろこんぶろぐ

かけだしR&Dフロントエンジニアの小言

React 時代に選ぶ GraphQL

概要

先日新規の Web サービス開発でフロントエンド側の技術選定を行いました。 利用する技術の中で GraphQL を提案した際に、RESTful な API で呼び出す方法と比較して納得感がないという意見があがりました。 そこで、なぜ、どういうときに GraphQL を選定すべきだと思うか、文章にして自分なりにまとめておこうと思います。

前提

構成が BFF か BE かで意見は大きく変わりません。 例えば BFF として利用されるケースでは、バックエンド側には BE チームとマイクロサービス的な API が存在しており、 BFF として GraphQL を配置するようなケースです。GraphQL のリゾルバは API を叩きます。 一方、 BE として利用されるケースとは、リゾルバが直接 DB を叩くような形です。 今回はフロントエンドのチームが管理する BFF として、JS のみで GraphQL が利用されることを想定して記載します。

先に結論

まず結論から述べておくと、以下のような条件が重なった際に GraphQL が良いと思っています。

  • Web フロントエンド側の UI がリッチで複雑
  • UI 要件をアジリティ高く柔軟に変更したい
  • Web に精通した技術力の高いエンジニアがいる

この理由を順を追って説明していきます。

Web UI の複雑性

静的な UI から動的な UI へ

従来 Web ページといえば、1 URL に 1 画面の UI がカッチリ決まっているものがほとんどでした。 より UI がリッチで複雑な Web アプリが登場し、 jQuery から React へと技術的な進化が起きました。 現在 Web は多様なサービスを提供しており、ニュース記事を配信するような静的なサイトから表計算を行う動的なアプリケーションまで様々です。 動的な Web アプリケーションは 1 URL に 1 画面という考え方に限定されず、 1 つの操作や 1 つのコンテキストが URL として定義されることもあります。

例えば、ある画面を表示したままモーダルを表示するような UI も複雑になりやすいものの一つです。 モーダルが表示された状態を URL は別で定義しつつ、同じ画面上に JS でモーダルを動的に表示します。 そのモーダルが表示されている画面とモーダルが表示される前の画面は、別の画面か同じ画面か定義するのは難しいかと思います。

コンポーネントベースの UI 設計で保守性を高く保つ

jQuery から React への進化は、複雑化した UI 要件にまで広く対応できるよう保守性が高く柔軟に利用できる技術として進化しています。 詳しいところは React について述べている記事や本がたくさんあるので、調べてみてもらえればと思います。

一つ特徴を取り上げると、再利用性を考慮しコンポーネントベースに Web アプリケーションを開発しやすい点が挙げられます。 複数の画面で共通して利用される部品を React の UI コンポーネントとして開発し、開発効率と保守性を高めることができます。 React では適切にコンポーネント設計を行うことで、保守性高く Web アプリケーションを開発できます。 例えば、共通のモーダル用のコンポーネントを作っておきます。 そうすることで、複数の画面で同じモーダルが使われる要件だとしても、画面ごとに新たにモーダルコンポーネントを作る必要はありません。

参考: React を深く知るための入り口

コンポーネントベースの設計とRESTful API の相性

ただしコンポーネントベースの設計はあくまで UI 設計であり、バックエンド側の DB に入ったデータ構造とは関係ありません。 バックエンド側の DB に入ったデータを UI に展開するために、大抵の場合 RESTful な API からデータを取得して UI に反映する作業が必要です。

例えば、店舗の名前をトップページのタイトル部分に表示したいとします。 タイトルは表示したいテキストをもらって適切なスタイルで表示する UI コンポーネントなので、おおよそこんな感じです。

type Props = {text: string};

const Title: React.FC<Props> = ({ text }: Props) => {
  return <div className="title">{text}</div>;
};

店舗データとして店名等のデータが DB に含まれており、 GET /shops で店舗情報が取得できる RESTful な APIが用意されているとします。

$ GET /shops/1
{
  name: "店名",
  tel: "0120-XXXX-YYYY",
  owner: { ... }
}

API から返却されたレスポンスに含まれる店名 name を Title の text 引数に渡すことで、店名をトップページのタイトルに表示できます。 リソースごとに定義された RESTful な API と個々のコンポーネントには直接的な関係がない方が健全です。

リソースごとにレスポンスを返す RESTful な API だけで UI を構築しようとすると、大抵の場合 1 画面を表示するために複数の API 呼び出しが必要になります。

例えば、トップページで店舗情報と店舗が販売している商品情報を表示する場合、店舗 API と商品 API を呼び出すことになります。 複数の API をブラウザから直接呼び出すことになるので、その分 NW コストがかかります。

さらに、API が返すレスポンスが UI に必要なデータと必ずしも一致しているとは限らないので、いわゆるオーバーフェッチやアンダーフェッチによる NW コストも発生し得ます。

参考

GraphQLでバックエンドのコードをすっきりさせた話 - LayerX エンジニアブログ

Why not use GraphQL? - WunderGraph

BFF で UI と API リソースを仲介する

画面単位と API の単位が合わないケースに対応するために BFF を利用することができます。 BFF では複数のリソースから UI を意識した API に結合することができます。これは API アグリゲーションと呼ばれます。 一応補足しておくと BFF の利用用途はこれに限りません。

参考: BFF(Backends For Frontends)の5つの便利なユースケース:マイクロサービス/API時代のフロントエンド開発(2) - @IT

複数の API リソースを集約して 1 画面に使われるデータを 1 API にまとめたものをページ API と呼ぶことにします。 ページ API を作ることで、複数個の API 呼び出しが不要でページに必要なデータだけを取得できるので、その分 NW コストがかかりません

ちなみにこの記事ではバックエンド側でページ API が開発されるケースは想定していません。 そのような開発が適しているのは、ユースケースが固定的だったり、小規模なチームで BE / FE を一気通貫で作りきることができるスモールプロダクトに限定されると思っています。 個人的には、この方法ではサービスの拡張性が低いと思っているためです。

コンポーネントベースの設計とページ API の相性

1 画面 1 API としてページ API を用意できたとしても、コンポーネントは複数画面に跨っています。 例えば、極端な例で言うと、全ての画面でヘッダーコンポーネントを共通で利用しているとします。 ヘッダーに新たに API から取得したキャンペーン情報を載せたい場合、 1 画面 1 API で構築されていたら全画面向けのページ API にキャンペーン情報を新たにアグリゲーションする変更を加える必要があります

これは極端な例なので、書き方を工夫すればある程度解消はできる問題ではあります。 ただし根本的な考え方として、1 画面 1 API という考え方と、コンポーネントベースに積み上げられた複雑な UI 構成がミスマッチになりつつあることを意識しておくと良いと思います。

コンポーネント API の可能性

UI がコンポーネントベースであるならば、 API も 1 画面 1 API の代わりに 1 コンポーネント 1 API にできるように思います。 しかし、これは別の問題が発生します。

まず、"1 コンポーネント" と言われたときに "コンポーネント" の粒度が様々であるということです。 Atomic Design などコンポーネント入れ子になった考え方で作られる以上、コンポーネントの設計時に API からデータを取得するべきコンポーネントとそうでないコンポーネントを分けて考える必要があります。 これは API の粒度もマチマチになってしまう可能性があるということです。 どの粒度以上のコンポーネントには API を用意し、どの粒度以下のコンポーネントには用意しないのか、ルールの定義が難しく保守性の観点でデメリットがあります。

さらに、1 画面内に配置された複数のコンポーネントで同じデータを使うことがあり得るため、同じデータを重複して取得し、重複して保持することになります。 もともとリソースごとの RESTful API の時に懸念した NW コストが高い問題が出てきます。

例えば、ヘッダーコンポーネントとフッターコンポーネントが同じ画面に配置されていたとします。 ヘッダーでもフッターでも、API から取得した店名を表示したい場合、ヘッダー API とフッター API の両方で店名を取得する必要が出てしまいます。 コンポーネントごとに API を用意するのも、あまり得策とは言えません。 RESTful な API で設計する場合は、ページ API で 1 画面 1 API をベースにしつつ、さらに画面での操作やコンテキストを元にユースケースごとに API を定義することで、設計することが適切になりそうです。 これをユースケース API と呼ぶことにします。

これまでの API と GraphQL

GraphQL は、UI 側で必要なデータをクエリとして記述し、その記述のまま取得して扱うことができます。 ユースケース API はあくまでもバックエンド側(上記の例では BFF)のものなので、バックエンドでユースケースを意識する必要があります。 どの画面にどのデータが必要か定義した上で、そのユースケースごとに API を開発します。

一方 GraphQL は極端に言うと、バックエンド側では /graphql という一つのエンドポイント API だけ開発すれば良く、バックエンド側でユースケースを意識する必要はありません。 実際には GraphQL のリゾルバで API でデータ呼び出しを行います。 GraphQL で改めて Schema を定義しておくことで、 UI で Schema として定義したプロパティを自由に組み合わせて呼び出すことができます。 UI 側、つまり UI コンポーネントでは必要なデータを必要な場所でクエリとして定義します。 これは Fragment Colocation と呼ばれます。 個々のコンポーネントで必要なデータを取得する部分的なクエリ(Fragment)を、コンポーネントに近い場所へ配置する(Colocation)ものです。

参考

GraphQL の Fragment Colocation について

GraphQLとクライアントサイドの実装指針.md · GitHub

使い方としては、上記で示したコンポーネント API に似ています。 コンポーネント API と異なる点は2点です。

1点目は、コンポーネントを意識してバックエンド (BFF) 側で API を用意する必要がない点です。 コンポーネントの粒度を気にして API 設計のためにルールを用意する必要はなく、コンポーネントで必要なデータを必要なだけクエリとして定義するだけで良いです。

2点目は、コンポーネントごとに同じリソースを使う場合でも API 呼び出しに重複が起きない点です。 例えば GraphQL の Relay というライブラリでは、コンポーネントごとに定義したクエリを実際にページから呼び出す際に 1 つの大きなクエリにアグリゲーションした上で重複を排除して呼び出す仕組みがデフォルトで機能に含まれています。

これまでの API よりも GraphQL の方が現代の React で培われているコンポーネント設計に合っていることがわかると思います。

閑話休題: 他のフレームワーク

GraphQL で実現していることを簡単に言うと、

となります。

他の対応策やフレームワークでの対応状況を軽く触れておきます。

React

React Server Components も、似た問題点を解決する方法となっています。 React は UI フレームワーク内の仕組みとして、 React Server Components を実装中です。 これは、あらかじめ UI コンポーネントがサーバー側 (BFF) でレンダリングするものかどうかを指定できるものです。 React UI コンポーネントAPI のデータフェッチが必要なコンポーネントは、React Server Components として定義しておくことで、API フェッチを BFF で済ませた状態にでき、ブラウザ側からのデータフェッチを不要とすることができます。

参考

Introducing Zero-Bundle-Size React Server Components – React Blog

RFC: React Server Components by josephsavona · Pull Request #188 · reactjs/rfcs · GitHub

React Server Components 総まとめ

Next.js

React を使った Web ページを簡単に構築できる Next.js は、サーバとしての機能も備えた一気通貫フレームワークとして人気を博しています。 All in One の仕組みをほぼ設定のチューニングなしに JavaScript のみで書き上げることができます。 Next.js の登場により Web フロントエンドは React でブラウザで動作する Web ページを構築するだけでなく、Web ページを配信するサーバ機能までをフロントエンドエンジニアが簡単に扱えるようになりました。 近年では Node.js での BFF の導入も Next.js をデプロイサービスの Vercel や GCP の Cloud Function にデプロイするだけで、簡単に使えるようになってきています。

Next.js はどちらかというと静的な Web ページを構築するものとして利用されることが想定されたフレームワークになっており、デフォルトで提供されているバックエンドの API データ取得機能に柔軟性が高いとは言えませんでした。

例えば、 Next.js の getServerSidePropsgetStaticProps などの API は、簡単に言うと 1 画面に必要なデータをまるごと取得しページに渡す機能となっており、ページ API 的な考え方になっています。 これは、記事のような 1 画面にコンテンツが集約され、名の通り Static 、つまりあらかじめ静的に用意しておけるような画面には有用だと思います。

今後は Nested Layout の RFC が登場し、 GraphQL で実現できていたことが Next.js でも近いことができるようになります。 これまで Next.js の画面遷移は、異なるページへの遷移時に画面に必要なデータ、コンポーネントを全て取得し直す形になっていました。 Nested Layout は名の通り Layout を入れ子に定義できるものになります。 これにより画面上の遷移や操作時に必要最低限のコンポーネントを UI 上で変化させることができるようになります。 つまり、コンポーネント API 的な API を不要に呼び出してしまうことはなくなります。

参考 Blog - Layouts RFC | Next.js

Remix

Remix について詳しくないのですが、 Remix でも近しいことが考えられているようです。 Remix は Next.js と同様の Web 開発用のフレームワークです。 Next.js ほど多く普及しているわけではありません。 詳しい人はぜひ教えてください。

参考 Remix | Data Loading

フレームワーク状況のまとめ

これらのように GraphQL で先進的だったコンポーネント指向の API データ取得方法の改善が進められ始めています。 GraphQL の利点はそれに限定されませんが、 GraphQL を選択する強い理由とは言えなくなるかもしれません。

ただエコシステムとしての成熟度は GraphQL の方が進んでいることは変わらない事実です。

アジャイルな開発と GraphQL

ユーザーが求める UI の進化に追いつくためのアジャイル

Web フロントエンドの UI がリッチで複雑なものになってきているのは、ユーザーがそれを求めているからに他なりません。 さらに開発手法としてアジャイルな考え方が広がっている一つの理由は、ユーザーが求める UI に日々変化があるからです。 ユーザーに提供される UI は様々なサービスで UX が進化し、ユーザーにとっての当たり前は日々アップデートされ、Web フロントエンド開発者は更なる UI 改善を日々求められるようになっています。 このようなよりリッチで複雑な UI へのアジリティ高く改善を進めるために、フロントエンドチーム単体で改善を主導しリリースできることが重要になっていくものと考えています。

リリースサイクルを高める GraphQL

GraphQL のメリットは UI 側で必要なクエリを定義することができる点です。 コンポーネントに対するクエリの変更だけで修正が済みます。 つまり不必要に API 側に手を入れる必要がなく UI に対する変更量を必要最低限に抑えることができます。 フロントエンドチームだけで改善のための開発からリリースまでのサイクルをより速く回すことができます。

バックエンド側の DB カラムや API の変更も必要となるケースでは、コンポーネントだけでは修正は留まらず BFF 側のリゾルバの変更も必要になります。 実際には上記のような変更量が大きいものよりも、 UI に対する細かい修正や A/B テストのようなケースの方が多いのが現実です。 例えば、ヘッダーで店名を表示するかフッターで店名を表示するべきか A/B テストをする場合であったり、ヘッダーに店名に加えて電話番号を表示すべきかどうかであったりといったケースが考えられます。

チームで工数管理が行われている現場なら、変更量がイコール工数にあたることになり、与えられた工数から修正要件のスプリント管理が行われていると思います。 変更量が少ないということは、スプリント期間内に含めることができるタスクの量が増やせることになります。

GraphQL と型生成

最近では、 Web フロントエンド開発で保守性を高めるために TypeScript で型付けすることがスタンダードになりつつあります。 GraphQL はあらかじめ Schema を定義しておくことで、BFF でのリゾルバと呼び出すクエリの型を自動生成することができます。

RESTful な API では、 API のパス、リクエスト、レスポンスに型を定義する必要があります。 ライブラリによるサポートはありますが、現時点ではエコシステムとして成熟しているとは言い難い状況です。 一方、 GraphQL では型の自動生成が GraphQL のエコシステムとして組み込まれた状態で登場し発展しています。 TypeScript スタンダードな時代に対しても、 GraphQL は開発効率と保守性で比較して優位な状態と言えます。

参考: TypeScript GraphQL Code Generator – Generate GraphQL Types with Apollo Codegen Tutorial - Apollo GraphQL Blog

GraphQL とサービスのエンハンス

将来的なサービスのエンハンスにも有用であると考えています。 例えば、 Web だけでなくネイティブアプリにも展開するケースです。 ネイティブアプリではアプリ向けに UI やサービス要件が異なっていたとしても、 GraphQL で必要なデータをクライアント側で定義することができれば、新たにバックエンドを修正する必要がないかもしれません。 または、パブリックな API としての提供にも GraphQL は有用です。

つまり基本的な考え方として、クライアントやユースケースによってバックエンド側の修正が必要ないため、将来的なクライアント側の変更に強いという特徴があります。

技術力の必要性

GraphQL はすでに世界中の様々なサービスで使われています。 開発元の Facebook、パブリックな API を GraphQL で公開している GitHub、国内ではメルカリや Yahoo など、利用シーンが増えてきています。 そうは言っても、圧倒的に従来の RESTful な API で Web サービスが構築されることの方が多いのは言うまでもありません。 つまり技術者がまだ多くないため、世の中に出ている記事やサンプルも多くありません。

ただ、この点に関して2点だけ注意して考えてもらいたいことがあります。

  • 問題の本質は記事を読めばわかるとは限らないこと
  • 要件が複雑なら技術力が高いエンジニアが必要ということ

問題の本質は記事を読めばわかるとは限らないこと

1点目は、世の中に出ている記事に書かれた内容がすべて正しいとは限らないということです。 Web 開発をしているとすんなりコードが動作するケースの方が稀です。 大抵の場合問題点にぶちあたります。 それを解決するために似たような問題を解決した人がいないかググる、ということが多く見られます。

例えば、 Qiita の記事が出てきて、何らかのプラクティスが書き示されているものを手放しで信用してコピペする人がいます。 僕は自分のメンバーに対してそのような対応を推奨していません。 問題が解消されたように見えたとしても、別の問題に置き換えているだけかもしれません。 実際に問題の本質は何なのか、紹介されているプラクティスでなぜ解決できているのか理解しなければ、解決策として認めません。

この時実際に問題の本質は何かを調べるときどうすればいいかというと、結局問題が発生しているコードを読むしかありません。 必要であればライブラリやフレームワークの中身までです。 そうなると、GraphQL が使われている記事やサンプルコードが RESTful API が使われているものより少ないというようなことは、重要な問題ではありません。 ライブラリやフレームワークの信頼性や保守性が高いコードであることの方が重要です。

要件が複雑なら技術力が高いエンジニアが必要ということ

もう一点は、Web UI の要件が複雑なものはいずれにせよ技術力が高いエンジニアが必要ということです。 要件が複雑なものに GraphQL は向いていると書きましたが、それ以前に複雑性の高い UI 要件をメンテナブルな状態で維持するためには、保守性高く設計する必要があります。 言うまでもなくこれにはフロントエンドに精通した技術力が必要です。

「リッチで複雑な UI 要件にはしたいが、 GraphQL を使えるほど優秀なエンジニアがいない」状態なら、 GraphQL を選ばない選択をするのではなく UI 要件を削り簡素なものにする選択をすべきです。

「リッチで複雑な UI 要件にはしたくて GraphQL を使える優秀なエンジニアもいる」状態であるとき、はじめて GraphQL か RESTful API かどちらを選ぶべきかの議論ができると思っています。

さいごに

GraphQL に対する個人的な考えをまとめました。

もし認識が間違っている点があればご指摘ください。