Upgrade to Pro
— share decks privately, control downloads, hide ads and more …
Speaker Deck
Features
Speaker Deck
PRO
Sign in
Sign up for free
Search
Search
GraphQLを使った共同開発の心構え 〜 フロントエンドの視点から / Hatena Eng...
Search
utagawa kiki
January 27, 2022
0
9k
GraphQLを使った共同開発の心構え 〜 フロントエンドの視点から / Hatena Engineer Seminar #18
https://hatena.connpass.com/event/235821/
utagawa kiki
January 27, 2022
Tweet
Share
More Decks by utagawa kiki
See All by utagawa kiki
ゆるやかにgolangci-lintのルールを強くする / Kyoto.go #56
utgwkk
2
420
君たちはどうコードをレビューする (される) か / 大吉祥寺.pm
utgwkk
21
13k
Dive into gomock / Go Conference 2024
utgwkk
14
5.4k
Goでリフレクションする、その前に / Kansai.go #1
utgwkk
5
2k
Go製Webアプリケーションのエラーとの向き合い方大全、あるいはやっぱりスタックトレース欲しいやん / Kyoto.go #50
utgwkk
7
3.4k
ありがとう、create-react-app
utgwkk
4
11k
mockgenによるモック生成を高速化するツール bulkmockgenのご紹介 / Kyoto.go #43
utgwkk
2
2.2k
SPAでもデータをURLでシェアしたい / Kyoto.js 19
utgwkk
2
1.8k
prototype大全 / YAPC::Kyoto 2023
utgwkk
1
4.4k
Featured
See All Featured
Adopting Sorbet at Scale
ufuk
73
9.1k
Navigating Team Friction
lara
183
15k
Bootstrapping a Software Product
garrettdimon
PRO
305
110k
Evolution of real-time – Irina Nazarova, EuRuKo, 2024
irinanazarova
6
450
YesSQL, Process and Tooling at Scale
rocio
169
14k
The Art of Delivering Value - GDevCon NA Keynote
reverentgeek
8
1.2k
Fashionably flexible responsive web design (full day workshop)
malarkey
405
66k
The Cult of Friendly URLs
andyhume
78
6.1k
Scaling GitHub
holman
459
140k
Statistics for Hackers
jakevdp
796
220k
Building Your Own Lightsaber
phodgson
103
6.1k
How to train your dragon (web standard)
notwaldorf
88
5.7k
Transcript
GraphQLを使った共同開発の心構え 〜 フロントエンドの視点から Hatena Engineer Seminar #18 id:utgwkk
自己紹介 • id:utgwkk (うたがわきき) • Webアプリケーションエンジニア 新規の共同開発案件に携わる • はてなサマーインターン2019出身 •
はてなブログMediaチーム アルバイトエンジニア (2019/9 - 2021/3)
今回の共同開発案件の特徴 • 開発領域が明確に分かれている ◦ はてな: デザイン・フロントエンド ◦ パートナー: サーバーサイド・インフラ •
APIのインタフェースにGraphQLを利用 ◦ 裏側に複数のマイクロサービスがある • フロントエンドをSPA (Single Page Application) として実装 ◦ TypeScript ◦ React ◦ Relay (GraphQLクライアント) • 手元からフロント開発者用のAPIを叩いて開発
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! }
GraphQLにおけるデータ型 • 組み込み型 ◦ Int, String, Boolean, … ◦ ID
• non-nullableな型 T! ◦ T はnullableであることに注意 • 配列型 [T] ◦ nullableとの組み合わせに注意 ◦ [T!]! として使うことが多い
query (データを取得する) • クライアントから取得したいフィールドを自由に指定できる query GetBlog { blog(id: 1) {
owner { hatenaId } entries { title content } } } { "data": { "blog": { "owner": { "hatenaId": "utgwkk" }, "entries": [ { "title": "日記", "content": "こんにちは" } ] } } }
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" } } } }
fragment • 特定の型のフィールドの集合に名前を付けられる fragment AuthorInfo on User { hatenaId nickname
} query GetEntry { entry(id: 100) { author { ...AuthorInfo } } }
interface • 特定のフィールドを持つ型であることを示せる interface User { hatenaId: String! } type
Visitor implements User { hatenaId: String! } type Author implements User { hatenaId: String! entries: [Entry!]! }
directive • フィールドなどに付ける指示子 • 例: @deprecated (廃止予定のフィールドに付ける) type Entry {
categoryStr: String! @deprecated(reason: "categoriesフィールドを使ってください ") categories: [Category!]! }
GraphQL Server Specification • https://relay.dev/docs/guides/graphql-server-specification/ • GraphQL自体の仕様とは別の、追加の仕様 • Nodeインタフェース、nodeクエリ •
connection
Nodeインタフェース、nodeクエリ • Nodeインタフェース ◦ id: ID! フィールドで一意に定まる • node クエリ
◦ idをもとに直接取得できる interface Node { id: ID! } type Query { node($id: ID!): Node } type Blog implements Node { id: ID! }
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! }
connection • カーソルベースのページングを書きやすい query BlogTop { blog(...) { entries(first: 10,
after: ...) { edges { node { ... } } } } } type EntryEdge { cursor: String! node: Entry! }
Relay • https://relay.dev/ • Facebookが開発 • Reactと組み合わせて使えるGraphQLクライアント • GraphQL Server
Specificationに従ったスキーマを要求する
クライアントの状態を同期してくれる • クライアントキャッシュをidフィールドをもとに同期する • mutationのレスポンスをもとにクライアントの状態を更新する ◦ 既存のデータの更新 ◦ connectionへのデータの追加・削除 •
データ更新→画面要素の同期 に割く労力を 減らせる mutation PostEntry { postEntry(...) { entry @appendNode(...) { id title content } blog { id entryCount } } }
再取得・ページングが簡単に書ける • 専用のdirectiveとフックを使うと簡単に書ける • GraphQL Server Specificationの恩恵を受けている const {data, loadNext}
= usePaginationFragment(...); return ( <> {data.blog.entries.edges.map(...)} <button onClick={() => loadNext(10)}>load</button> </> )
fragmentとコンポーネント分割 • fragmentとコンポーネントの分割単位を強く紐づける • コンポーネントが要求するフィールドを知らなくてよい query GetEntry { entry(id: ...)
{ author { ...EntryFooter_user } } } <EntryFooter user={entry.author} /> fragment EntryFooter_user on User { hatenaId nickname }
Suspenseをフル活用している • 「読み込み中」を表現するためにSuspenseを活用 • useEffectフックで読み込んで……みたいなロジックを書かなくてよい • React 18のトランジションと相性がよい <Suspense fallback={<Loading
/>}> <BlogTop blog={blog} /> </Suspense> const BlogTop = ({blog: _blog}) => { const blog = usePreloadedQuery(..., _blog); return ...; };
新卒入社した時点では • 初めて触る技術要素が多い • React ◦ ガッツリ触ったことはなかった ◦ 本格的なアプリケーションを作るのは初めて •
GraphQL ◦ そういえばサマーインターンで触った、ぐらい • Relay ◦ そういうのがあるのか ◦ サマーインターンではApolloを使っていた
前提知識をつける • 本を読む ◦ 初めてのGraphQL ◦ GraphQLの世界観をなんとなく把握する • Relayのドキュメントを読む ◦
クライアントライブラリの世界観を把握する • 社内のGraphQL有識者に聞く
手を動かしてみる • モックAPIサーバーを作る • GraphQLクライアントを書いてみる ◦ クエリを発行する ◦ データを更新する ◦
テストを書く • ペアプロで練習する ◦ だいたいこんな感じかな、と会話しながら実装する • 雰囲気が分かってきた!!
機能開発の流れ • デザイン・仕様をもとにページを仮組みする • GraphQLのクエリを実装してもらう • GraphQL APIからデータを取得してページを表示する • 必要に応じてフィードバック・修正する
GraphQL APIを実装してもらって終わり、ではない • 最初から理想のGraphQLスキーマになるとは限らない ◦ ドメイン知識が足りなかった ◦ 仕様を修正した結果、表示したいものが増えた ◦ etc.
• 実装してもらったGraphQL APIを使って起こったこと・感じたことをフィードバックし て、よりよいGraphQL APIにする
フィードバック • バグ報告 ◦ 値がおかしい ◦ エラーになる ◦ etc. •
GraphQLスキーマへのフィードバック ◦ フィールドを修正してほしい ◦ スキーマの構造を修正してほしい ◦ etc.
バグ報告 • 手元からフロント開発者用のAPIを叩いて開発 ◦ ログを確認しづらい ◦ 自分で修正できない • 再現するクエリ例と実行結果を共有する ◦
Chromeの開発者ツールでCopy as cURLすると手軽 • クライアントライブラリの挙動を共有する ◦ Relayは id: ID! フィールドをキーとしてクライアントの状態を正規化する ◦ Relayはmutationの返り値をもとにクライアントの状態を更新する ◦ etc. • スムーズに状況を把握してもらえる
スキーマへのフィードバックの心構え • どういう観点でスキーマへのフィードバックを行っているのかを紹介 • 心構えからスキーマの設計方針まで
ベストプラクティスに寄せていく • 社内や世間の事例を参考にする • GitHub GraphQL API • 本 ◦
GraphQLスキーマ設計ガイド 第2版 • チュートリアル ◦ ShopifyのGraphQL API設計チュートリアル
高速なフィードバックループ • 高速にフィードバックループを回したい ◦ 実装してもらったAPIを使ってフィードバック、が早いとよい • スキーマの段階でフィードバックできることがあれば伝える ◦ 懸念を先に伝えることで手戻りを減らす
nullableにするかどうか • 基本的にはnon-nullだと分岐が減らせてありがたい • nullになる場合があるならドキュメントに書いてもらう type Entry { """記事を投稿したユーザー (ユーザーが退会済なら
null)""" author: User }
Nodeインタフェースを実装するかどうか • idがあると便利 ◦ キャッシュの同期 ◦ 直接取得できる • idを持たせられない場合もあるかもしれない ◦
付随的な情報である ◦ 直接取得するのが難しい • 直接取得したいものは必ずNodeインタフェースを実装してもらう
配列にするかconnectionにするか • connectionにしておくと便利なことが多い ◦ ページング ◦ mutation発行後のデータ同期 • 用途によっては配列でもよいかもしれない ◦
データ量がじゅうぶん少ない ◦ GraphQL API経由の追加・更新が発生しない ◦ 付随的な情報である • フロントエンドとしての実装しやすさとのバランス
合意をスキーマに落とし込む • 自分から見た値が取れると書きやすい場面が多い ◦ 例: リポジトリにスターを付けているか、ブログの記事を編集できるか ◦ フィールドを追加してもらうとよい • author.id
=== viewer.id みたいなロジックを書かない • フロントエンドで値を作り上げようとしない ◦ サーバーサイドとの合意をスキーマに落とし込むべき type Entry { """author.id === viewer.id と等価?""" canEdit: Boolean! }
語彙や認識を揃える • 議論していて話が噛み合わないと思ったらまず整理する • 仕様書を参照する • GraphQLスキーマとコンポーネントの語彙が揃っていると議論しやすい • なんでも無理に英語にしなくてもよいと思う •
固有名詞が出てきたときにどうするか ◦ 固有名詞をそのまま使う方向で進めている ◦ 一般的な概念の名前に寄せると、後から renameしなくて便利かも?
相談も受ける • こちらからフィードバックするだけではない • スキーマやサーバーサイドの実装についての相談を受けることもある • いろいろな観点で考えてから返事する ◦ フロントエンドとしてはどうあるのが望ましいか ◦
サーバーサイドで実現可能か ◦ 効率が良いか
フィードバックを反映してもらったら • フロントエンド側のGraphQLスキーマを更新する ◦ get-graphql-schema, GraphQL Code Generator • スキーマ更新反映を1つのPRにすると吉
◦ スキーマの差分が確認しやすい • 非互換変更に対応する ◦ 本番運用中なら @deprecated directiveを付けてもらうと思う ◦ タスクを切ってTODOコメントを書き残して仮修正する ▪ あとで腰を据えて修正するとごちゃごちゃになりにくい
まとめ • 臆さずに学び続ける姿勢を持つ、インプットを欠かさない • フロントエンドからサーバーサイドに対して働きかける機会を逃さない • 最高のサービスを提供するために、最高のGraphQLスキーマについて考える