Slide 1

Slide 1 text

GraphQLを使った共同開発の心構え 〜 フロントエンドの視点から Hatena Engineer Seminar #18 id:utgwkk

Slide 2

Slide 2 text

自己紹介 ● id:utgwkk (うたがわきき) ● Webアプリケーションエンジニア 新規の共同開発案件に携わる ● はてなサマーインターン2019出身 ● はてなブログMediaチーム アルバイトエンジニア (2019/9 - 2021/3)

Slide 3

Slide 3 text

今回の共同開発案件の特徴 ● 開発領域が明確に分かれている ○ はてな: デザイン・フロントエンド ○ パートナー: サーバーサイド・インフラ ● APIのインタフェースにGraphQLを利用 ○ 裏側に複数のマイクロサービスがある ● フロントエンドをSPA (Single Page Application) として実装 ○ TypeScript ○ React ○ Relay (GraphQLクライアント) ● 手元からフロント開発者用のAPIを叩いて開発

Slide 4

Slide 4 text

GraphQL ● https://graphql.org/ ● Facebookが考案した、APIのためのクエリ言語 ● スキーマ定義に基づいたクエリを記述して データを取得・更新する ● スキーマの例 (ブログサービス) type User { id: ID! hatenaId: String! nickname: String! blogs: [Blog!]! } type Blog { id: ID! owner: User! entries: [Entry!]! } type Entry { id: ID! author: User blog: Blog! title: String! content: String! }

Slide 5

Slide 5 text

GraphQLにおけるデータ型 ● 組み込み型 ○ Int, String, Boolean, … ○ ID ● non-nullableな型 T! ○ T はnullableであることに注意 ● 配列型 [T] ○ nullableとの組み合わせに注意 ○ [T!]! として使うことが多い

Slide 6

Slide 6 text

query (データを取得する) ● クライアントから取得したいフィールドを自由に指定できる query GetBlog { blog(id: 1) { owner { hatenaId } entries { title content } } } { "data": { "blog": { "owner": { "hatenaId": "utgwkk" }, "entries": [ { "title": "日記", "content": "こんにちは" } ] } } }

Slide 7

Slide 7 text

mutation (データを更新する) ● 更新後のデータをmutationのレスポンスとして取得できる (queryと同様) mutation PostEntry { postEntry( input: { blogId: 1, title: "aaa", content: "bbb" } ) { entry { id title content } } } { "data": { "postEntry": { "entry": { "id": 100, "title": "aaa", "content": "bbb" } } } }

Slide 8

Slide 8 text

fragment ● 特定の型のフィールドの集合に名前を付けられる fragment AuthorInfo on User { hatenaId nickname } query GetEntry { entry(id: 100) { author { ...AuthorInfo } } }

Slide 9

Slide 9 text

interface ● 特定のフィールドを持つ型であることを示せる interface User { hatenaId: String! } type Visitor implements User { hatenaId: String! } type Author implements User { hatenaId: String! entries: [Entry!]! }

Slide 10

Slide 10 text

directive ● フィールドなどに付ける指示子 ● 例: @deprecated (廃止予定のフィールドに付ける) type Entry { categoryStr: String! @deprecated(reason: "categoriesフィールドを使ってください ") categories: [Category!]! }

Slide 11

Slide 11 text

GraphQL Server Specification ● https://relay.dev/docs/guides/graphql-server-specification/ ● GraphQL自体の仕様とは別の、追加の仕様 ● Nodeインタフェース、nodeクエリ ● connection

Slide 12

Slide 12 text

Nodeインタフェース、nodeクエリ ● Nodeインタフェース ○ id: ID! フィールドで一意に定まる ● node クエリ ○ idをもとに直接取得できる interface Node { id: ID! } type Query { node($id: ID!): Node } type Blog implements Node { id: ID! }

Slide 13

Slide 13 text

connection ● ページングに関する情報をひとまとめにした型 type Blog implements Node { id: ID! entries( after: String, before: String, first: Int, last: Int ): EntryConnection! } type EntryConnection { edges: [EntryEdge!]! pageInfo: PageInfo! } type EntryEdge { cursor: String! node: Entry! }

Slide 14

