📦

Vue 3×Clean Architectureで破綻しないSPA設計 ─ PiniaをUseCaseに据えた実践アーキテクチャ【第1回】

に公開

📖 Vue 3×Clean Architectureで破綻しないSPA設計 ─ PiniaをUseCaseに据えた実践アーキテクチャ【第1回】

📑 概要

Vue3によるClean Architectureを意識した、中〜大規模開発向けSPAアーキテクチャ設計思想と具体的な開発方針を解説。

🎯 対象読者

  • Vue3 における中〜大規模開発向けスタック・アーキテクチャーに興味のある方
  • Clean Architectureの考え方に理解のある方
  • Vue3 / Pinia / TypeScript 開発経験のある方
  • これからVue 3+型安全な設計に取り組みたい方

📌 はじめに

皆さん、Vue 3ってとても素敵なSPAフレームワークなのに「大規模開発には向いてない」と言われ、採用を見送った経験はありませんか?

確かに、Vueは自由度が高い反面、中〜大規模開発で設計破綻しやすいフレームワークでもあります。特にPiniaやComposable APIの登場で、責務の所在が曖昧になりコード肥大化・複雑化するケースも増えてきました。

そこで今回は、Vue 3 / Pinia / TypeScript を用いたSPA開発でClean Architectureを意識し、実務開発でも破綻しないアーキテクチャ設計の方針をまとめました。

連載形式で、型安全・責務分離を徹底したVue構成の実践パターンを解説していきますので、ぜひ最後までお付き合いください!


📌 なぜ Vue で Clean Architecture が必要なのか

VueはもともとView層中心の軽量・簡潔な設計思想ですが、以下のような状況では設計破綻しやすくなります。

  • 複数画面で同じユースケース(API呼び出し)を再利用したい
    → 例えばユーザー登録とプロフィール編集で同じAPI呼び出し処理を各コンポーネントに記述し、仕様変更時に複数箇所の修正が必要になる問題。

  • API仕様変更に対し、View層の修正影響を最小化したい
    → APIレスポンスのフィールド名変更で大量のVueコンポーネント修正が発生し、デグレの恐怖。

  • 多段階フォームの状態管理や画面遷移が複雑化
    → ブラウザバックや画面遷移でも状態を保持したいが、状態管理が複雑化し、どこに何があるか見失いがち。

  • 開発人数増加で責務の曖昧さが問題化
    → ロジックがあちこちに散らばり、どこに何を書くべきか判断が曖昧になる。

これらを防ぐには責務の明確な分離とレイヤ設計によるアプリケーション構造化が必須。
そこで参考になるのがClean Architectureの原則です。


📖 Clean Architectureの基本とVueへの適用

Clean Architectureは本来以下のようなレイヤ分割構造です。

  • Entity:企業のビジネスルール等
  • UseCase:アプリケーション固有のビジネスロジック
  • InterfaceAdapter:外部との接続・データ変換
  • Framework & Driver:Web, API, DB, ローカルストレージ等

上のレイヤほど変更が少なく、下のレイヤ変更の影響を受けない依存構造を維持します。

これをVueで実現する場合、構成要素と依存関係は以下のように整理できます。

📊 レイヤ構造図

※ VueのViewModelは双方向リアクティブの特性上、Controller / Presenterの責務分割が曖昧になりがちです。そこで本構成では、View Component+ComposableをまとめてInterfaceAdapter層として表現しています。
※図中の「Gateway→UseCase」は、実際にはUseCase(Pinia Store)がGatewayの抽象(インターフェース)に依存し、その具体実装をmain.tsで注入する構造です。依存性逆転(DIP)を実現しているものの、図上では便宜的にこのように表記しています。
※ 一番外側のFramework&Driverは、Vueアプリケーション全体とAPI通信といった外部インフラ層を表しています。

📖 レイヤと構成要素の対応

Clean Architecture Vueの構成要素
Entity TypeScriptロジック(ドメイン)
UseCase Pinia Store(ユースケース単位)
InterfaceAdapter View Component / Composable / Gateway
Framework&Driver Vue App / Vue Router / Axios等

