Medley Developer Blog

株式会社メドレーのエンジニア・デザイナーによるブログです

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

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

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

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

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

リニューアルの背景

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

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

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

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

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

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

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

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

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

採用ライブラリ 説明
Next.js React用のフレームワーク(ボイラープレート)
TypeScript JavaScriptのスーパーセットで、静的型付け言語
React UIを構築するためのライブラリ(バージョン16.8.0でリリースされたhooksを全面的に使用)
Apollo Client GraphQL APIのクライアントで、アプリケーション全体の状態管理を実施
GraphQL Code Generator GraphQLスキーマから定義ファイル(型、カスタムhooks等)を生成
emotion + Styled System CSS 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 実行タイミング
useQuery Componentが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()に渡すschemaSDLjsonファイルで定義されていますが、graphql-tagのライブラリを併用すれば、graphqlファイルでも同様にschemaSDLとして適用することも可能です)

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

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

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

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

さいごに

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

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

www.medley.jp