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

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

utagawa kiki
January 27, 2022
8.9k

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

utagawa kiki

January 27, 2022
Tweet

Transcript

  1. 今回の共同開発案件の特徴 • 開発領域が明確に分かれている ◦ はてな: デザイン・フロントエンド ◦ パートナー: サーバーサイド・インフラ •

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

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

    owner { hatenaId } entries { title content } } } { "data": { "blog": { "owner": { "hatenaId": "utgwkk" }, "entries": [ { "title": "日記", "content": "こんにちは" } ] } } }
  5. 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" } } } }
  6. interface • 特定のフィールドを持つ型であることを示せる interface User { hatenaId: String! } type

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

    categoryStr: String! @deprecated(reason: "categoriesフィールドを使ってください ") categories: [Category!]! }
  8. Nodeインタフェース、nodeクエリ • Nodeインタフェース ◦ id: ID! フィールドで一意に定まる • node クエリ

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

    after: ...) { edges { node { ... } } } } } type EntryEdge { cursor: String! node: Entry! }
  11. 再取得・ページングが簡単に書ける • 専用のdirectiveとフックを使うと簡単に書ける • GraphQL Server Specificationの恩恵を受けている const {data, loadNext}

    = usePaginationFragment(...); return ( <> {data.blog.entries.edges.map(...)} <button onClick={() => loadNext(10)}>load</button> </> )
  12. 新卒入社した時点では • 初めて触る技術要素が多い • React ◦ ガッツリ触ったことはなかった ◦ 本格的なアプリケーションを作るのは初めて •

    GraphQL ◦ そういえばサマーインターンで触った、ぐらい • Relay ◦ そういうのがあるのか ◦ サマーインターンではApolloを使っていた
  13. 手を動かしてみる • モックAPIサーバーを作る • GraphQLクライアントを書いてみる ◦ クエリを発行する ◦ データを更新する ◦

    テストを書く • ペアプロで練習する ◦ だいたいこんな感じかな、と会話しながら実装する • 雰囲気が分かってきた!!
  14. フィードバック • バグ報告 ◦ 値がおかしい ◦ エラーになる ◦ etc. •

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

    Chromeの開発者ツールでCopy as cURLすると手軽 • クライアントライブラリの挙動を共有する ◦ Relayは id: ID! フィールドをキーとしてクライアントの状態を正規化する ◦ Relayはmutationの返り値をもとにクライアントの状態を更新する ◦ etc. • スムーズに状況を把握してもらえる
  16. ベストプラクティスに寄せていく • 社内や世間の事例を参考にする • GitHub GraphQL API • 本 ◦

    GraphQLスキーマ設計ガイド 第2版 • チュートリアル ◦ ShopifyのGraphQL API設計チュートリアル
  17. Nodeインタフェースを実装するかどうか • idがあると便利 ◦ キャッシュの同期 ◦ 直接取得できる • idを持たせられない場合もあるかもしれない ◦

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

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

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

    固有名詞が出てきたときにどうするか ◦ 固有名詞をそのまま使う方向で進めている ◦ 一般的な概念の名前に寄せると、後から renameしなくて便利かも?
  21. フィードバックを反映してもらったら • フロントエンド側のGraphQLスキーマを更新する ◦ get-graphql-schema, GraphQL Code Generator • スキーマ更新反映を1つのPRにすると吉

    ◦ スキーマの差分が確認しやすい • 非互換変更に対応する ◦ 本番運用中なら @deprecated directiveを付けてもらうと思う ◦ タスクを切ってTODOコメントを書き残して仮修正する ▪ あとで腰を据えて修正するとごちゃごちゃになりにくい