Slide 14 text

connection ● カーソルベースのページングを書きやすい query BlogTop { blog(...) { entries(first: 10, after: ...) { edges { node { ... } } } } } type EntryEdge { cursor: String! node: Entry! }

Slide 15

Slide 15 text

Relay ● https://relay.dev/ ● Facebookが開発 ● Reactと組み合わせて使えるGraphQLクライアント ● GraphQL Server Specificationに従ったスキーマを要求する

Slide 16

Slide 16 text

クライアントの状態を同期してくれる ● クライアントキャッシュをidフィールドをもとに同期する ● mutationのレスポンスをもとにクライアントの状態を更新する ○ 既存のデータの更新 ○ connectionへのデータの追加・削除 ● データ更新→画面要素の同期 に割く労力を 減らせる mutation PostEntry { postEntry(...) { entry @appendNode(...) { id title content } blog { id entryCount } } }

Slide 17

Slide 17 text

再取得・ページングが簡単に書ける ● 専用のdirectiveとフックを使うと簡単に書ける ● GraphQL Server Specificationの恩恵を受けている const {data, loadNext} = usePaginationFragment(...); return ( <> {data.blog.entries.edges.map(...)} loadNext(10)}>load > )

Slide 18

Slide 18 text

fragmentとコンポーネント分割 ● fragmentとコンポーネントの分割単位を強く紐づける ● コンポーネントが要求するフィールドを知らなくてよい query GetEntry { entry(id: ...) { author { ...EntryFooter_user } } } fragment EntryFooter_user on User { hatenaId nickname }

Slide 19

Slide 19 text

Suspenseをフル活用している ● 「読み込み中」を表現するためにSuspenseを活用 ● useEffectフックで読み込んで……みたいなロジックを書かなくてよい ● React 18のトランジションと相性がよい }> const BlogTop = ({blog: _blog}) => { const blog = usePreloadedQuery(..., _blog); return ...; };

Slide 20

Slide 20 text

新卒入社した時点では ● 初めて触る技術要素が多い ● React ○ ガッツリ触ったことはなかった ○ 本格的なアプリケーションを作るのは初めて ● GraphQL ○ そういえばサマーインターンで触った、ぐらい ● Relay ○ そういうのがあるのか ○ サマーインターンではApolloを使っていた

Slide 21

Slide 21 text

前提知識をつける ● 本を読む ○ 初めてのGraphQL ○ GraphQLの世界観をなんとなく把握する ● Relayのドキュメントを読む ○ クライアントライブラリの世界観を把握する ● 社内のGraphQL有識者に聞く

Slide 22

Slide 22 text

手を動かしてみる ● モックAPIサーバーを作る ● GraphQLクライアントを書いてみる ○ クエリを発行する ○ データを更新する ○ テストを書く ● ペアプロで練習する ○ だいたいこんな感じかな、と会話しながら実装する ● 雰囲気が分かってきた!!

Slide 23

Slide 23 text

機能開発の流れ ● デザイン・仕様をもとにページを仮組みする ● GraphQLのクエリを実装してもらう ● GraphQL APIからデータを取得してページを表示する ● 必要に応じてフィードバック・修正する

Slide 24

Slide 24 text

GraphQL APIを実装してもらって終わり、ではない ● 最初から理想のGraphQLスキーマになるとは限らない ○ ドメイン知識が足りなかった ○ 仕様を修正した結果、表示したいものが増えた ○ etc. ● 実装してもらったGraphQL APIを使って起こったこと・感じたことをフィードバックし て、よりよいGraphQL APIにする

Slide 25

Slide 25 text

フィードバック ● バグ報告 ○ 値がおかしい ○ エラーになる ○ etc. ● GraphQLスキーマへのフィードバック ○ フィールドを修正してほしい ○ スキーマの構造を修正してほしい ○ etc.

Slide 26

Slide 26 text

バグ報告 ● 手元からフロント開発者用のAPIを叩いて開発 ○ ログを確認しづらい ○ 自分で修正できない ● 再現するクエリ例と実行結果を共有する ○ Chromeの開発者ツールでCopy as cURLすると手軽 ● クライアントライブラリの挙動を共有する ○ Relayは id: ID! フィールドをキーとしてクライアントの状態を正規化する ○ Relayはmutationの返り値をもとにクライアントの状態を更新する ○ etc. ● スムーズに状況を把握してもらえる

