NxのGeneratorを活用した管理画面200ページのリニューアル事例

ファインディ株式会社でフロントエンドの開発をしております千田(@_c0909)です。

この記事はFindy Advent Calendar 2024 24日目の記事です。

adventar.org

転職サービス『Findy』の管理画面リニューアルプロジェクトで、約200ページ規模の開発をしました。

管理画面の機能は構成が似通っているため、NxのGeneratorによるコード自動生成を活用して画面作成の効率化を図りました。

本記事では、その取り組みについて共有させていただきます。

Nxについては以前の記事で紹介しておりますので、併せてご覧ください。

tech.findy.co.jp

Generator導入の背景

既存の管理画面は約50の機能で構成されています。

各機能には一覧・詳細・作成・編集の4種類の基本画面が必要となるため、合計で約200ページ(50機能 × 4画面)の開発が必要でした。

開発工数を試算したところ、コンポーネント作成、ロジック実装、テスト作成を含め、1画面あたり約160分の作業時間が必要になります。

これを単純計算すると、全画面の開発には約67日(160分/画面 × 200画面 ÷ 1日8時間)もの期間が必要という結果になりました。

そのため、構造が類似した200ページの開発の効率化を目指し、Generatorによるコードの自動生成を検討しました。

なぜNxのGeneratorなのか

冒頭でも書いたように、FindyにおいてはNxのGeneratorを選択しました。

その理由は既にNxを使っていたという背景に加え、次の技術的な利点があるためです。

Nxエコシステムとの親和性が高い

GeneratorがNxのコア機能として提供されているため、開発者は追加のセットアップの必要なく利用可能です。

公式やサードパーティのNxプラグインはGeneratorに対応しているものがほとんどであり、コンテキストスイッチの負荷を最小限にできます。

また、Nx ConsoleをIDEにインストールすれば、GUIからGeneratorを起動できます。

Nx Consoleはオプションの確認やコマンドのDry-runといった機能もサポートされているため便利です。

nx.dev

再利用性が高い

Generatorから他のGeneratorを呼び出すことができます。

これにより、Generatorの構成をシンプルに保つことができます。

export default async function(tree: Tree, schema: Schema) {
  // 設定ファイル群の追加
  generateFiles(
    tree,
    path.join(__dirname, 'files/feature'),
    `libs/projectName/src`,
    substitutions
  );

  // 特定のフィーチャーコンポーネントの追加
  generateFiles(tree, path.join(__dirname, 'files'), options.projectRoot, {
    ...names(options.name),
    importPath: options.importPath,
    projectRoot: options.projectRoot,
    ...
  });
}

nx.dev

Generatorに対するテストが書けるため、メンテナンスがしやすい

仮想ファイルシステムを利用してファイル生成の結果を確認できます。

例えば、次のようにJestでGenerateコマンド実行後のファイルの存在を確認できます。

import { Tree } from '@nx/devkit';
import generator from './generator';

it('should generate expected files', async () => {
  await generator(tree, defaultOptions);

  expect(tree.exists('/src/components/index.ts')).toBe(true);
});

どうやって対応したか

NxのGeneratorを使用したコード生成の実装について説明します。

実際にテンプレートファイルからファイルを生成するには、generateコマンドを利用します。

画面の雛形を生成するためのコマンドは次のように設計しました。

npx nx generate @findy-code/workspace-plugin:features user \
  --featureType=detail \
  --featureNameJa=ユーザー
  • user: 機能名(変数名やファイル名として使用)
  • --featureType: 画面タイプ(list/detail/create/edit)
  • --featureNameJa: 画面に表示する日本語名

これらの引数の設定はschema.jsonで行います。

