突然ですが、皆さんはフロントエンド開発における「ルール」について、考えたことはありますか?
今回、実務の中でルールを考える機会があったため、その備忘録として記事にまとめました。
今回は社内のためのルールというより、「AIエージェントに私の理想とするコードを生成してもらうためのルール設計」として取り組んでいきます。
なぜルールを作るのか?(目的の明確化)
ルールを考えよう!となったとき、ただ闇雲に考えても答えは出ません。
フロントエンドのルールといえば、クラスの命名規則やコーディングガイドラインなど、事細かに考えられることは山ほどあります。
しかし、「なぜそれを作成しないといけないのか」「どんなものを作成したらいいのか」という目的を明確化させないと、後々軸がブレて困ることになります。
目的を決めるための「理想の姿」
目的を見つけるための一番の近道は、「自分の思う理想の姿(コード)」を言語化することです。私の考える「理想のコード」は以下の通りです。
- シンプルなソース、最小限(無駄のない)のコード
- 保守がやりやすい(見返してやりやすい)
- コンポーネント(ボタンデザインなど)が共通化されている
- 役割単位でも共通化されている
- 公式ドキュメントのルールに則っている
- プラモデルをつくるように組み合わせていける
この「理想のコード」を達成するために、ルールの目的を以下の2点に落とし込みました。
1:「誰が書いても(AIが書いても)」同じ構造・同じ品質になるようにする
2:開発時の「無駄な迷い」を減らし、効率を上げる
ルール設計のための3ステップ
AIに適切なコードを出力させるためのルール(制約)を考えるために以下のステップを行ってみました。
Step 1: 「やらないこと」を決める
無駄なコードを削ぎ落とす。
例)
- 情報のベタ書き(ハードコード)
- 類似コードの増殖
- any 型と as アサーションの禁止(ライブラリの都合でやむを得ない場合のみ例外)
Step 2: パーツの「規格」を決める
フォルダ構成・型などパーツの形を揃える
例)
- 配置場所(ファイル構成)の統一(フォルダ構成は公式を参照する)
- 命名規則の統一
Step 3: パーツの「切り出し方」を決める
何をひとつのブロック(コンポーネント化・共通化)にするか基準を決める
例)
- 「利用者から見て同じ概念」ならコンポーネント化
名前をつけた時に自然に意味が伝わる塊はコンポーネントとして切り出す - 「変更理由が同じ」なら共通化
同じバリデーションルールが複数箇所にある場合、仕様変更時に全部同時に直す必要があるため共通化させる - 役割の分担
コンポーネント自身の責務は小さくシンプルに保つ
開発ガイドライン・コーディング規約の検討
ここからは、実際に策定したルールの具体例と基準を記載していきます。
今回は以下のフレームワークを使用する想定です。
- vue.js
- TailwindCSS
まずは、Step 1でAIが勝手に複雑なコードや古い記法を生成するのを防ぎ、レビューの負担を減らすための「禁止事項(制約)」を定義します。
マジックナンバー・ハードコードの禁止
情報や数値の直書きは原則禁止です(0など、初期化のための値は例外)。AIは文脈を無視してベタ書きしがちなため、文字列・数値・設定値は必ず utils/const.ts 等へ定数化させます。
any 型と as アサーションの禁止
TypeScriptの恩恵を殺す any と、強制的な型推論(as)は原則使用しません(ライブラリの都合でやむを得ない場合のみ例外)。
CSSのベタ書き・独自カラーの禁止(Tailwind First)
CSSは原則書かず、TailwindのUtility Classを使用します。bg-[#fff] のようなベタ打ちも禁止し、独自の色は必ず tailwind.config に定義し、一箇所にまとめて使用します。
「見た目そのまま」のクラス名(色名など)の直書き禁止
bg-blue-500 のような具体的な色名に依存したクラス名の使用を禁止します。「青色だから」ではなく「主要なボタンだから」という理由でスタイルを当てるべきです。デフォルトのカラーパレット直書きはテーマ変更時に破綻するため、必ずセマンティック(意味・役割)な独自定義クラス(bg-primaryなど)を使わせることで、変更を容易にします。
過度な設計の禁止
要求されていない機能は実装しません。AIの「気を利かせすぎたオーバースペックな実装」をここで防ぎます。また、いきすぎたコードは保守時に必要かの判断を鈍らせバグの温床になりかねません。
DOMの直接操作の禁止
要素を操作する際、AIは平気で document.querySelector や getElementById を書こうとします。Vueのライフサイクルと競合してバグの温床になるため、DOMアクセスが必要な場合は必ず Template Refs (ref="xxx") を使用させます。
算出プロパティ(computed)内での副作用の禁止
Vueの公式ベストプラクティスに基づき、computed の中で非同期通信(API呼び出し)を行ったり、他の変数を書き換えたりする「副作用」を固く禁じます。computed は純粋な値の計算のみに専念させます。
むやみなリアクティビティ(ref / reactive)の乱用禁止
Vueのパフォーマンス最適化の観点から、「変更されない巨大なデータ(マスターデータや長いリスト)」に対して通常の ref を使うことを禁止します。レンダリングのオーバーヘッドを減らすため、監視が不要なデータには定数としてletやconstの使用も検討してください。
セキュリティリスク(v-html)の原則禁止
XSSの脆弱性を防ぐため、ユーザー入力を含むコンテンツを v-html で直接描画することを禁じます。バックエンドから返るHTMLを描画する場合は、必ずサニタイズ処理を挟むよう強制します。
次に、Step 2の作ったパーツが確実に組み合わさるよう、命名規則やファイル構成といった「ジョイントの規格」を統一します。
ファイル構成と役割(どこに何を書くか)
プロジェクト全体で一貫性を保ち、AIがファイルの置き場所で迷わないよう以下の構成とします。
src/
├── components/
│ ├── parts/ # 汎用UI部品(BaseButton, Badge等)
│ └── forms/ # フォーム関連(Input, Select等)
├── composables/ # 共通ロジック(useAuth, useFetch等)
├── utils/ # API通信・定数・純粋な関数
│ ├── api.ts # API通信ラッパー・共通エラーハンドリング
│ ├── colors.ts # カラーパレット定義
│ ├── const.ts # 定数定義
│ ├── utils.ts # ユーティリティ関数
│ └── validation-schemas.ts # バリデーションスキーマ
└── types/ # 共通のTypeScript型定義
命名規則と基本構文
ファイル名やクラス名を作成するときに以下の命名規則を使用してください。
- 定数: 全て大文字のスネークケース
(例:MAX_COUNT) - 関数: 始まりは動詞を用い、原則アロー関数で定義
(例:const getUserData = () => {}) - ファイル名: パスカルケース(PascalCase.vue)
基底コンポーネントにはプレフィックスに Base を付ける - CSSクラス(カスタム定義時): ケバブケース(kebab-case)とBEM命名を採用
状態を示すものは is- を付ける- BEM命名の基本: Block(.form-group等)、Element(.form-label等)、Modifier(.required等)で区別する。
型定義
type で定義し、共通のものは types/index.ts に配置する。
日付処理
日付を直接操作しようとする場合、いくつかの考慮が必要となります。
複雑な処理または、仕様上ライブラリの活用が難しい場合などを除いて、直接 new Date() の使用は避け、dayjs などを活用する。
CVAパターンの導入(クラス名長すぎ問題の解決)
Tailwind特有の「クラス名が横に長すぎて可読性が落ちる問題」を解決するため、複数のスタイルバリエーションを持つUI(ボタンやバッジなど)には、class-variance-authority (cva) などのパターンを使用し、クラス名を抽象化させます。
セマンティックなHTMLタグを使用する
div の乱用を避け、nav, main, article を積極的に使うことで、構造がわかりやすく可読性も向上します。
最後に、Step 3でどこまでをひとつのブロックとして扱うか、パーツ間の連携をどうするかという境界線を決めます。
コンポーネント化と共通化の基準
利用者から見て同じ概念
コンポーネント化 PrimaryButton, UserCard のように、名前をつけたときに自然に意味が伝わるものはコンポーネント化の対象とします。
変更理由が同じになりそう
共通化 (例)同じバリデーションルールが複数箇所にある場合(仕様変更が来たときに全部同時に直す必要があるため)。
新しいコンポーネントを新規作成・分割する基準
- 3回以上使用される見込みがある
- 独立した機能・UI部品である(例: Badge.vue のように単一責任)
- 他のコンポーネントで再利用可能である
ドメイン駆動設計(DDD)に基づく「コンテキスト」での分離
見た目が同じというだけでコンポーネントを共通化はしないこと。
例えば「商品購入ボタン」と「友達招待ボタン」は、コンテキスト(役割)が全く異なります。仕様変更で「購入ボタンにだけ在庫切れのローディングを出したい」となった時に、共通化していると複雑な if 文の塊(負債)になります。AIには「見た目ではなく、変更理由が同じものだけを共通化する」思想を守らせます。
アーキテクチャと連携のルール
イベントの伝達(親子間の連携)
コンポーネント自身の責務を小さく保つため、コンポーネント内の @click や @focus イベントは自身で処理せず、emit で上位(Controller層)に伝えて処理させます。
UIとロジックの分離(Composablesへの抽出)
コンポーネント(.vue)の中に、複雑なビジネスロジックやAPI通信の詳細を直接書くことを禁止します。UIに関係ないデータフェッチや状態管理は、必ず useUser() のような Composables(関数)として外部ファイルに切り出し、コンポーネントをクリーンに保ちます。
おわりに
ルール設計の考え方についてまとめました。
「ルール設計」と聞くと難しく思えますが、この3ステップから落とし込んで行くことでコードの「ブレ」を減らし、バラつきを軽減させることができます。
またこのガイドラインに従うことで、人間にとっても、実装を担当するAIエージェントにとっても、一貫性のある「プラモデルのような」開発が可能となってくるのではないでしょうか。
皆さんの現場でも、ぜひAIエージェントに向けた独自の「ルール設計」に挑戦してみてください!
参考
本記事の取り組みの際に以下記事を参考にさせていただきました。