サーバサイドの進捗に依存しないための Next.js x GraphQL のフロントエンド開発の工夫

こんにちは!
マネーフォワード クラウド横断本部のWebエンジニアの はるやま (@linnefromice) です。

現在クラウド横断本部では BtoB 向けサービスのリプレイスプロジェクトの開発を行っており、今回は とフロントエンド開発においてそのプロジェクトで悩んだ点とそれに対する工夫の紹介をさせていただきたいと思います!(Special Thanks かわかみさん(@thinceller)、一緒にアーキテクチャの実現とこのフローを考えてもらいました)

プロジェクトの特徴

今回のプロジェクトはリプレイスのため、既存のサービスで実現している機能の再現/改善または新規機能の開発を行うのですが、

  1. 現行サービスのコアモデル/データをベースに扱う必要がある
  2. サービスの特徴として、他のクラウドサービスとの連携により実現したい機能が多い

のような特徴があります。
これらの特徴により、

  • サーバサイドの開発
    • 要件定義/設計および他サービスとの連動の詳細設計など上流でやるべき活動のコストが高く、すぐ開発着手はできない状態
  • フロントエンドの開発
    • リプレイスにおける機能要求/要件整理と現行システムの挙動の整理ができれば、デザイン->開発とサーバサイドより比較的早く開発着手ができる状態

フロントエンドがサーバサイドより比較的早く開発着手ができる状態なので、サーバサイドに極力依存しない開発フローで出来る限りproductionレベルのフロントエンド構築を進める方法を考えました。

本プロジェクトにおいてフロントエンドは比較的モダンな Next.js/TypeScript ベースにしており、フロントエンド /サーバサイドの連携方式は GraphQL を選択しています。

課題

前述の通り、出来る限り production レベルに近いフロントエンドを最初から作っていきたい、具体的にはなるべくサーバとの通信も考慮してフロントエンド開発を進めたいのですが、サーバサイドのAPIは全くない状態です。
ハードコードされたデータを使った描画のみの画面モックからサーバ通信を考慮したロジック込みの画面作成では大分考慮する点が増加するので、できれば最初からサーバ通信を加味したいと考えていました。

工夫したプロセス

サーバとの通信を最初から意識したいため、各画面に対して必要なデータ/実現したい機能からサーバサイド/フロントエンドのインターフェースをざっくり考えたら、そのインターフェースを利用して、フロントエンドの実装において

  • クライアントライブラリの利用はなるべく自動で
  • 実際のサーバサイド連携も加味した実装となる

ような流れを意識して、ツール選定や作業フローを考えました。

工夫を実現するためのメインアプローチは、

です。

サーバサイドは Rails ベースにしているので、 GraphQL API の実現には graphql-ruby を採用しています。
graphql-ruby を利用したサーバサイドでのスキーマ定義からスタートします。

1. Code First で graphql-ruby の query/mutation の schema定義

最近では GraphQL Schema 作成においてはスキーマを直接記述する方式 (Schema First) から、サーバサイドのコードから GraphQL Schema を生成するような Code First の方針がよく用いられています。
Code First の方針では以下のようなメリットがあります。

  • GraphQLスキーマと実際のプロダクトコードである resolver の同期を常にしている状態になる
  • 個別の type/query/mutation を表現するコードが個別ファイルに記述されるので、(GraphQLスキーマ自体のファイルは1つでも)コードとして個別のモジュールとして管理できる

Rails では graphql-ruby、TypeScript では GraphQL Nexus などで Code First なアプローチを実現できます。

Schema First と Code First についてはどちらの方針が良いかは開発者の好みもあると思いますので、気になる方は下記など参考にしてもらえるとよりイメージできるかと思います!

参考

2. .graphql の schema file を自動生成

サーバサイドで type と query あるいは mutation のコードを記述したら、 .graphql のスキーマファイルを自動生成します。
bin/rake graphql:schema:idl
※ 公式ドキュメントからは見つけづらいのですが、 .json または .graphql 形式でスキーマファイルを出力する rake task が準備されています。

3. schema file と operation 定義から、graphql-code-generatorを利用してAPI利用部分を自動生成

フロントエンドで画面で表示/更新処理を行うデータから .graphql ファイルで operation の定義を行います。
この operation の定義と前ステップで作成したスキーマファイルを用いて hooks を自動生成します。

graphql-code-generator を利用する際に@graphql-codegen/typescript-react-apolloプラグインも含めて利用することで、 GraphQL API をコールしレスポンスの状態の監視を含めたロジックを持つ React の hooks を自動生成することができます。

  1. ビルド用の設定ファイルである codegen.yml を準備
  2. graphql-codegen --config codegen.yml

で自動生成することができます。
こちらで生成した hooks を用いて GraphQL API 連携を加味したコンポーネント/画面作成を行なっていきます。

ex. codegen.yml

overwrite: true
schema: './schema.graphql'
documents: 'src/**/*.graphql'
generates:
  src/__generated__/graphql.ts:
    plugins:
      - 'typescript'
      - 'typescript-operations'
      - 'typescript-react-apollo'
    config:
      scalars:
        ISO8601DateTime: string
        ISO8601Date: string

4. schema file と Apollo MockでGraphql APIのMockを生成

自動生成した hooks を利用してコンポーネント/画面実装をしたら、実際にそれを動かすためには画面が呼び出す GraphQLAPI からのレスポンスが必要になります。
このレスポンスを Apollo Mock を利用し実現します。