📌 PiniaをUseCase層とする理由

PiniaをUseCase層とする理由と、その設計アプローチがもたらすメリットについて詳述します。

  1. Piniaライブラリの設計思想と、Clean Architecture のユースケース層との親和性
    PiniaをUseCase層として据えるアーキテクチャは、そもそもPiniaの設計思想との親和性があります。Pinia自身のアーキテクチャ原則を深く掘り下げると、「モジュール性」「リアクティビティ」「関心の分離」を重視していると言えます。また、Piniaのactionsは「ビジネスロジックと状態変更」を「サービス層パターン」内にカプセル化すると説明されます。この設計思想は、Clean Architectureにおけるユースケース層の役割によく合致します。
    この相乗効果によって、Vue3エコシステムに複雑なアーキテクチャパターンを採用する際に生じる「摩擦」を大きく和らげることが期待できます。厳密には Clean Architecture では UseCase層が状態を保持することは推奨されません。しかし、SPAアプリケーションの特性上、画面遷移や非同期処理の都合から、UseCase層内で状態を持つ必要性も実務上は存在します。本構成ではこの点を現実解として受け入れ、Pinia Storeに状態とユースケース実行の責務をまとめることで、実用性とアーキテクチャ原則のバランスを取っています。なお、この運用を行う場合でも、ユースケースの本質的な責務と状態管理の線引きを明確にし、責務の肥大化を避けることが重要です。
  2. システムシングルトンとしてのPiniaの有用性
    Piniaストアは、アプリケーション全体で単一のインスタンスとして振る舞う「システムシングルトン」としての特性を持ちます。この設計上の特長は、ユースケースとして定義されたビジネスロジック、およびそれが管理するアプリケーションの状態が、どのコンポーネントやコンポーザブルからアクセスされたとしても、常に一貫性を保つことを保証します。
    特に重要なのが、Piniaストアが画面のライフサイクルとは独立したアプリケーションスコープで状態を保持できる点です。これにより、コンポーネントがマウント・アンマウントを繰り返しても、Piniaストア内の状態は維持されます。
    さらに、この特性は非同期API実行中の画面遷移にも強く、処理結果の整合性を保つ上で非常に有効です。例えば、データ取得中にユーザーが別のページに移動してしまっても、Piniaストアで管理されている非同期処理はバックグラウンドで継続 されます。処理が完了した後、ユーザーが元のページに戻ってきた際に、その結果を適切に反映させることができます。これにより、ユーザー体験を損なうことなく、より堅牢なアプリケーションを構築できます。
    これは、ユーザー認証情報、アプリケーション設定、ショッピングカートの状態など、グローバルに共有されるビジネスロジックや状態を扱う際に極めて重要です。
    use()を呼び出すだけでユースケース(Piniaストア)にアクセスできるため、DIコンテナを別途用意することなく、アプリケーション内の依存関係を簡潔に解決しつつ、ビジネスロジックの実行と共有状態の操作を容易に行うことができます。
  3. main.tsにおけるGateway実装のコンストラクタDIが可能であること
    Pinia Store(ユースケース)内ではインターフェースのみを参照する構成とし、依存する具体的なGateway(APIクライアントやリポジトリなど、外部サービス連携を抽象化したもの)の具体実装は、Vueアプリの起動時(main.ts)でインスタンスを生成し、Piniaストア初期化時に手動でDIします。Vue 3にはAngularのようなDIコンテナ機能は存在しないため、この手法で依存性逆転(DIP)を実現します。
    このアプローチにより、ユースケース層は抽象(インターフェース)のみに依存し、具体的な実装はアプリケーションのルートで提供されるため、依存性逆転の原則(DIP)が厳密に遵守されます。この設計は、テスト時にモックのGateway実装への差し替えを容易にし、ユースケースの単体テストを簡素化します。さらに、開発、ステージング、本番といった異なる環境に応じて、異なるAPIエンドポイントやサービス実装を柔軟に切り替えることを可能にします。
  4. Vue標準フレームワークへの組み込みと高い習熟度
    PiniaはVue 3の公式状態管理ライブラリであり、Vue開発者にとって非常に馴染み深いツールです。多くの開発者が既にPiniaの概念、API、そしてベストプラクティスに習熟しているため、新たなアーキテクチャパターンにPiniaをUseCaseとして導入する際の学習コストが大幅に軽減されます。これにより、チーム全体の開発効率が向上し、新規メンバーのオンボーディングもスムーズに進みます。また、豊富なドキュメント、チュートリアル、活発なコミュニティサポート、そして強力な開発ツール(DevTools)が利用可能であり、Vueのリアクティビティシステムとシームレスに統合されているため、Vueの他の機能との連携も自然に行えます。
  5. PiniaプラグインによるAOP的な実装が標準で可能であること
    Piniaプラグインは、ストアのstate初期化時やaction呼び出し時にフックし、その振る舞いを拡張する仕組みを提供します。これにより、ロギング・エラー通知などの横断的関心事(Cross-cutting Concerns) をユースケース(Piniaストア)の核となるビジネスロジックから分離して、アスペクト指向プログラミング(AOP)的に適用できます。
    このようなアプローチはユースケースの純粋性を保ちつつ、アプリケーション全体で一貫した非機能要件を適用することを可能にします。プラグインによって横断的関心事を外部化することで、Piniaストア(ユースケース)は「純粋なビジネスロジックの実行」という主要な責務に集中でき、ストアのコードがよりクリーンで読みやすくなり、保守性が向上します。