Slide 27

Slide 27 text

スキーマへのフィードバックの心構え ● どういう観点でスキーマへのフィードバックを行っているのかを紹介 ● 心構えからスキーマの設計方針まで

Slide 28

Slide 28 text

ベストプラクティスに寄せていく ● 社内や世間の事例を参考にする ● GitHub GraphQL API ● 本 ○ GraphQLスキーマ設計ガイド 第2版 ● チュートリアル ○ ShopifyのGraphQL API設計チュートリアル

Slide 29

Slide 29 text

高速なフィードバックループ ● 高速にフィードバックループを回したい ○ 実装してもらったAPIを使ってフィードバック、が早いとよい ● スキーマの段階でフィードバックできることがあれば伝える ○ 懸念を先に伝えることで手戻りを減らす

Slide 30

Slide 30 text

nullableにするかどうか ● 基本的にはnon-nullだと分岐が減らせてありがたい ● nullになる場合があるならドキュメントに書いてもらう type Entry { """記事を投稿したユーザー (ユーザーが退会済なら null)""" author: User }

Slide 31

Slide 31 text

Nodeインタフェースを実装するかどうか ● idがあると便利 ○ キャッシュの同期 ○ 直接取得できる ● idを持たせられない場合もあるかもしれない ○ 付随的な情報である ○ 直接取得するのが難しい ● 直接取得したいものは必ずNodeインタフェースを実装してもらう

Slide 32

Slide 32 text

配列にするかconnectionにするか ● connectionにしておくと便利なことが多い ○ ページング ○ mutation発行後のデータ同期 ● 用途によっては配列でもよいかもしれない ○ データ量がじゅうぶん少ない ○ GraphQL API経由の追加・更新が発生しない ○ 付随的な情報である ● フロントエンドとしての実装しやすさとのバランス

Slide 33

Slide 33 text

合意をスキーマに落とし込む ● 自分から見た値が取れると書きやすい場面が多い ○ 例: リポジトリにスターを付けているか、ブログの記事を編集できるか ○ フィールドを追加してもらうとよい ● author.id === viewer.id みたいなロジックを書かない ● フロントエンドで値を作り上げようとしない ○ サーバーサイドとの合意をスキーマに落とし込むべき type Entry { """author.id === viewer.id と等価?""" canEdit: Boolean! }

Slide 34

Slide 34 text

語彙や認識を揃える ● 議論していて話が噛み合わないと思ったらまず整理する ● 仕様書を参照する ● GraphQLスキーマとコンポーネントの語彙が揃っていると議論しやすい ● なんでも無理に英語にしなくてもよいと思う ● 固有名詞が出てきたときにどうするか ○ 固有名詞をそのまま使う方向で進めている ○ 一般的な概念の名前に寄せると、後から renameしなくて便利かも?

Slide 35

Slide 35 text

相談も受ける ● こちらからフィードバックするだけではない ● スキーマやサーバーサイドの実装についての相談を受けることもある ● いろいろな観点で考えてから返事する ○ フロントエンドとしてはどうあるのが望ましいか ○ サーバーサイドで実現可能か ○ 効率が良いか

Slide 36

Slide 36 text

フィードバックを反映してもらったら ● フロントエンド側のGraphQLスキーマを更新する ○ get-graphql-schema, GraphQL Code Generator ● スキーマ更新反映を1つのPRにすると吉 ○ スキーマの差分が確認しやすい ● 非互換変更に対応する ○ 本番運用中なら @deprecated directiveを付けてもらうと思う ○ タスクを切ってTODOコメントを書き残して仮修正する ■ あとで腰を据えて修正するとごちゃごちゃになりにくい

Slide 37

Slide 37 text

まとめ ● 臆さずに学び続ける姿勢を持つ、インプットを欠かさない ● フロントエンドからサーバーサイドに対して働きかける機会を逃さない ● 最高のサービスを提供するために、最高のGraphQLスキーマについて考える