{
  "$schema": "http://json-schema.org/schema",
  "properties": {
    "name": {
      "type": "string",
      "description": "The name of the feature (feature-<name>-{list,detail,create,edit}) .",
      "$default": {
        "$source": "argv",
        "index": 0
      },
      "x-prompt": "What name would you like to use?"
    },
    "featureType": {
      "type": "string",
      "description": "The mode of feature generation. Possible values are 'list', 'detail', 'create', 'edit', or leave empty to generate all types.",
      "default": ""
    },
    "featureNameJa": {
      "type": "string",
      "description": "The name of the feature in Japanese",
      "default": ""
    }
    ...
  }
}

次に、生成されるファイルのテンプレートを作成します。

例えば、 詳細画面用のカスタムフックのテンプレートは次のように実装しました。

export const use<%= functionName %>Detail = ({ <%= propertyName %>Id }: Props) => {
  const <%= propertyName %> = { id: <%= propertyName %>Id };
  // ここに詳細画面用のロジックを実装

  return {
    <%= propertyName %>
  } as const;
};

次のように展開されます。

export const useUserDetail = ({ userId }: Props) => {
  const user = { id: userId };
  // ここに詳細画面用のロジックを実装

  return {
    user
  } as const;
};

テンプレート内では<%= %>記法で動的な値を埋め込みます。

テンプレートファイルはsrc/__featureName__-detail.ts.templateという形式で作成し、ファイル名も可変にしています。

テンプレート内で使用するfunctionNamepropertyNameなどの変数は、Nxが提供する names関数を使用して適切な命名に変換します。

例えば、次のようにnames('my-name')と呼び出すことで、classNamepropertyNameconstantNameのように、よくある命名規則に従った文字列を作成できます。

names('my-name');
// {
//   name: 'my-name',
//   className: 'MyName',
//   propertyName: 'myName',
//   constantName: 'MY_NAME',
//   fileName: 'my-name'
// }

最後に、generateFiles関数を使用してファイルの生成を実装します。

この関数にテンプレートのディレクトリ、出力先のディレクトリ、そして置換する変数を指定することで、実際のファイル生成が行われます。

  const substitutions = {
    featureName: names(schema.name).name,
    featureNameJa: schema.featureNameJa,
    functionName: names(schema.name).className,
    ...
  };

  generateFiles(
    tree,
    path.join(__dirname, 'files/feature'), // テンプレートを定義しているファイルのディレクトリ
    `${options.projectRoot}/src`, // 生成したいディレクトリ
    substitutions // テンプレートで置換される変数
  );

これにより、コマンド1つで必要な画面の雛形を生成できるようになりました。

生成されるファイルには、カスタムフックの実装だけでなく、対応するテストファイルやコンポーネントなども対応しています。

今後やりたいこと

NxのGeneratorの導入により、画面を作り込むことは出来ましたが、新たな課題も見えてきました。

現在のGeneratorは画面のコードを一気に生成するため、ファイルの変更数が増えPull requestの粒度が大きくなり、レビューの負荷が高くなりました。

今後は、コード生成の粒度をより細かく制御できるように改善を進めていく予定です。

具体的には、画面を構成する要素ごとに生成を分割し、Componentの生成、ルーティング設定の追加、カスタムフックの実装といった単位でPull requestを作成できるようにすることで、レビューの負荷を軽減したいと考えています。

適切なPull requestの粒度については、既に以前の記事で紹介しておりますので、気になる方は是非ご覧ください。 tech.findy.co.jp

まとめ

この記事では、約200ページ規模の開発におけるNxのGeneratorを活用した事例をご紹介しました。

私自身、Nxを単にモノレポ管理ツールとして認識していましたが、今回のプロジェクトを通じてGeneratorという強力な機能の存在を知ることができました。

今回の取り組みを通じて、類似した構造を持つ画面を大量に開発する場合、NxのGeneratorが非常に効果的なツールとなることが分かりました。

この記事が皆様のプロジェクトにおける開発効率化のヒントになれば幸いです。

それでは、また次回の記事でお会いしましょう!


ファインディでは一緒に会社を盛り上げてくれるメンバーを募集中です。興味を持っていただいた方はこちらのページからご応募お願いします。

herp.careers