株式会社メドレーDeveloper Portal

2020-11-06

GraphQL, TypeScript, React を用いて型安全に社内システムをリニューアルした話

こんにちは。メドレーのエンジニアの山田です。現在、医療介護求人サイト「ジョブメドレー」のチームで開発を担当しています。

今回、ジョブメドレーの社内スタッフが利用する社内システムをリニューアルした事例をご紹介します。

リニューアル対象はバックエンド領域も含まれますが、本記事ではフロントエンドの話を中心にご紹介します。

また、弊社デザイナー酒井が以前投稿した デザイナーがデザインツールを使わずに、React を使ってデザインした話 も関連しているので、よろしければあわせてご覧ください。

リニューアルの背景

社内システムでは、求人サイト「ジョブメドレー」を利用する求職者に関する情報や求職者の応募状況を管理しています。

前回のリニューアルから時間が経ち、複雑性が高くなってきました。その複雑性に比例して、新機能の追加や改修するためのコストも高くなっていました。

そこで上記の課題を解決するため、状態管理がしやすく、テストコードも書きやすい、メンテナブルなアーキテクチャにすべくリニューアルを実施することにしました。

検証期間も経て、今回のリニューアルにあわせて新規に作成する API は、GraphQL によって実装することを決めました。

型システムを持つため画面に必要なデータを柔軟に過不足なく取得できる、手動でドキュメントに落とし込まなくてもスキーマが定義されていれば API の仕様を簡単に把握できる、等がメリットとして感じられました。

特に、GraphQL が持つ型システムが、TypeScript、Apollo、GraphQL Code Generator のライブラリを組み合わせることで、API に渡すパラメータや、レスポンスにも型が適用され、GraphQL スキーマの変更にクライアントの実装が比較的容易に追従できることが、大きなポイントでした。

フロントエンドの技術的なリニューアル内容

今回は特に、リニューアルに用いられたフレームワークやライブラリ、Apollo Client を用いた状態管理、テストコード実装における Tips 等をそれぞれ部分的にご紹介します。

採用したフレームワークと主要ライブラリ

採用ライブラリ説明
Next.jsReact 用のフレームワーク(ボイラープレート)
TypeScriptJavaScript のスーパーセットで、静的型付け言語
ReactUI を構築するためのライブラリ(バージョン 16.8.0 でリリースされた hooks を全面的に使用)
Apollo ClientGraphQL API のクライアントで、アプリケーション全体の状態管理を実施
GraphQL Code GeneratorGraphQL スキーマから定義ファイル(型、カスタム hooks 等)を生成
emotion + Styled SystemCSS in JS として利用
formik + yupフォームのビルダー + バリデーター
Jest + React Testing Libraryテストコード実装用のツール群
ESLint + Prettierルールに基づいたコードの静的解析 + スタイリング

TypeScript

今回のリニューアルで求められたことの一つとして、さらなる改善・新規機能追加などをしていく上で、ソフトウェア品質を担保するための、アプリケーションの堅牢さがありました。

そこで、フロントエンド側の開発言語としては、プログラムコード内で宣言された型によって、エラーを未然に防ぎつつ、VSCode をはじめとするエディタのコード補完の恩恵を受けられるメリット等を考慮して TypeScript の採用を決めました。また、他のプロジェクトでも既に TypeScript は部分的に利用し始めていた事情もあり、逆に TypeScript を採用しない、という選択肢はあまり考えられませんでした。

React

UI を構築するためのライブラリ/フレームワークは React を採用しました。こちらも、弊社では別プロジェクトで React を既に利用し始めていたこともあり、学習コストの観点から、新たに他のフレームワークを選択するメリットはほぼ無かったためです。しかし、その事を差し引いたとしても TypeScript と GraphQL との相性の良さで、React が優勢でした。

特に、React の場合は、GraphQL スキーマをベースに、GraphQL Code Generator によって型定義ファイルだけではなく、GraphQL API とのやり取りに使えるカスタム hooks も生成して利用できるという点が、大きな利点として考えられました。

Next.js

フロントエンド開発環境を素早く構築するため、ボイラープレートとして Next.js を採用しました。

Next.js の具体的な採用ポイントとしては、主に次の3点です。

  1. webpack における、バンドルやコンパイル、ホットリロード等の設定に時間を費やすことなく、ビジネスロジックの実装に集中できる
    • 必要があれば、next.config.js で設定を拡張できる
    • CRA(Create React App)とは異なり、拡張性に優れている
  2. pages 配下に置く React Component のディレクトリ構成が、自動的にルーティングとして定義される
    • ルーティングに関する設計作業が不要になる
  3. 自動コード分割等によるパフォーマンス最適化をよしなに行ってくれる

