【その①】生成AI時代のフロントエンドにおけるアーキテクチャの最適解
はじめに
先日、個人的に参加したエンジニア飲み会で「世の中のフロントエンドエンジニアのレベルが低すぎる」という LT をしたところ、かなり反響がありました。
この記事では、そのとき話した内容をもとに、自分の中の考えを整理してまとめます。
生成 AIという名前に釣られたあなた、この記事は生成 AI での開発に直接は関係ありません。
ただし、あなたのフロントエンドエンジニアとしてのレベルが一段階上がることを保証します。
対象読者
フロントエンドで強強になりたい人
今開発しているアプリがぐっちゃぐちゃのスパゲティコードの人
会社独自のデザインに変換中である人
なんかおんなじよーなコンポーネントが 10 個くらいある人
なぜ今、この技術が重要なのか?
生成 AI 時代の今、無限にコードを生成できるようになりました。
ただ、後述する LT でも言いましたがフロントエンドの生成品質だけ他の言語やフレームワークに比べて圧倒的に低いです。
それはつまり世界のほとんど全てのフロントエンドエンジニアのレベルが低いということ、、、であればそんなレベルの低いフロントエンドエンジニアをごぼう抜きしよう!!っていうのが今回の記事の目的です。
本題
前述した LT で以下のように発表をしました。
「フロントエンド、、難しくね?」→ 「じゃあどうすればええねん」をもっと深掘りしていこうと思います。
生成 AI 時代におけるフロントエンドにおけるアーキテクチャの最適解シリーズの記事では以下の構成を想定しており、今回はコンポーネントのアーキテクチャについて解説をします。
- 1. コンポーネント
- 2. ページ
- 3. デザインシステム
- 4. Provider Context
- 5. features
- 6. その他・全体構成
(この記事だけで全部書きたいのですが、すっごい分量になってしまうのでいくつか分けさせていただきたい、、🙇♂️)
アーキテクチャとは
いろいろ人によって解釈があると思いますが、僕の中では、IO と依存関係が定まっており、各パーツがいつでも交換可能に設計されている状況をアーキテクチャが整理されていると表現しています。
このブログでは、IO と依存関係が定まっており、各パーツがいつでも交換可能な構造がアーキテクチャであると定義します。
コンポーネントにおけるアーキテクチャ
ここでは、コンポーネントにおけるよくある問題点をあげていき、今回提案するアーキテクチャがその問題点を明確にクリアできる、メリットがありますよということを解説できればと思います。
あるあるなコンポーネントの問題点
-
- コンポーネントがモンスター化 👹
-
- コピペ祭り ✂️✂️✂️
-
- props 地獄 🧵
-
- デザインがバラバラ 🎨
-
- テストがしんどい 😵💫
-
- 修正したら別のところが壊れる 💥
「でかい・重い・ぐちゃぐちゃ」 の三拍子が揃って、完全に新しく作成しなおすか……という選択肢を取られがちです。
こういうプロジェクトに当たったとき、地獄ですよねぇ……わかります(😇)
今回提案するアーキテクチャ
上記を解決するために、命名規則を用いた DI パターンと Props を唯一のインタフェースとするコンポーネントシステム を提案します。
以下具体例と、ユースケースも含めて利点を紹介していきます。
アーキテクチャ全体像
tags/
├── index.ts
└── textTag
├── customV1.css
├── customV1TextTag.stories.tsx
├── customV1TextTag.tsx # 重要
└── index.ts # 重要
index.ts
export type TextTagProps = {
text: string;
variant?: "primary" | "secondary" | "third";
};
export { CustomV1TextTag as TextTag } from "./customV1TextTag";
customV1TextTag.tsx
import type { TextTagProps } from ".";
import "./customV1.css";
export const CustomV1TextTag = ({
text,
variant = "primary",
}: TextTagProps) => {
return (
<div
className={`custom-v1-text-tag-container custom-v1-text-tag-${variant}`}
>
<span className="custom-v1-text-tag-content">{text}</span>
</div>
);
};
(CSS と Story は割愛)
解説
今回意識すべき点は 3 点です。
- index.ts で Props を定義すること
- コンポーネントやファイルの命名方法
- コンポーネント名の変換
です。
index.ts で Props を定義すること
まず、なぜ「index.ts で Props を定義するべきなのか」についてですが、バックエンドやアーキテクチャを齧っていてインタフェース分離と依存性の逆転がわかる人は飛ばして、次の解説に行ってください。
大前提として、Props を唯一のインタフェースとするということが念頭にあれば、ここはさらっと理解できるはずです。
よくある初学者のコード
import "./customV1.css";
export type TextTagProps = {
text: string;
variant?: "primary" | "secondary" | "third";
};
export const CustomV1TextTag = ({
text,
variant = "primary",
}: TextTagProps) => {
return (
<div
className={`custom-v1-text-tag-container custom-v1-text-tag-${variant}`}
>
<span className="custom-v1-text-tag-content">{text}</span>
</div>
);
};
こういう書き方をすると、「コンポーネントがモンスター化 👹」する原因になります。
TextTagProps を使用した違うデザインのコンポーネントを作ろうと思った時、TextTagProps をコピペして別ファイルにコンポーネントを作ります。
→2 年経つと、TextTagProps を変更しようとすると 20 ファイル編集しなきゃいけない、気がついたら TextTagProps & SomeType を使って変に拡張されたコンポーネントまである……地獄の出来上がりです。
どの言語にも言えることですが、ベストプラクティスを参考にして、実装とインタフェースは必ず別のファイルで定義をすることにしましょう。(すごい簡略化しました。もっと詳しく知りたい場合はこちらを参考に)
コンポーネントやファイルの命名方法
よくある初学者のファイル・ディレクトリ・コンポーネント名を見てみましょう。
// default.tsx
export const Tag = ({
text,
variant = "primary",
}: TextTagProps) => {...}
// default1.tsx
export const DefaultTag1 = ({
text,
variant = "primary",
}: TextTagProps) => {...}
なぜダメなのか?
-
ファイル名が意味不明:
default.tsxとかdefault1.tsxって、何が違うのかわからない -
コンポーネント名が汎用的すぎる:
TagとかDefaultTag1って、用途が全くわからない -
拡張性ゼロ:新しいデザインの Tag を作ろうと思ったら、また
default2.tsxを作るハメに...
実際に自分が遭遇したプロジェクトでは、Button.tsx、Button1.tsx、Button2.tsx、NewButton.tsx、UpdatedButton.tsxみたいなファイルが 20 個以上あって、どれが最新版なのか誰もわからない状態でした。
個人的にベストプラクティスだと思っている命名規則は、{デザインパターン}{コンポーネント}{With 差分}.tsx です。
- ChakraButtonWithIcon.tsx
- MuiCardWithLink.tsx
このように命名して、ButtonWithIcon ディレクトリや、CardWithLink ディレクトリにまとめておくことにより、外部(コンポーネントを使う側)からは ButtonWithIcon の塊なんだな、Icon があるんだなということが分かりやすくなります。
コンポーネント名の変換
ここが今回の記事の肝の部分です。
実は、この部分でDI(依存性注入)パターンを使っているんです。
export type TextTagProps = {
text: string;
variant?: "primary" | "secondary" | "third";
};
// export { CustomV1TextTag as TextTag } from "./customV1TextTag";
// export { CustomV2TextTag as TextTag } from "./customV2TextTag";
export { MuiTextTag as TextTag } from "./muiTextTag";
// export { ChakraTextTag as TextTag } from "./chakraTextTag"
コメントアウトしている部分をよくみてください。
これで、TextTagという名前で統一されたインタフェースを使いながら、実装を簡単に切り替えることが可能です。
実際の使用例:
// どこからでも同じように使える
import { TextTag } from "@/components/tags";
const MyComponent = () => {
return (
<div>
<TextTag text="重要" variant="primary" />
<TextTag text="注意" variant="secondary" />
</div>
);
};
切り替えのメリット:
-
開発中:
CustomV1TextTagでプロトタイプを作る -
本番:
MuiTextTagで Material-UI のデザインに統一 -
デザインシステム変更:
ChakraTextTagに 1 行で切り替え - A/B テスト:同じコンポーネント名で異なる実装をテスト
さらに、この仕組みのメリット:
-
型安全性:
TextTagPropsが変わったら、全実装でエラーが出る - テスト容易性:モック実装を簡単に作れる
- 保守性:実装を変更しても、使用側のコードは一切変更不要
// テスト用のモック実装
export const MockTextTag = ({ text }: TextTagProps) => (
<div data-testid="text-tag">{text}</div>
);
// テスト時だけこれに切り替える
// export { MockTextTag as TextTag } from "./mockTextTag";
こんな感じで、1 行コメントアウトするだけで実装を変更できるのです。
まとめ
今までたくさんのエンジニアに会ってきましたが、フロントエンドエンジニアでアーキテクチャを意識している方はほんとに稀です。
バックエンドはめっちゃ意識しているのに、フロントエンドでは全く意識していないひとも多いです。
僕の今回の記事で、フロントエンドでのアーキテクチャを意識していただければと思います。
次はその ② でページ周りのアーキテクチャを詳しく解説していきます。
読んでいただきありがとうございました。
付録
参考記事
- 依存性の逆転のいちばんわかりやすい説明 2025/10/15 access
- Next.js/App Router を CleanArchitecture 風に構築してみた 2025/10/15 access
🤖 本記事における AI 活用レポート
この記事は、エンジニアの知見を核としながら、生産性向上のために生成 AI を積極的に活用しています。
-
活用目的:
- アイデアの壁打ち・構成案のブレインストーミング
- 専門用語の平易な表現への言い換え
- サンプルコードの生成・リファクタリング
- 記事全体の誤字脱字チェック
- 記事タイトル案の複数提示
-
使用ツール:
- ChatGPT gpt-5
- cursor cheetah
プロンプトハイライト
アーキテクチャ設計がされていないフロントエンドによくあるコンポーネント周りに発生する問題点をあげて
執筆者のコメント
今回は、推敲と文体調整、あるあるゴミコードの生成に使いました。
💡 コードからクリニックへ:この記事の技術がもたらした価値
この記事で解説した「【その ①】生成 AI 時代のフロントエンドにおけるアーキテクチャの最適解」は、以下の価値を生み出しました。
-
To ビジネス 📈
-
提供価値:
- 保守運用性を高め、フロントエンドの開発速度を上昇させ顧客への PDCA 体験の向上をした。
-
インパクト:
- なし
-
提供価値:
📊 エンジニア・スコアカード:今回のソリューション自己評価
この記事で解説した「【その ①】生成 AI 時代のフロントエンドにおけるアーキテクチャの最適解」について、担当エンジニアが 5 段階で自己評価しました。
- パフォーマンス: ★★★★★
- 保守性: ★★★★★
- テスト容易性: ★★★★★
- コスト効率: ★★★☆☆
- 革新性: ★★★★☆
担当エンジニアの評価理由
LT での反響と、どんなフレームワークでも適応できるアーキテクチャのため。
Discussion