🐕

scaffdog で管理画面を生成する

2024/12/25に公開

「よ〜し、管理画面を開発するぞ〜」
「でも10個以上も管理コンテンツあってめんどくさいな〜」
「どうせ似たような画面ばっかりなんだから、コマンドをポチポチして全部作れないかな〜」

というのに真面目にチャレンジして、管理画面をほぼ丸ごと scaffdog テンプレートで生成できるようにしてみました。

次の呪文を唱えて、

pnpm scaffold artist artists アーティスト

pnpm exec scaffdog generate form-input-text -f --answer "property:name" --answer "required:true" --output "/model/artist/components/form-with-preview"

pnpm exec scaffdog generate form-input-text -f --answer "property:iconUrl" --answer "required:true" --output "/model/artist/components/form-with-preview"

pnpm exec scaffdog generate form-input-tags -f --answer "property:tags" --answer "required:true" --output "/model/artist/components/form-with-preview"

pnpm exec scaffdog generate form-input-switch -f --answer "property:authorized" --answer "required:true" --output "/model/artist/components/form-with-preview"

pnpm exec scaffdog generate search-input-checkbox -f --answer "property:tags" --output "/model/artists/components/list/search"

ちょちょっと編集するだけで、コンテンツ管理に必要なこれらの画面ができちゃうようになったんです!

アーティストモデル一覧画面
一覧画面

アーティストモデル新規作成画面
新規作成画面

アーティストモデル編集画面
編集画面

そのエッセンスをテンプレートリポジトリとして公開したので、ここで公開した内容に基づいて今回のチャレンジを紹介します。テンプレートリポジトリには scaffdog に加えて Next.js, Mantine UI, Zustand などを利用しています。

https://github.com/yumemi-inc/next-admin-studio

チャレンジの概要

管理画面で管理したいコンテンツがA, B, Cの3つあるとします。

まず、それぞれのコンテンツに対して用意するページは基本的に同じです。例えば今回は

  • 一覧画面
  • 新規作成画面
  • 編集画面

の3ページがそれぞれのコンテンツに対して存在すると考えます。

一方で、新規作成画面や編集画面で入力する項目は、コンテンツのプロパティによって異なります。例えばざっくり下記のようなモデリングを想定するとき、

type A = {
  name: string;
  sex: "Male" | "Female" | "Other";
  birthday: Date;
};

type B = {
  title: string;
  description: string;
  price: number;
};

type C = {
  phase: "X" | "Y" | "Z";
  checks: string[];
};

Aにはテキストインプット・ラジオボタン・日付インプットを、Bにはテキストインプット・テキストエリア・数値インプットを、Cにはセレクトボックス・チェックボックスを用意したいです[1]

以上を踏まえ、

  • コンテンツに共通するページ部分はなるべく一括で生成したい
  • コンテンツごとに異なるフォームのインプットについては、テンプレートで一定の標準化をしながら組み合わせをカスタマイズして生成したい

というのが、今回のチャレンジの概要でした。

なぜ共通コンポーネントではなくテンプレートなのか?

一定のパターンで繰り返し発生する要件には、「共通コンポーネントを用いる」というアプローチがよく取られます。共通化することでコード量が減り、テスト効率も上がるためです。今回のプロジェクトでも、Mantine UI などを用いて、各種インプットやテーブルなど“表層・骨格”にあたる部分は共通化しています。

一方で、管理画面で扱うコンテンツは、見た目が似ていてもそれぞれ独立した別の機能・要件を持ちます。もし「見た目が似ている」という理由だけで"構造"以上のレベルで共通化してしまうと、設計変更が難しくなり、柔軟性を損なうリスクが高いです。こうした背景から、共通化できるところは共通化しつつも、コンテンツごとに分離したコードを書くことが好ましいです。

UXの5段階モデル
https://goodpatch.com/blog/elements-of-ux より

以上を踏まえて、一定のパターンを保存しながら全く別のモジュールとして書きたく、「テンプレートからコードを生成する」というアプローチを考えました。

細かい(そして深い)話になるのでここでは簡単な紹介にとどめますが、ディレクトリ構成を

 ├── app
 ├── common
 └── model
     ├── ...各種コンテンツモデル
     └── common

のようにしており、

  • common/配下にインプットコンポーネントやバリデーションなど標準的なモジュールを配置
  • model/common/配下に「コンテンツの作成状態に関するモジュール」などコンテンツ間で共通化して取り扱いたいモジュールを配置
  • model/[モデル名]/(例: model/artist/)配下にそのモデル独自のモジュールを配置(ほとんどテンプレートから生成)

