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

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

utagawa kiki
January 27, 2022
8.7k

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

utagawa kiki

January 27, 2022
Tweet

Transcript

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  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!
    }

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  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"
    }
    }
    }
    }

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  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!
    }

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  19. Suspenseをフル活用している
    ● 「読み込み中」を表現するためにSuspenseを活用
    ● useEffectフックで読み込んで……みたいなロジックを書かなくてよい
    ● React 18のトランジションと相性がよい
    }>


    const BlogTop = ({blog: _blog}) => {
    const blog = usePreloadedQuery(..., _blog);
    return ...;
    };

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide