
GraphQLのFragment Colocationを検証しました
はじめに
こんにちは。GLOBIS学び放題 / GLOBIS Unlimitedの受講者体験開発チーム(通称LEDチーム)でエンジニアをしている富山です。
LEDチームでは、2週間単位のスプリントを回しながら、エピック機能の開発と並行して、リファクタタスクにも積極的に取り組んでおり、最近は特にディレクトリ構成やツール周りの設定を見直しています。
上記取り組みの一環で、graphql-codegenのプラグインを使用して、ApolloでもFragment Colocationを導入できるかもしれないと感じたので、検証してみました。
※ 因みにGLOBIS学び放題のフロントエンド構成は、React + TypeScriptでバックエンドとの通信はGraphQLを使用しています。
そもそもFragment Colocationとは
コンポーネント内で必要なデータをコンポーネントと同じ場所で宣言する手法で、これを実現するために、GraphQLのFragmentを利用します。
現在、LEDチームのWeb開発では、PageComponent単位で必要なデータをクエリとして記述する形で運用しています。この場合、親であるPageComponentがそれを構成する子コンポーネントで必要なデータを知っている必要があります。
// コンポーネントがどんなデータを要求するかをページ単位のクエリとして記述
// 新規ページ追加時には、query配下にそのページに紐づくクエリ定義を新規追加する
graphql
├── fragment
├── mutation
└── query
├── accountDetailsPage.graphql
├── accountRegistrationPage.graphql
////////////////////////////////////////////////
src
└── components
└──pages
├── accountDetailsPage
├── accountRegistrationPage
開発が進む中で、子コンポーネントにフィールドを足したりするケースでは、型でその不備を防ぐことができますが、子コンポーネントで不要になったフィールドを削除する場合には、使用しなくなったフィールドが宣言されてる状態、すなわち、本来必要とする以上のデータを取得してしまうケースが発生するリスクが起こりやすくなります。
これを防ぐには、現状、子コンポーネントの要求するデータとクエリを突き合わせて確認する事になるため、保守性もイマイチです。
一方、Fragment Colocationの考え方では、コンポーネントが必要なデータをGraphQLのFragmentで記述し、コンポーネント内で宣言する考え方です。これは、CSS in JSなどの考え方と似ていて、コンポーネントの必要とするデータ項目の情報が外に漏れないようにして、保守性を高める手法となります。
Fragment Colocationの考え方で組み立てていくと、トップのコンポーネント(= PageComponent)は、子コンポーネントで宣言されたFragmentを取りまとめて、単一クエリを投げるという形を取ります。こうするとPageComponentは各コンポーネントで宣言されたフラグメントだけ知っていれば良いため、上記のようなデータとクエリを突き合わせて確認ということも無くなってきます。
Apollo ClientでFragment Colocationを試してみる
上記を踏まえて、gql-tag-operations-preset を導入して、Fragment Colocationでの開発を試してみたいと思います。
以下ドキュメントにインストール方法や使用方法の記載がありますので詳細は省きますが、Fragment Colocationがどういうものかというイメージを掴んでもらえたらと思います。
まず、子コンポーネントが必要とするデータをFragmentとして定義し、graphql-codegenで、型を生成します。
import React from 'react'
import { FragmentType, useFragment } from 'gql/fragment-masking'
import { gql } from 'gql/gql'
const CourseCardFragment = gql(/* GraphQL */ `
fragment courseCardItem on Course {
id
name
slug
videoDuration
photo {
thumb {
url
}
}
}
`)
type Props = {
course: FragmentType<typeof CourseCardFragment>
}
export const SampleCourseCard: React.VFC<Props> = props => {
const courseCardItem = useFragment(CourseCardFragment, props.course)
return (
<Link href={courseCardItem.slug}>
<img src={courseCardItem.photo?.thumb?.url} />
<div>{courseCardItem.name}</div>
<div>{courseCardItem.videoDuration}</div>
</Link>
)
}
次にPageComponentでPageクエリを作成します。
PageComponentでは、子コンポーネントで定義したFragmentを展開します。 後は、PageComponentで取得したデータを流し込んでいくだけです。
こうしておくと、子コンポーネントで必要な値に増減があった際に、Fragmentの修正だけで済むので、保守性も上がります。
const SamplePageQuery = gql(/* GraphQL */ `
query SamplePage {
seriesItems {
nodes {
id
title
courses(first: 10) {
nodes {
id
...courseCardItem
}
}
}
}
}
`)
export const SamplePage: React.VFC = () => {
const { data } = useQuery(SamplePageQuery)
const nodes = data?.seriesItems?.nodes ?? []
return (
<>
{nodes.filter(isNotNullable).map(seriesItem => (
<div key={seriesItem.id}>
<h2> {seriesItem.title}</h2>
{seriesItem.courses.nodes &&
seriesItem.courses.nodes
.filter(isNotNullable)
.map(course => (
<SampleCourseCard key={course.id} course={{ ...course }} />
))}
</div>
))}
</>
)
}
gql-tag-operations-preset にはFragmentMaskingという設定があり、有効化しておくと、各コンポーネントはフラグメントによって記述されたデータ依存関係のみアクセスすることができます。(= 子コンポーネントで定義されたFragmentのデータに親コンポーネントはアクセス出来ないようにできます)
上記のコードの場合、SamplePageは、 seriesItem.nodes.id 、seriesItem.node.title、courses.nodes.id にしかアクセスできないように制約をかけることができていることが確認できます。

最後に
導入した際の課題としては、graphql-codegenではFragmentやQueryなどの命名はコードベース全体でユニークである必要があり、Fragmentの名前がバッティングする可能性が高いです。導入時にFragmentの命名に関しては、事前に認識を合わせておいた方が、良さそうです。
以下、Relayのドキュメントの引用になりますが、<module_name>_<property_name> という規則を踏襲すれば、対象のFragmentが、「どのファイルで使われる、何なのか」を表現できて良いと感じました。
Fragment names need to be globally unique. In order to easily achieve this, we name fragments using the following convention based on the module name followed by an identifier: <module_name>_<property_name>. This makes it easy to identify which fragments are defined in which modules and avoids name collisions when multiple fragments are defined in the same module.
GLOBIS学び放題 / GLOBIS Unlimitedのコードベースも小さくはないので、実際に導入するかはまだまだ検証の余地ありですが、Fragment Colocationを導入することで、ページとコンポーネントの依存関係を減らし、クエリを過不足なく必要十分に保つことが出来そうに感じました。
グロービスでは一緒に働く仲間を募集しています!
LEDのフロントエンド開発においては、ADR(Architecture Decision Records)を導入しています。1スプリントの中で、全体設計や実装方針についてチームで議論する時間が設けられており、その中で、リファクタタスクが起票され、本実装と並行して、改善にも取り組んでいける仕組みがあります。
フロントエンド開発だけで見ても、まだまだやるべき事もやりたい事も沢山あります。GLOBIS学び放題 / GLOBIS Unlimitedでは、チームで議論しながら、モノづくりをしていきたい仲間を募集しています。少しでも興味を持った方は採用ページをご覧になって頂ければと思います。