として、共通化とテンプレート化のちょうどいい塩梅を探りました。

テンプレートを用いた管理画面の開発手順

先程のテンプレートリポジトリで管理画面を開発する際の手順を紹介します。例として、Artist という管理コンテンツを想定します。一連の流れを通じて

  • /artists にアーティスト一覧画面
  • /artists/new にアーティスト新規作成画面
  • /artists/[id] にアーティスト編集画面

が出来上がります。

1. 立ち上げ

まず、scaffold コマンドを用いて Artist の一覧・新規作成・詳細画面の大部分を作成します。

# $1: モデル名
# $2: パス名
# $3: 呼称
pnpm scaffold artist artists アーティスト

scaffold コマンドは、実際には複数の scaffdog テンプレートを一括で実行するシェルスクリプトを実行しています。

https://github.com/yumemi-inc/next-admin-studio/blob/79057812983e25f6a2906fdb2b9b45c24af67f54/.scaffdog/init-content.sh#L1-L30

2. 新規作成・詳細フォームの編集

次に、モデルのプロパティに合わせてインプットを生成します。

例えば、Artist に下記のプロパティの入力が必要な場合、

export type Artist = {
  name: string;
  iconUrl: string;
  tags: string[];
  authorized: boolean;
};

下記のコマンドを実行することで、詳細画面のフォームに各種インプットが追加されます[2]。インプットのUIごとにテンプレートが用意されており、各プロパティに対応するものを呼び出しています。

# name
pnpm exec scaffdog generate form-input-text -f --answer "property:name" --answer "required:true" --output "/model/artist/components/form-with-preview"

# iconUrl
pnpm exec scaffdog generate form-input-text -f --answer "property:iconUrl" --answer "required:true" --output "/model/artist/components/form-with-preview"

# tags
pnpm exec scaffdog generate form-input-tags -f --answer "property:tags" --answer "required:true" --output "/model/artist/components/form-with-preview"

# authorized
pnpm exec scaffdog generate form-input-switch -f --answer "property:authorized" --answer "required:true" --output "/model/artist/components/form-with-preview"

あとは各種インプットのラベルやディスクリプション、バリデーションなどを編集してフォームの完成です[3][4]。「ラジオボタンで一定の値を選択したときに、さらなる選択肢としてチェックボックスが現れる」などテンプレートの内容とはかけ離れた特殊なインプットがある場合は、テンプレート生成とは別に独自に実装します。

フォームの内容と連動するプレビューをいい感じに編集すれば、新規作成画面・編集画面がおおよそ完成します。

アーティストモデル編集画面

3. 一覧ページの編集

一覧には検索フォームがついており、検索フォームのインプットもテンプレートを用いて追加することができます。

# タグ検索を追加
pnpm exec scaffdog generate search-input-checkbox -f --answer "property:tags" --output "/model/artists/components/list/search"

詳細フォームと同様にラベルやディスクリプションを編集してください。

最後に、一覧ページのテーブルに表示する要素を調整して、一覧ページがおおよそ完成します。

アーティスト一覧画面

以上がおおよその開発手順となります。もちろん、デフォルトで用意されている内容だというだけで、scaffdogテンプレートはプロジェクトに合わせていかようにも編集可能です。

実際にやってみて

実際に、テンプレートによる生成を利用して管理コンテンツ12個・ファイル数2800程度・行数13万行程度の管理画面の開発を行いました。そのときに感じたテンプレート開発の長所・短所・その他所感をご紹介します。

長所

テンプレートを作り込んでレバレッジを効かせられる

テンプレートを用いた開発は、大まかに以下の手順で進みました。

  1. 最初のコンテンツを手書きで開発する
  2. そのコードをテンプレートに落とし込む
  3. 残りのコンテンツをテンプレートから生成する

最初に手動で作ったコードが「原型」になるため、この段階で十分に動作確認やリファクタリングを行うことが重要です。初期段階は普段より手間がかかりますが、テンプレートさえ整えば、後の開発は非常に短期間で進められます。

実際のプロジェクトでは、最初のコンテンツをテンプレート化するまでに全体の2/3ほどの時間をかけ、残る11個のコンテンツは残り1/3の時間で一気に仕上げました。テンプレートを準備するからこそ、最初にじっくり検証・改善に取り組み、後からの作業効率を大きく高めることができたのです。

テンプレートは制限ではなく単なる「土台」

テンプレートは制限ではなく単なる「土台」なので、テンプレートから外れるカスタマイズをしたいコンテンツに関しても「途中までテンプレート、途中から手書き」ができるのが柔軟で強力でした。