Apollo Mock は自動生成したスキーマをインプットとすることで、スキーマの定義にマッチしたレスポンスを構築できます。
カスタムスカラーについてもモックデータを設定できたり、モックデータ自体もコードで書けるので固定値もランダムな値どちらも利用できます。
(本プロジェクトでは faker.js を利用して、ランダムな値を表示させています)

ex.

import { readFileSync } from 'fs';
import { ApolloServer, gql, MockList } from 'apollo-server-micro';
import faker from 'faker/locale/ja';

const typeDefs = gql(
  readFileSync(
    './schema.graphql',
    'utf-8'
  ).toString()
);

const mocks = {
  ISO8601DateTime: () => new Date().toISOString(),
  ISO8601Date: () => new Date().toISOString().split('T')[0],
  Members: () => ({
    nodes: () => new MockList(10),
  }),
  Member: () => ({
    name: `${faker.name.lastName()}${faker.name.firstName()}`,
    email: `${faker.random.alphaNumeric(10)}@example.com`,
    identification_code: faker.random.alpha({ count: 5 }),
  }),
};

const apolloServer = new ApolloServer({ typeDefs, mocks });

export const config = {
  api: {
    bodyParser: false,
  },
};

export default apolloServer.createHandler({ path: '/api/graphql' });

Apollo MockをフックさせたエンドポイントをAPI Routeに実装

Next.js の API Route を利用することで Apollo Mock で構築したモックサーバ用のエンドポイントを Next.js アプリケーション内に実現することができます。

具体的には pages/apiapi/graphql をエンドポイントとしたモックサーバを設定しています。
上記のようにすることで /api/graphql に向けて API コールすることでモックサーバによるレスポンスを取得できます。

ex. pages/api/graphql.ts

...
export default apolloServer.createHandler({ path: '/api/graphql' });

Next.js examples に上記の実装例があるので、それを参考にして簡単に対応できました!

参考
next.js/examples/api-routes-apollo-server-and-client at canary · vercel/next.js

5. テスト/Storybookの実装

様々な箇所で見かける組み合わせですが、

を利用しています。
Jest には ApolloのMockedProvider を利用し、 Storybook にはApollo 用の Storybook Addonを利用することで、モックを利用したテスト/Storybookコンポーネント実装を簡単に行うことができました!

Jest / React Testing Library
ex. src/components/.../__tests__/XxComponent.tests.tsx

describe('XxComponent', () => {
  const mocks = [
    {
      request: {
        query: GetMembersDocument,
      },
      result: {
        data: {
          members: {
            nodes: [
              {
                id: '1'
                name: 'Tom'
                email: 'tom@example.com'
              },
              ...
            ],
          },
        },
      },
    },
  ];
  it('has text', async () => {
    render(
      <MockedProvider mocks={mocks} addTypename={false}>
        <XxComponent />
      </MockedProvider>
    );
    await waitFor(() => {
      expect(screen.getByText('Tom')).toBeInTheDocument();
      expect(screen.getByText('tom@example.com')).toBeInTheDocument();
    });
  });
});

Storybook

ex. .storybook/preview.js

import { MockedProvider } from '@apollo/client/testing';

export const parameters = {
  actions: { argTypesRegex: '^on[A-Z].*' },
  apolloClient: {
    MockedProvider,
  },
};

ex. src/components/.../XxComponent.stories.tsx

export default {
  title: 'components/.../XxComponent',
  component: XxComponent,
} as Meta;

export const Default: Story = () => <XxComponent />;

Default.parameters = {
  apolloClient: {
    mocks: [
      {
        request: {
          query: GetMembersDocument,
        },
        result: {
          data: {
            members: {
              nodes: [
                ...
              ],
            },
          },
        },
      },
    ],
  },
};

まとめと感想

production code のみならずモックデータはテスト/Storybookでも利用することで、Code First で生成した GraphQL Schema をベースにフロントエンド全ての開発を進めていけるようになり、とても心地よい開発者体験につながっています。
また

  • 最初にスキーマ定義を Code First で作成することで、”フロントエンド/サーバサイド接続はインターフェース定義から”ということをチームの当たり前にできている
  • スキーマ定義、API の実ロジック作成、フロントエンド全般 を分けて進めることが自然になっている
    • かつ、サーバサイド開発とフロントエンド開発を並行で進められる

というような、個人だけでなくチーム開発にも貢献する部分があり、良いことだらけでした!
ぜひ体験したことない方は取り入れてみてはいかがでしょうか。

クラウド横断本部では、このようにモダンな技術を取り入れつつ技術的な負債を解消したり、基盤となっているマイクロサービスをより成長させていく活動を行っており、これらを一緒に推進していく仲間を募集しております…!

【サーバサイドエンジニア】_東京(田町) | 株式会社マネーフォワード
【フロントエンジニア】_東京(田町) | 株式会社マネーフォワード

最後までお読みいただきありがとうございました。


マネーフォワードでは、エンジニアを募集しています。
ご応募お待ちしています。

【サイトのご案内】
マネーフォワード採用サイト
Wantedly
京都開発拠点

【プロダクトのご紹介】
お金の見える化サービス 『マネーフォワード ME』 iPhone,iPad Android

ビジネス向けバックオフィス向け業務効率化ソリューション 『マネーフォワード クラウド』

おつり貯金アプリ 『しらたま』

お金の悩みを無料で相談 『マネーフォワード お金の相談』

だれでも貯まって増える お金の体質改善サービス 『マネーフォワード おかねせんせい』

金融商品の比較・申し込みサイト 『Money Forward Mall』

くらしの経済メディア 『MONEY PLUS』

Pocket