React Component の分類

component は大きく2つに分類し、src/components/app/src/components/ui/ それぞれのディレクトリに component を置いています。分類は以下の基準で行ないました。

  • app: 本アプリケーション固有で使用される想定のもので、再利用性が低く、具体的な component

  • ui: 本アプリケーション外でも使用可能な、再利用性が高く、抽象的な component

社内向けシステムではあるものの、Material-UI や Ant Design 等をはじめとする、外部の UI ライブラリは使用せず、カスタマイズがしやすいように、全て自前で作成しました。

app配下とui配下、どちらの component も基本的には コロケーション の考え方でファイルを構成しています。

一般的には、よく一緒に変更するファイルを近くに置いておくのは良いアイディアです。 この原則は、「コロケーション」と呼ばれます。

この考え方でファイルを構成することで、関連するファイルがまとまっていて、作業がしやすくなります。


src/
  components/
    app/
      partials/
        ${component 名}/
          apollo.cache.ts
          apollo.query.graphql
          index.tsx
          index.test.tsx
        ...

      screens/
        ${component 名}/
          apollo.cache.ts
          apollo.query.graphql
          index.tsx
          index.test.tsx

          ${子 component 名}/
            apollo.cache.ts
            apollo.query.graphql
            index.tsx
            index.test.tsx
            validation.ts

src/components/app ディレクトリ配下でさらに、partialsscreensのディレクトリで component を分けています。

screensには、Next.js で route として扱われる src/pages 配下の component から import される component が配置されています。

画面のバリエーションが増える度に、このscreensにファイルが追加されていきます。

partialsには、app 配下で複数の component から利用される component(画面をまたいで共有されるもの等)を配置しています。

screenspartialsそれぞれ直下の component で、必要であれば適宜、component を分割して子 component を持つ構成にしています。

apollo.cache.tsapollo.query.graphqlについては後述の状態管理の話でご紹介します。

状態管理

アプリケーションの状態管理については、グローバルにアクセスできる状態の管理には Apollo Client の InMemoryCache による cache 機構で行い、特定の component 内に閉じている局所的な状態の管理にはuseState等の React Hooks を使って行っています。

状態管理の必要性が生じた際、アプリケーションの複雑性を上げないように、なるべく useState 等の hooks を用いた local state だけで済ませられないかどうかを検討します。

例えば、クリックするとドロップダウンリストが表示されるセレクトボックスの component で、ドロップダウンリストの表示状態をその component 内だけで扱いたいのであれば useState を用いた local state で十分であると考えられます。

親子関係ではない component 同士でのやりとりが必要になった時や、サーバのデータと関連する場合等で、ローカルのデータを一元管理しておいた方が良さそうなケースでは、Apollo Client の cache を利用します。

Apollo Client

Apollo に関連するファイルの構成については以下の通りです。

src/
  apollo/
    cache.ts
    client.ts
    types.ts
    withApollo.ts
  • cache.ts: Apollo における local state のinitialStateresolverを全画面分このファイルでまとめて、最終的に Next.js のsrc/pages/_app.tsxに渡るようにする

    • component 固有の local state に関するinitialStateおよび state の updater となるresolverは component 毎のapollo.cache.tsにて、別途定義
  • client.ts: Apollo Client のインスタンスを生成するファイル

  • types.ts: Apollo 関連の型定義ファイル

  • withApollo.ts: Apllo Client の <ApolloProvider /> でラップして返す Higher-Order Compoents(HOC)

実装については割愛しますが、client.ts と withApollo.ts に関しては、Next.js の example(with-apollo)等を参考にしました。

画面固有の Apollo の状態管理に関わるファイルはsrc/components/**/${component 名}/配下に置いています。

こちらもコロケーションの考え方で、component に関わる状態管理は該当の component と同じ場所に置くことを意識しています。

src/
  components/
    app/
      ${component 名}/
        apollo.cache.ts
        apollo.query.graphql
        apollo.schema.graphql
  • apollo.cache.ts: component 固有の Apollo における local state のinitialStateおよびresolverを定義するファイル

  • apollo.query.graphql: クエリを定義するファイル

  • apollo.schema.graphql: local state の GraphQL スキーマを定義ファイル

ファイルの命名について、ディレクトリ階層をできるだけ深くしたくない ので、apollo等によるディレクトリは設けていませんが、Apollo 関連のファイル群として認識できるよう、ファイル名にapollo.のプレフィックスをつけて命名しています。

Query と Mutation の実行について