命名規則などが揃えやすく、強い「反復」のデザインが可能

テンプレート生成だと自然と命名規則や実装パターンなどが揃います。コードに対して強めの「反復」を施すことになり、1を見て10を把握することが可能になります。

「土台」から外れて特別なカスタマイズをするときにも、他の部分が強く反復されているおかげでどこが「そのモデルに特有」なのかがはっきりと浮かび上がります。

短所

テンプレート管理がめんどう

mdによるシンプルなテンプレート管理・helperによる柔軟な記述が可能な scaffdog を用いても、巨大なテンプレート管理は面倒でした。

特に、一度生成して実装した箇所を変更する場合は「テンプレートも変更しといて!」となり、「一手増えた感」がありました。

生成スピードにレビューが追いつかないかも

生成する範囲が広がると、Pull Requestでのレビューがどうしても大変になります。

実際、1コンテンツあたり5,000~7,000行のコードを生成していたので、PRを分割しようにもそこそこ手間がかかります。開発の勢いを落としたくなかったこともあり、テンプレートから生成されたコードは最小限の確認にとどめ、手作業が絡む部分を中心にレビューする方針をとりました。

ただ、大量のコードを一気に生成している分、後になって実装漏れに気づくケースもありました。

その他

Zustand の Slices パターンが強力

管理画面の複雑なフォームを作るうえで「formを一定は中央集権的に管理しながら、各種インプットの都合(どういうバリデーションが設定されているか?など)は自律分散的に管理したいなあ」と考えていました。そのようなときに Zustand の Slices パターンが強い味方になってくれました。

https://zustand.docs.pmnd.rs/guides/typescript#slices-pattern

各種インプットにまつわるプロパティをまとめてひとつのSliceとして表現しながら、

https://github.com/yumemi-inc/next-admin-studio/blob/79057812983e25f6a2906fdb2b9b45c24af67f54/src/model/artist/components/form-with-preview/form/inputs/name/slice.ts#L15-L33

モデルのフォームのstoreで各Sliceを展開することで、中央集権と自立分散を両立しています。

https://github.com/yumemi-inc/next-admin-studio/blob/79057812983e25f6a2906fdb2b9b45c24af67f54/src/model/artist/components/form-with-preview/store/index.ts#L14-L35

テンプレートという手法は粒度問わず利用できる

「コンポーネントファイル+Storybookファイル」のような小規模なものから、「あるインプット部分のコード」といった中規模なもの、さらには「画面全体をほぼまるごと」という大規模なものまで、テンプレートという手法はさまざまな粒度で活用できるのだと改めて実感しました。今回のように一気に画面全体を生成しなくても、プロジェクトやチームの状況に合わせて取り入れやすい規模で導入できるのが魅力です。

AIによる生成と比べると、テンプレート生成は「純粋」

AIによるコード生成と比べると、テンプレート生成の最大の特徴は「同じ引数を与えれば、常に同じコードが得られる」という“純粋さ”にあると思います。AI生成のような柔軟性はないものの、テンプレートさえしっかり作り込めば、引数を確認するだけで生成物の品質をある程度保証できるのが利点です。

一方でAIによる生成は、状況に応じたコードを柔軟に生成できる反面、「思わぬコードが混ざるかもしれない」という懸念から、結果を細かくチェックせざるを得ない場合が多いです。テンプレート生成はそうした不確定要素が少なく、大量のコードを安定して生み出したい場面に向いていると感じます。

実践的には「AIを使ってテンプレート自体を生成し、そのテンプレートからコードを作る」という方法が非常に便利でした。テンプレートの純粋さとAIの柔軟性を組み合わせることで、両者のメリットをうまく活かせます。


以上、scaffdog テンプレートを用いて管理画面をほぼ丸ごと生成するアイデアをご紹介しました。テンプレートによるコード生成は一定のパターンを繰り返す開発には有効なアプローチのひとつでしょう。

テンプレートリポジトリはかなりopinionatedな感じで作ってあるので、もし実際に利用される際はご不明点などお気軽にお問い合わせください。

脚注
  1. データ型とインプットの対応は複数あり、他のインプットの組み合わせも考えられます ↩︎

  2. コマンドは pnpm gen で対話式に実行することもできます ↩︎

  3. テンプレートリポジトリでは、Injectionの都合で一部手作業が発生します ↩︎

  4. 余談ですが、バリデーションもextention的に気軽にカスタマイズできるようにしています ↩︎

株式会社ゆめみ

Discussion