実務開発において、Pinia Storeをユースケース単位で設計・運用することで、Clean Architecture の考え方をVueエコシステムに組み込む際の「摩擦」を大幅に減少させることができるのと共に、ユースケースを「純粋なビジネスロジック」責務に専念させる責務の明確化と保守性の高い開発が実現できます。


📌 採用する技術スタックと特徴

  • Vue 3:Composition API前提の最新環境
  • Vue-Router:画面遷移制御
  • Pinia:状態管理(UseCase層として運用)
  • Zod:型安全なバリデーション&型推論
  • Axios:HTTP通信
  • OpenAPI:API仕様定義・型生成
  • openapi-zod-client:OpenAPIからZodスキーマ自動生成

これにより、API通信〜型検証〜ユースケース管理まで型安全かつ保守性の高い構成を目指します。


📊 全体レイヤー構成と役割

[ Presentation層 ]
├── Viewコンポーネント
├── UI Composable
├── Application Composable

[ Application層 (UseCase) ]
├── Pinia Store

[ Infrastructure層 ]
├── API Gateway
├── Mapper
├── DTO (Zod)

[ Domain層 ]
├── Entity
├── UseCaseインターフェース

Application Composableは、ユースケース(Pinia Store)の呼び出しと、画面間状態の橋渡し・ユースケースの実行制御を担う重要レイヤーです。ここでは、ユースケース層とView層の間のアプリケーション固有の処理や、ユースケース横断のアプリケーション状態を一元管理します。責務を明確化することで、アプリ全体の構造が整理され、開発効率と保守性が向上します。


📌 中〜大規模Vue開発での設計方針まとめ

  • ユースケース実行は必ずApplication Composable経由で行い、責務と依存関係を明確化する
  • API通信はすべてGateway層に集約。View層からaxios直接呼び出し禁止
  • DTOとドメイン型は分離し、Zodで型検証を行う
  • 複数画面で同一ユースケース利用時はApplication Composableで再利用

これにより、実運用・複数人開発を想定しても破綻しにくい型安全・保守性の高い構造が維持できます。


📖 次回予告

次回は
「Composableの責務整理と設計ベストプラクティス」 をテーマに、Composableの分類と役割分担および実装例を解説する予定です。

実はここがVue 3開発における設計破綻の最大の分かれ道。
ここを押さえれば、開発効率と保守性が劇的に改善しますので、ぜひご期待ください!


📚 おわりに

今回の記事がVue×型安全×アーキテクチャ設計で悩んでいる方の参考になれば嬉しいです。
この設計方針を採用することで、開発効率と保守性を両立した、破綻しないSPA開発が可能になります。

ご意見・コメントもぜひお寄せください!
次回もよろしくお願いします。

Discussion