はじめに
どうも、はじめまして。
私は株式会社ニジボックスでフロントエンドエンジニアをしているイナバくんです。
前職では、主に WordPress のテーマ作成をしていて、「モダンなグループ開発がしたいっ!」という思いでニジボックスに転職しました。
そんな私のささやかな願いが叶い、とある会社の社内システムの新規開発プロダクトにアサインされました。
そのプロダクトの技術スタックは、いかにもモダン!!という面構えをしていて、私の興奮は最高潮に高まりました。
- フロント: React(Next.js) + TypeScript
- ライブラリ: Storybook, Chakra UI, Jest, MSW などなど
- バックエンド: Go
- API: GraphQL(Apollo)
- インフラ: AWS
そして私は、このプロダクトの肝とも言える「ドキュメントの編集・プレビュー表示」をする巨大な一画面を担当することになりました。
プレビュー画面の仕様のざっくりとした紹介
社内システムなのでモザイクをかけていますが、このようなドキュメントを、画像や表でプレビュー表示する画面となります。(この画面が縦にめっちゃ長い)
この画面のソース上の構成(Fragment Colocation導入前)
GraphQL の schema
実際のコードとは異なる一例になりますが、下記のような schema になっています。
# ドキュメント
type Document {
id: ID
# 情報01
info01: Info01
# 情報02
info02: Info02
# 情報03
info03: Info03
}
# 情報01
type Info01 {
# 色々なデータ
}
# 情報02
type Info02 {
# 色々なデータ
}
# 情報03
type Info03 {
# 色々なデータ
}
実際は大量の schema 定義がズラーーっと並んでいます。
クライアントサイド
ディレクトリ構造 (Fragment Colocation導入前)
/src/
├── /components/
│ └── /Document/
│ ├── Document.tsx // ページコンポーネント
│ ├── /Info01/
│ │ └── Info01.tsx // 子コンポーネント
│ ├── /Info02/
│ │ └── Info02.tsx // 子コンポーネント
│ └── /Info03/
│ └── Info03.tsx // 子コンポーネント
└── /graphql/
└── getDocument.graphql
GraphQL Query (Fragment Colocation導入前)
クライアントサイドから実行する情報取得の query は、/src/graphql/getDocument.graphql
1 ファイルに記述しておりました。
# ドキュメント情報を取得するquery
query getDocument($documentId: ID!) {
document(documentId: $documentId) {
info01 {
# 色々なデータ
}
info02 {
# 色々なデータ
}
info03 {
# 色々なデータ
}
}
}
実際のコードでは、getDocument.graphql
は1000行以上ある巨大queryでした。。。
React コンポーネント (Fragment Colocation導入前)
ディレクトリ構造を見ていただけると分かる通りですが、巨大な画面なので、ページコンポーネント配下に大量の子コンポーネントがネストされています。
実際は子コンポーネントが 30 個ほど並んでいます。。。
ページコンポーネントの中身 (Fragment Colocation導入前)
export function Document() {
// query でデータを取得している
const { data, error } =
useGetDocumentQuery({
variables: {
documentId: documentId,
},
});
return (
<>
<Info01 savedData={data.info01}/> {/* <Info01 />で使うデータのみをpropsで渡しています */}
<Info02 savedData={data.info02}/> {/* <Info02 />で使うデータのみをpropsで渡しています */}
<Info03 savedData={data.info03}/> {/* <Info03 />で使うデータのみをpropsで渡しています */}
</>
);
}
query の実行回数を減らすために、ページコンポーネントの層でqueryを実行して、取得したデータを分割し、子コンポーネントに props リレーで渡していく方針です。
子コンポーネントの中身 (Fragment Colocation導入前)
type Info01Props = GetDocumentQuery["info01"] // ← Code Generator が生成するQueryの型からコンポーネントで使うデータの型までネストして指定する必要がある
export function Info01({savedData}: Info01Props) {
return (
<>
<p>{savedData.title}</p>
<p>{savedData.description01}</p>
<p>{savedData.description02}</p>
</>
);
}
クライアントサイドで使う型について
GraphQL Code Generator を使って、schema, query から TypeScript の型を生成しております。
生成した型を React コンポーネントの props にハメて開発しています。
Fragment Colocation 導入前に抱えていた問題
-
API をデータを表示する子コンポーネントではなく、ページコンポーネントの層で一括で取得しているため、query のコードがとっても長い!!!
-
子コンポーネントの props で使う型(Code Generator が生成した型)が
Document["Info01"]["xxxx"]["xxxx"]
のようにネストされた形式になってしまって可読性が低い!!! -
コード量の多いqueryの中のどのデータが、コンポーネントで使われているのか探すのに時間がかかる!!
Fragment Colocation とは?? (やっと本題)
私は英語が苦手かつ嫌いなのですが、fragment
とは「断片」という意味らしいです。
また、co-location
は、「一緒に置く」という意味らしいです。
GraphQL においては、fragment
は query を分割・共有することができる機能となります。
Fragment Colocation
とは、queryをfragmentでファイル分割し、fragmentで取得するデータを使うReactコンポーネントのディレクトリに配置するというソース管理の考え方になります。
上記の問題1 ~ 3 をまるごと解決するために、このプロダクトでFragment Colocationを導入しました。
※ ちなみに、Fragment Colocation
という考え方は、こちらのQuramyさんの記事を読んで勉強させていただきました。
Fragment Colocation 導入後のソース構成
ディレクトリ構造 (Fragment Colocation導入後)
/src/
└── /components/
└── /Document/
├── Document.tsx // ページコンポーネント
├── getDocument.graphql // queryファイル
├── /Info01/
│ ├── Info01.graphql // Info01 コンポーネントで使うデータを取得するfragmentファイル
│ └── Info01.tsx // 子コンポーネント
├── /Info02/
│ ├── Info02.graphql // Info02 コンポーネントで使うデータを取得するfragmentファイル
│ └── Info02.tsx // 子コンポーネント
└── /Info03/
├── Info03.graphql // Info03 コンポーネントで使うデータを取得するfragmentファイル
└── Info03.tsx // 子コンポーネント
Fragment Colocation導入前は、/graphql/
ディレクトリに膨大なコード量のqueryファイルが1つあったのですが、導入後はコンポーネントで使うデータのquery, fragmentのファイルがコンポーネントディレクトリに一緒に格納されているので、とても分かりやすいです。
(オーバーフェッチ・アンダーフェッチにも気づきやすい!)
次にそれぞれのgraphqlファイルの中身を見ていきましょう。
GraphQL query + fragment (Fragment Colocation導入後)
query getDocument($documentId: ID!) {
document(documentId: $documentId) {
...info01 # fragmentはJSのスプレッド構文みたいにドットを3つ「...」並べて読み込みます
...info02
...info03
}
}
fragment info01 on Document {
info01 {
# 色々なデータ
}
}
fragment info02 on Document {
info02 {
# 色々なデータ
}
}
fragment info03 on Document {
info03 {
# 色々なデータ
}
}
on
の後に分割元になる Type を指定することで、query を fragment で分割することができます。
また、fragment の中に fragment を作ることができます。
fragment ごとにファイルを分割しても、元の query はちゃんと実行されます。
(リクエスト回数は1回のままです! ← 重要)
なので、膨大なコード量の queryをfragmentでファイル分割することができるのです!!!
子コンポーネントの中身 (Fragment Colocation導入後)
type Info01Props = Info01Fragment // ← Fragment Colocation導入前の型にあった["info01"]などのネストが無くなっています!!
export function Info01({savedData}: Info01Props) {
return (
<>
<p>{savedData.title}</p>
<p>{savedData.description01}</p>
<p>{savedData.description02}</p>
</>
);
}
fragment 分割後に Code Generator で型生成すると、fragment の型(サンプルコードだとInfo01Fragment
)ができるので、前述の問題 2 を解決することができます。
コンポーネントのディレクトリ内に、使うデータの fragment ファイルを格納できる上に、コンポーネントでは fragment の型を使うので、分かりやすくなりました。
コンポーネント分割の考え方と、API をページコンポーネントの層で実行する考え方を両立できるのが「Fragment」です!
おまけ
私は今すでにある程度開発してしまった巨大な画面のFragment Colocation対応をしています。
このリファクタリングが地味になかなか大変で、かれこれ1ヶ月ぐらいfragmentばっかり書いています。
(でもソースが少しでも綺麗になると脳汁が出るので好きな作業です。)
もし皆さんがこれから新規プロダクト開発で、この記事のような技術スタック・運用方針でしたら、早めにFragment Colocationを取り入れることをオススメします!