この記事はSchoo Advent Calendar2025の15日目の記事です。
はじめに
Vue.js/Nuxt を用いて、複雑なロジックを持つ画面を開発したときの話です。
今回、デザインが事前に用意されていたため、 着手しやすい子コンポーネント単位で並行開発を進めるという、効率化を考えたアプローチを試みました。
しかし、最後の統合フェーズで 「つなぎ込み地獄」 に直面し、結果として1人のエンジニアに作業が集中してしまいました。
「統合時の調整コストはある程度仕方ない」当初はそう考えていましたが、振り返ってみると、これは進め方のアプローチで未然に防げた問題ではないかと考えています。
具体的には、 以下の2つを組み合わせたアプローチです。
- 「トップダウン実装」
- 「インターフェース先行設計」
本記事では、私たちのチームが直面した「統合フェーズの肥大化」という課題を整理し、どうすればスムーズに並行開発を進められたのか、その解決策を考察します。
対象読者
- Vue.js/Nuxt でチーム開発を行っているフロントエンドエンジニア
- コンポーネント並行開発での統合に課題を感じている方
- 複雑な状態管理の設計に悩んでいる方
実装したプロジェクトの概要
以下のような特徴を持つ画面を実装しました:
実装における難所
-
複雑な状態依存: ユーザーの操作履歴や選択状態によって、UIの構成や遷移先が動的に変化する。
-
多岐にわたる分岐フロー: 単一本道ではなく、条件によって複数の処理パターン(リカバリー処理や中断フローなど)が存在する。
技術スタック
- Nuxt 3 + TypeScript
- Composition API
- 状態管理: Composables(ページをまたぐ情報がないため Pinia Store は不使用の方針)
- GraphQL
プロジェクト開発体制
- フロントエンドエンジニアメンバー: 2-3名(全体:4~5名)
- デザイン:Figmaで用意済み
- 作業分担:コンポーネント単位(用意されているデザインベースにUIから着手)
直面した課題
課題1: 統合フェーズでの作業集中
開発の流れ(実際のプロセス):
- 初期設計(根幹データのみ確定)
- 根幹となるデータ設計は行った。
- しかし、コンポーネント間で持ち回る詳細なインターフェース仕様は未確定。
- コンポーネント単位で作業分担
- Props/Emitsの仕様決めは実装時まで先送り。
- 各自が個別に開発・テスト
- 親から渡ってくるイベントはmock実装。
- コンポーネント単体では完成している状態。
- 統合作業(最終段階)
- 親コンポーネントでのつなぎ込み。
- データフローの調整。
- 動的処理の実装。
→ 結果:1人に作業が集中し、並行開発のメリットが消失
問題点:
- 最後のつなぎ込み作業は分担が難しい
- 統合してみると「想定外」の調整が多発
課題2: つなぎ込みで特に工数がかかった内容
2-1. フロントエンドでのデータの持ち回り処理
複雑だった点:
- 複数の状態管理層が親コンポーネントで相互に依存
- コンテキスト情報(ユーザー入力値やステップの進行状況など)によって表示ロジックが複雑に分岐
- データの同期タイミングが複雑 (入力データとUIの紐付け、API呼び出し後のID更新など)
2-2. 動的処理を後回しにしたこと
問題の構造:
- 並行開発フェーズ:
- 静的なコンポーネント(表示・スタイル)を先に作成
- 統合フェーズ:
- 親コンポーネントでの状態管理、イベントハンドリング、エラー処理などを実装
結果:
統合フェーズで複雑なロジックを一気に実装することになった。
課題の根本原因
振り返りを通じて、以下の2つが根本原因だと分析しました:
原因1: ロジックの実装と統合を最終フェーズに先送り
何が欠けていたか:
UIパーツ(子コンポーネント)の開発を優先し、それらを束ねて動かす親コンポーネントのロジック実装と、コンポーネント間のつなぎ込みを、実装の最終段階に集中させてしまいました。
なぜ問題になったか:
親コンポーネントのロジックが未完成である間は、個別に完成した子コンポーネントの挙動確認や結合テストを進めることができませんでした。結果として、すべての問題解決と修正作業が最終フェーズに一気に集約し、スケジュールのボトルネックとなってしまいました。
原因2: 厳密なインターフェース定義を後回しにしたことによる調整コストの増大
何が欠けていたか:
親コンポーネントのロジックが未着手だったため、「通信制御」や「エラー分岐」といった動的な連携に必要な Props や Emits の厳密な仕様定義を、事実上後回しにしてしまいました。
なぜ問題になったか:
動的なロジックを組み込む際に、子コンポーネント側に必要な「制御フラグ」や「イベント引数」の微細な仕様追加・変更が、想定を遥かに上回る量で発生しました。この積み重なった手戻りが、統合時の負荷を増大させる一因となりました。
改善アプローチ:2つの手法の組み合わせ
今回の課題を解決するためには、以下の手法を組み合わせるべきでした。
採用すべきだった手法
| 手法 | 概要 | 今回のプロジェクトへの適用 |
|---|---|---|
| トップダウン実装 | 上位レベルから段階的に開発 | 子の実装を待たず、親の骨格(ロジック)とモックを先に作る |
| インターフェース先行 | 実装の前にインターフェースを定義する | 親子間の Props/Emits の型定義 を最初に確定させる |
この手法を使っていればどう改善できたか
改善1: トップダウン実装でデータフローと全体像を共有
課題:
今回のボトムアップ実装では、親コンポーネントのUI配置モックは作成していましたが、ロジックや状態管理の実装を後回しにしたことで、統合時に全体像が見えず、作業が集中してしまいました。
改善策: 「ロジックを担保する親の骨格」を先に作る
このトップダウンアプローチは、コンポーネントの役割分離の思想を開発プロセスの初期から厳密に適用します。開発の初期段階で、単なるUIのモックではなく、データフローと状態遷移を制御するロジックの骨格を親コンポーネントに実装しきります。
実装レベルの目安
親コンポーネントでは、すべての主要な状態と、子コンポーネントから Emit されるすべてのイベントに対するハンドラ関数を定義します。非同期処理は setTimeout などでモック化し、状態遷移ロジック(ローディングの表示/非表示、次のステップへの移行など)は完全に記述します。
改善効果:
- ✅ データフローの早期確定: 開発の最も早い段階でロジックの骨格が完成し、データがどこから来て、どこへ流れるかが明確になります。
- ✅ 全体像の共有: 並行開発中も「動く骨格」を通じて全体像が見えるため、メンバー間の認識ズレが発生しません。
改善2: 型定義でイベント仕様の認識齟齬を予防 (Interface-First)
改善策: 各コンポーネントにおけるPropsとEmitsを先に定義する
設計フェーズで、各コンポーネントが「何を受け取り(Props)」、「何を親に伝えるか(Emits)」を先に定義し、チーム内で合意形成を行います。特にイベントは名前や引数がズレやすいため、厳密に定義します。
// 子コンポーネントの実装イメージ
<script setup lang="ts">
// 1. 親から受け取るデータの型定義
interface PresentationProps {
displayData: { id: number; text: string };
isLoading: boolean;
}
const props = defineProps<PresentationProps>();
// 2. 親に通知するイベントの型定義(ここが重要)
// イベント名と、引数(payload)の型を厳密に決める
interface PresentationEmits {
(e: 'submit-data', value: string): void; // データ送信
(e: 'retry', id: number): void; // リトライ
(e: 'cancel'): void; // キャンセル
}
const emits = defineEmits<PresentationEmits>();
</script>
改善効果:
- ✅ イベント仕様の強制:
submit-dataという名前でstringを渡さないと型エラーになるため、実装のブレを防げます。 - ✅ 親の実装がスムーズ: 親側は定義されたイベント名に合わせてハンドラを書くだけなので、統合時の「イベント名が違う」「引数が足りない」といったトラブルがゼロになります。
開発フローの比較まとめ
| フェーズ | 従来のフロー(ボトムアップ) | 改善フロー(トップダウン+インターフェース先行) |
|---|---|---|
| 設計 | 主要なデータ設計 詳細仕様/インターフェースは未確定 |
インターフェース先行設計 型定義ファイルの作成 |
| 開発序盤 | コンポーネント分担を決定 | 親の骨格(データフロー)実装 |
| 開発中盤 | 実装とデータ仕様の検討を並行 個別コンポーネントを完成させる |
子コンポーネント実装 (型定義に基づき並行開発) |
| 終盤 | ロジックとUIを同時統合し1人に集中 調整・手戻り多発 |
統合コストは既に吸収 全体の通しテストが中心 |
まとめ: 次回へのアクションプラン
複雑なステップ動作を伴うUIの実装を通じて、 「コンポーネントを作る前に、つなぎ方を設計する」 ことの重要性を痛感しました。
次回からは以下のフローを考慮し、開発効率と品質の向上を目指します。
1. 設計フェーズで作成するもの
- 型定義ファイル(TypeScript Interface / Types)
- 親コンポーネントの骨格(モックで動作確認できるもの)
2. 型定義に含めるべき項目
- Props定義: 名前、型、必須/任意
- Emits定義: イベント名、渡される引数(Payload)の型
最後に:振り返りと今後の展望
もちろん、トップダウン実装にも 「親の骨格が完成するまで、子コンポーネントの並行開発が本格化しにくい」 といった課題はあり、すべてのプロジェクトに適用できる万能な解決策では無いと思います。
しかし、今回のようなデータ操作のロジックが複雑に入り組むUI開発においては、序盤のスピードを多少犠牲にしてでも、インターフェース先行設計とトップダウン実装で統合時のリスクを最小化することが、結果として最も有効なアプローチの一つだと、振り返りを通じて感じています。
今回の経験が、同様の課題に取り組む方々の参考となれば幸いです。
参考資料
- TypeScript Handbook - Interfaces
- Contract-First API Development
- Vue.js 公式 - コンポーネントの基礎
- TypeScript 公式 - Interfaces
Schooでは一緒に働く仲間を募集しています!
