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
8.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
君たちはどうコードをレビューする (される) か / 大吉祥寺.pm
utgwkk
21
12k
Dive into gomock / Go Conference 2024
utgwkk
14
5k
Goでリフレクションする、その前に / Kansai.go #1
utgwkk
5
1.7k
Go製Webアプリケーションのエラーとの向き合い方大全、あるいはやっぱりスタックトレース欲しいやん / Kyoto.go #50
utgwkk
7
3k
ありがとう、create-react-app
utgwkk
4
10k
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.3k
なんでもPull Requestにする / Kichijoji.pm 31
utgwkk
3
6.2k
Featured
See All Featured
How STYLIGHT went responsive
nonsquared
95
5.2k
Six Lessons from altMBA
skipperchong
27
3.5k
Dealing with People You Can't Stand - Big Design 2015
cassininazir
364
24k
Imperfection Machines: The Place of Print at Facebook
scottboms
265
13k
Done Done
chrislema
181
16k
YesSQL, Process and Tooling at Scale
rocio
169
14k
[RailsConf 2023 Opening Keynote] The Magic of Rails
eileencodes
28
9.1k
Building Your Own Lightsaber
phodgson
103
6.1k
A designer walks into a library…
pauljervisheath
204
24k
4 Signs Your Business is Dying
shpigford
180
21k
Learning to Love Humans: Emotional Interface Design
aarron
273
40k
Raft: Consensus for Rubyists
vanstee
136
6.6k
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スキーマについて考える