Upgrade to Pro — share decks privately, control downloads, hide ads and more …

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

9e71410aa424ad8c6f179dd10e3850ef?s=47 utagawa kiki
January 27, 2022
1.7k

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

9e71410aa424ad8c6f179dd10e3850ef?s=128

utagawa kiki

January 27, 2022
Tweet

More Decks by utagawa kiki

Transcript

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

  2. 自己紹介 • id:utgwkk (うたがわきき) • Webアプリケーションエンジニア 新規の共同開発案件に携わる • はてなサマーインターン2019出身 •

    はてなブログMediaチーム アルバイトエンジニア (2019/9 - 2021/3)
  3. 今回の共同開発案件の特徴 • 開発領域が明確に分かれている ◦ はてな: デザイン・フロントエンド ◦ パートナー: サーバーサイド・インフラ •

    APIのインタフェースにGraphQLを利用 ◦ 裏側に複数のマイクロサービスがある • フロントエンドをSPA (Single Page Application) として実装 ◦ TypeScript ◦ React ◦ Relay (GraphQLクライアント) • 手元からフロント開発者用のAPIを叩いて開発
  4. 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! }
  5. GraphQLにおけるデータ型 • 組み込み型 ◦ Int, String, Boolean, … ◦ ID

    • non-nullableな型 T! ◦ T はnullableであることに注意 • 配列型 [T] ◦ nullableとの組み合わせに注意 ◦ [T!]! として使うことが多い
  6. query (データを取得する) • クライアントから取得したいフィールドを自由に指定できる query GetBlog { blog(id: 1) {

    owner { hatenaId } entries { title content } } } { "data": { "blog": { "owner": { "hatenaId": "utgwkk" }, "entries": [ { "title": "日記", "content": "こんにちは" } ] } } }
  7. 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" } } } }
  8. fragment • 特定の型のフィールドの集合に名前を付けられる fragment AuthorInfo on User { hatenaId nickname

    } query GetEntry { entry(id: 100) { author { ...AuthorInfo } } }
  9. interface • 特定のフィールドを持つ型であることを示せる interface User { hatenaId: String! } type

    Visitor implements User { hatenaId: String! } type Author implements User { hatenaId: String! entries: [Entry!]! }
  10. directive • フィールドなどに付ける指示子 • 例: @deprecated (廃止予定のフィールドに付ける) type Entry {

    categoryStr: String! @deprecated(reason: "categoriesフィールドを使ってください ") categories: [Category!]! }
  11. GraphQL Server Specification • https://relay.dev/docs/guides/graphql-server-specification/ • GraphQL自体の仕様とは別の、追加の仕様 • Nodeインタフェース、nodeクエリ •

    connection
  12. Nodeインタフェース、nodeクエリ • Nodeインタフェース ◦ id: ID! フィールドで一意に定まる • node クエリ

    ◦ idをもとに直接取得できる interface Node { id: ID! } type Query { node($id: ID!): Node } type Blog implements Node { id: ID! }
  13. 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! }
  14. connection • カーソルベースのページングを書きやすい query BlogTop { blog(...) { entries(first: 10,

    after: ...) { edges { node { ... } } } } } type EntryEdge { cursor: String! node: Entry! }
  15. Relay • https://relay.dev/ • Facebookが開発 • Reactと組み合わせて使えるGraphQLクライアント • GraphQL Server

    Specificationに従ったスキーマを要求する
  16. クライアントの状態を同期してくれる • クライアントキャッシュをidフィールドをもとに同期する • mutationのレスポンスをもとにクライアントの状態を更新する ◦ 既存のデータの更新 ◦ connectionへのデータの追加・削除 •

    データ更新→画面要素の同期 に割く労力を 減らせる mutation PostEntry { postEntry(...) { entry @appendNode(...) { id title content } blog { id entryCount } } }
  17. 再取得・ページングが簡単に書ける • 専用のdirectiveとフックを使うと簡単に書ける • GraphQL Server Specificationの恩恵を受けている const {data, loadNext}

    = usePaginationFragment(...); return ( <> {data.blog.entries.edges.map(...)} <button onClick={() => loadNext(10)}>load</button> </> )
  18. fragmentとコンポーネント分割 • fragmentとコンポーネントの分割単位を強く紐づける • コンポーネントが要求するフィールドを知らなくてよい query GetEntry { entry(id: ...)

    { author { ...EntryFooter_user } } } <EntryFooter user={entry.author} /> fragment EntryFooter_user on User { hatenaId nickname }
  19. Suspenseをフル活用している • 「読み込み中」を表現するためにSuspenseを活用 • useEffectフックで読み込んで……みたいなロジックを書かなくてよい • React 18のトランジションと相性がよい <Suspense fallback={<Loading

    />}> <BlogTop blog={blog} /> </Suspense> const BlogTop = ({blog: _blog}) => { const blog = usePreloadedQuery(..., _blog); return ...; };
  20. 新卒入社した時点では • 初めて触る技術要素が多い • React ◦ ガッツリ触ったことはなかった ◦ 本格的なアプリケーションを作るのは初めて •

    GraphQL ◦ そういえばサマーインターンで触った、ぐらい • Relay ◦ そういうのがあるのか ◦ サマーインターンではApolloを使っていた
  21. 前提知識をつける • 本を読む ◦ 初めてのGraphQL ◦ GraphQLの世界観をなんとなく把握する • Relayのドキュメントを読む ◦

    クライアントライブラリの世界観を把握する • 社内のGraphQL有識者に聞く
  22. 手を動かしてみる • モックAPIサーバーを作る • GraphQLクライアントを書いてみる ◦ クエリを発行する ◦ データを更新する ◦

    テストを書く • ペアプロで練習する ◦ だいたいこんな感じかな、と会話しながら実装する • 雰囲気が分かってきた!!
  23. 機能開発の流れ • デザイン・仕様をもとにページを仮組みする • GraphQLのクエリを実装してもらう • GraphQL APIからデータを取得してページを表示する • 必要に応じてフィードバック・修正する

  24. GraphQL APIを実装してもらって終わり、ではない • 最初から理想のGraphQLスキーマになるとは限らない ◦ ドメイン知識が足りなかった ◦ 仕様を修正した結果、表示したいものが増えた ◦ etc.

    • 実装してもらったGraphQL APIを使って起こったこと・感じたことをフィードバックし て、よりよいGraphQL APIにする
  25. フィードバック • バグ報告 ◦ 値がおかしい ◦ エラーになる ◦ etc. •

    GraphQLスキーマへのフィードバック ◦ フィールドを修正してほしい ◦ スキーマの構造を修正してほしい ◦ etc.
  26. バグ報告 • 手元からフロント開発者用のAPIを叩いて開発 ◦ ログを確認しづらい ◦ 自分で修正できない • 再現するクエリ例と実行結果を共有する ◦

    Chromeの開発者ツールでCopy as cURLすると手軽 • クライアントライブラリの挙動を共有する ◦ Relayは id: ID! フィールドをキーとしてクライアントの状態を正規化する ◦ Relayはmutationの返り値をもとにクライアントの状態を更新する ◦ etc. • スムーズに状況を把握してもらえる
  27. スキーマへのフィードバックの心構え • どういう観点でスキーマへのフィードバックを行っているのかを紹介 • 心構えからスキーマの設計方針まで

  28. ベストプラクティスに寄せていく • 社内や世間の事例を参考にする • GitHub GraphQL API • 本 ◦

    GraphQLスキーマ設計ガイド 第2版 • チュートリアル ◦ ShopifyのGraphQL API設計チュートリアル
  29. 高速なフィードバックループ • 高速にフィードバックループを回したい ◦ 実装してもらったAPIを使ってフィードバック、が早いとよい • スキーマの段階でフィードバックできることがあれば伝える ◦ 懸念を先に伝えることで手戻りを減らす

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

    null)""" author: User }
  31. Nodeインタフェースを実装するかどうか • idがあると便利 ◦ キャッシュの同期 ◦ 直接取得できる • idを持たせられない場合もあるかもしれない ◦

    付随的な情報である ◦ 直接取得するのが難しい • 直接取得したいものは必ずNodeインタフェースを実装してもらう
  32. 配列にするかconnectionにするか • connectionにしておくと便利なことが多い ◦ ページング ◦ mutation発行後のデータ同期 • 用途によっては配列でもよいかもしれない ◦

    データ量がじゅうぶん少ない ◦ GraphQL API経由の追加・更新が発生しない ◦ 付随的な情報である • フロントエンドとしての実装しやすさとのバランス
  33. 合意をスキーマに落とし込む • 自分から見た値が取れると書きやすい場面が多い ◦ 例: リポジトリにスターを付けているか、ブログの記事を編集できるか ◦ フィールドを追加してもらうとよい • author.id

    === viewer.id みたいなロジックを書かない • フロントエンドで値を作り上げようとしない ◦ サーバーサイドとの合意をスキーマに落とし込むべき type Entry { """author.id === viewer.id と等価?""" canEdit: Boolean! }
  34. 語彙や認識を揃える • 議論していて話が噛み合わないと思ったらまず整理する • 仕様書を参照する • GraphQLスキーマとコンポーネントの語彙が揃っていると議論しやすい • なんでも無理に英語にしなくてもよいと思う •

    固有名詞が出てきたときにどうするか ◦ 固有名詞をそのまま使う方向で進めている ◦ 一般的な概念の名前に寄せると、後から renameしなくて便利かも?
  35. 相談も受ける • こちらからフィードバックするだけではない • スキーマやサーバーサイドの実装についての相談を受けることもある • いろいろな観点で考えてから返事する ◦ フロントエンドとしてはどうあるのが望ましいか ◦

    サーバーサイドで実現可能か ◦ 効率が良いか
  36. フィードバックを反映してもらったら • フロントエンド側のGraphQLスキーマを更新する ◦ get-graphql-schema, GraphQL Code Generator • スキーマ更新反映を1つのPRにすると吉

    ◦ スキーマの差分が確認しやすい • 非互換変更に対応する ◦ 本番運用中なら @deprecated directiveを付けてもらうと思う ◦ タスクを切ってTODOコメントを書き残して仮修正する ▪ あとで腰を据えて修正するとごちゃごちゃになりにくい
  37. まとめ • 臆さずに学び続ける姿勢を持つ、インプットを欠かさない • フロントエンドからサーバーサイドに対して働きかける機会を逃さない • 最高のサービスを提供するために、最高のGraphQLスキーマについて考える