GraphQL Code Generator のプラグイン TypeScript React Apollo をインストールして、hooks を生成する設定にした上で、component 毎にそれぞれ GraphQL のスキーマとクエリが記述された.graphqlファイルをもとに、GraphQL Code Generator が生成するカスタム hooks を利用します。

こちらのカスタム hooks を React Component で利用することで、Apollo Client 経由で GraphQL API とローカルの Apollo cache に接続して、データのやり取りを行うことができます。

Query

Query の hooks は2種類あり、実行するタイミングによっていずれか適切な方を選んで実行しています。

API実行タイミング
useQueryComponent が render されたらクエリ実行
useLazyQuery任意のイベントをトリガーにしてクエリ実行

use***Query

通常であればuseQueryでクエリの結果を render しますが、GraphQL Code Generator を利用する場合は、それぞれのクエリをラップしたカスタム hooks が生成されるので、useQuery,useLazyQueryをそのまま使うことはありません。

query AllPosts {
  allPosts {
    id
    title
    rating
  }
}

↑ のようなクエリを用意するとsrc/__generated__/graphql.tsxに対して、次のようなカスタム hooks が型と一緒に生成される設定にしています。

// Apollo Client: 2.6.9、GraphQL Code Generator: 1.15.0 の場合の例
export function useAllPostsQuery(baseOptions?: ApolloReactHooks.QueryHookOptions<AllPostsQuery, AllPostsQueryVariables>) {
  return ApolloReactHooks.useQuery<AllPostsQuery, AllPostsQueryVariables>(AllPostsDocument, baseOptions);
}

export function useAllPostsLazyQuery(baseOptions?: ApolloReactHooks.LazyQueryHookOptions<AllPostsQuery, AllPostsQueryVariables>) {
  return ApolloReactHooks.useLazyQuery<AllPostsQuery, AllPostsQueryVariables>(AllPostsDocument, baseOptions);
}

React Component では生成されたカスタム hooks を次のように呼び出してサーバーから返ってくる結果を受け取って、データ出力、ローディング状態のチェック、エラーハンドリング等を行います。

const { data, loading, error } = useAllPostsQuery();

Mutation

データの書き込みはuseMutationで行います。

Query 同様、GraphQL Code Generatorによって生成されたカスタム hooks use***Mutation を使っています。

cache の更新

Mutation が複数エンティティの更新、エンティティの新規作成または削除の場合、Apollo Client の cache は自動更新されず、Mutation の結果が自動的に render されません。

このような場合でも、useMutationupdate option を使えば、cacheオブジェクトを引数に取れる関数を設定できるので、この関数内で直接 cache を更新できます。

また、updateの代わりに refetchQueries の option を使って、任意の Query を実行して、シンプルに cache を更新することもできます。

但し、この方法だと Network 通信によるオーバーヘッドが発生します。

このオーバーヘッドを犠牲にしてでも、サーバーからデータ取得したい Query があるような場合には、このrefetchQueriesが有効です。

local state の管理

ここからは特定の component の状態管理を local state を使ってどのように管理しているかを、ご説明していきます。

@client を使った Query

Next.js のプロジェクトで、local state の管理を Apollo Client で行う場合の例としては、次の通りです。

スキーマ:

# src/components/app/Home/apollo.schema.graphql

type Home {
  currentPostId: Int!
}

extend type Query {
  home: Home
}

クエリ:

# src/components/app/Home/apollo.query.graphql

query HomeCurrentPostId {
  home @client {
    currentPostId
  }
}

キャッシュの初期値:


// src/components/app/Home/apollo.cache.ts

export const cache = {
  __typename: 'Home',
  currentPostId: 0,
  ...,
};

// src/apollo/cache.ts

const caches = {
  ...,
  home: home.cache,
};

export {
  ...,
  caches,
};

// src/pages/_app.tsx

export const cache = new InMemoryCache();

  ...

const client = new ApolloClient({
  link,
  cache: cache.restore(initialState || {}),
  resolvers,
  connectToDevTools: true,
});

cache.writeData({ data: caches });

GraphQL クエリとスキーマが定義されていれば GraphQL Code Generator がuse***Queryのコードを生成する設定にしています。

ローカルデータの場合、クエリで@clientディレクティブをつけてローカルデータであることを明示します。

@client を使った Mutation

local state の更新を GraphQL の Mutation として行う場合の例としては、次の通りです。

スキーマ:


# src/components/app/Home/apollo.schema.graphql

type UpdateCurrentPostId {
  currentPostId: Int!
}

extend type Mutation {
  updateCurrentPostId(id: Int!): UpdateCurrentPostId
}

クエリ:

# src/components/app/Home/apollo.query.graphql

mutation UpdateCurrentPostId($id: Int!) {
  updateCurrentPostId(id: $id) @client {
    currentPostId @client
  }
}

resolver:

// src/components/app/Home/apollo.cache.ts

const updateCurrentPostId: MutationResolvers["updateCurrentPostId"] = (
  _,
  args,
  { cache }
) => {
  cache.writeData({
    data: {
      home: {
        __typename: "Home",
        currentPostId: args.id,
      },
    },
  });

  return null;
};

export const Mutation = {
  updateCurrentPostId,
};

Query 同様に@clientディレクティブをつけてローカルデータであることを明示します。

実際の Mutation の処理自体はresolverの中にcache.writeData()を使って記述します。

Mutation の命名は、動詞+名詞の形式で可能な限り意味のある具体的な名前をつけることを意識しています。

Apollo を使った開発を便利にしてくれるツール

Apollo Client を使って開発する際は、ローカルの Apollo cache の状態や、クエリを試しに実行するためのツールとして、Google Chrome の拡張機能 Apollo Client Developer Tools が非常に便利です。

こちらの拡張機能を Chrome にインストールすると、Apollo Client を使って GraphQL API にアクセスするサイトに遷移した状態で Chrome Dev Tools を開くと Apollo のタブが表示されます。そこでクエリの実行や、API 仕様の確認、ローカルの Apollo cache の確認等を行うことができます。

GraphQL 関連のテストコードについて

Apollo Client を使った React Component の開発で、Query および Mutation 実行のテストを実施するには、テストフレームワークの Jest、react-testing-library とあわせて、Apollo 公式でも紹介されている MockedProvider を用いる方法が一般的かと思います。

クエリとクエリに対するレスポンスを組み合わせたモックデータを用意しておき、ApolloProvider の代わりに MockedProvider でテスト対象の component をラップすることで、API サーバーや Network 環境に依存せず、モックで指定したクエリがリクエストされると、モックでそれに対応するように用意したレスポンスデータが確実に取得できる仕組みを作れます。

その仕組みと react-testing-library を使って、component で render される UI 上の操作をトリガーにして実行される、クエリのテストを行うことができます。

Query だけではなく Mutation もモックすることができて、便利なツールではありますが、テストケース毎にモックデータは手動で作成しなければならない点が、なかなか骨が折れる作業です。

実際にアプリケーションを動かして、テスト対象の component を render し、Query に渡される variables やレスポンスの値を Console に出力し、ブラウザの Dev Tools 上で一個一個オブジェクトをコピーして、エディタに貼り付けしたりする作業が発生します。

AutoMockedProvider の作成

そこで、わざわざテスト作成やスキーマ変更の度に、手動でモックデータを用意しなくても、GraphQL スキーマで定義されている型を見て、自動でクエリに対するレスポンスをモックしてくれる AutoMockedProvider を、 こちらの記事 を参考にして作成しました。

MockedProvider の代わりに、AutoMockedProvider を用いてテスト対象の Component をラップすることで、MockedProvider を使ってテストしていた内容と同じテストが実施できます。

MockedProvider を使って毎回モックデータを用意し、テストを実施することに疲れている方は是非、お試しください。

(紹介先の記事では、graphql-toolsmakeExecutableSchema()に渡すschemaSDLが json ファイルで定義されていますが、graphql-tagのライブラリを併用すれば、graphql ファイルでも同様にschemaSDLとして適用することも可能です)

リニューアルを振り返って

今回のリニューアルでは、GraphQL、TypeScript、React をセットで採用したことにより、フロント側では GraphQL Code Generator を使って、あらかじめ用意しておいた GraphQL スキーマから、TypeScript の型だけではなく、React の Hooks 関数まで生成して利用できたことが、開発効率の向上に非常に影響を与えたと思います。

GraphQL API のクライアントで、アプリケーション全体の状態管理を行う Apollo Client の cache 機構の使い方等を体得するまでに、学習コストは決してゼロではありませんでしたが、TypeScript と GraphQL の型システムの恩恵をフルに受け、Next.js のレールにのっかり、型安全な開発環境を手に入れることができました。

我々、開発者の体験だけではなく、今後のプロダクト全体への生産性にも良い影響を及ぼしてくれると確信しています。

さいごに

メドレーではエンジニア・デザイナーを積極募集しています。

「テクノロジーを活用して医療ヘルスケアの未来をつくる」というミッションに共感し、課題解決を行いたい方は是非、ご応募ください。

https://www.medley.jp/jobs/

株式会社メドレーDeveloper Portal

© Medley Developer Portal