大規模開発のアーキテクチャパターン
Angularが大規模開発に向いている最大の理由は、フレームワーク自体が厳格なアーキテクチャを推奨していることです。このドキュメントでは、エンタープライズレベルでのAngularアーキテクチャパターンを解説します。
1. Feature-based ディレクトリ構成
大規模Angularアプリケーションで推奨されるのが、機能(feature)単位でディレクトリを分割する構成です。
src/
├── app/
│ ├── core/ # シングルトンサービス、ガード、インターセプター
│ │ ├── guards/
│ │ │ └── auth.guard.ts
│ │ ├── interceptors/
│ │ │ └── auth.interceptor.ts
│ │ └── services/
│ │ └── auth.service.ts
│ ├── shared/ # 共有コンポーネント・ディレクティブ・パイプ
│ │ ├── components/
│ │ │ ├── button/
│ │ │ └── modal/
│ │ ├── directives/
│ │ └── pipes/
│ ├── features/ # 機能ごとのモジュール
│ │ ├── dashboard/
│ │ │ ├── components/ # Presentationalコンポーネント
│ │ │ ├── pages/ # Smartコンポーネント(ページ)
│ │ │ ├── services/ # 機能固有のサービス
│ │ │ ├── models/ # 型定義・インターフェース
│ │ │ └── index.ts # Barrel export
│ │ └── user/
│ │ ├── components/
│ │ ├── pages/
│ │ ├── services/
│ │ ├── models/
│ │ └── index.ts
│ └── app.routes.ts # ルート定義
各ディレクトリの役割
| ディレクトリ | 役割 | 例 |
|---|---|---|
core/ | アプリ全体で1つだけ存在するシングルトンサービス・ガード | AuthService, ApiInterceptor |
shared/ | 複数のfeatureで再利用するUIコンポーネント・ユーティリティ | ButtonComponent, DatePipe |
features/ | ビジネス機能ごとの独立したモジュール | DashboardModule, UserModule |
Next.js との比較
Next.jsのapp/ディレクトリはファイルシステム = ルーティングですが、Angularはルーティングとディレクトリ構成を分離できます。これにより、ページ遷移とは独立したビジネスロジックの整理が可能になります。
2. Smart/Dumb コンポーネントパターン
コンポーネントを「ロジックを持つ」「表示のみ」に分類する設計パターンです。
Smart(Container)コンポーネント
データ取得・状態管理のロジックを担当します。サービスに依存し、子コンポーネントへデータをInputで渡します。
// features/user/pages/user-list.component.ts
@Component({
selector: 'app-user-list-page',
template: `
<app-user-list
[users]="users()"
[loading]="loading()"
(userSelected)="onUserSelected($event)"
/>
`,
})
export class UserListPageComponent {
private userFacade = inject(UserFacade);
users = this.userFacade.users; // Signal
loading = this.userFacade.isLoading; // Signal
onUserSelected(userId: string) {
this.userFacade.selectUser(userId);
}
}
Dumb(Presentational)コンポーネント
@Input / @Output のみで動作する純粋な表示コンポーネントです。サービスに依存しません。
// features/user/components/user-list.component.ts
@Component({
selector: 'app-user-list',
template: `
@if (loading()) {
<app-spinner />
} @else {
<ul>
@for (user of users(); track user.id) {
<li (click)="userSelected.emit(user.id)">{{ user.name }}</li>
}
</ul>
}
`,
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class UserListComponent {
users = input.required<User[]>();
loading = input<boolean>(false);
userSelected = output<string>();
}
分離のメリット
| 観点 | メリット |
|---|---|
| テスト容易性 | Dumbコンポーネントはサービスのモックが不要でテストが簡単 |
| 再利用性 | Dumbコンポーネントはどのページでも使い回せる |
| 関心の分離 | ロジックと表示が分離されてコードが読みやすい |
| パフォーマンス | OnPush戦略を適用しやすく変更検知を最小化できる |
3. Facade パターン
Facadeパターンは、コンポーネントと複雑なロジック(Store・API・ビジネスルール)の間に中間層を設けるパターンです。Angularの強力なDIシステムを活かして特に効果を発揮します。
// features/user/services/user.facade.ts
@Injectable({ providedIn: 'root' })
export class UserFacade {
private store = inject(Store);
private apiService = inject(UserApiService);
// コンポーネントに公開するSignals
users = toSignal(this.store.select(selectAllUsers), { initialValue: [] });
isLoading = toSignal(this.store.select(selectUsersLoading), { initialValue: false });
selectedUser = toSignal(this.store.select(selectSelectedUser));
loadUsers() {
this.store.dispatch(UserActions.loadUsers());
}
selectUser(userId: string) {
this.store.dispatch(UserActions.selectUser({ userId }));
}
updateUser(user: User) {
this.store.dispatch(UserActions.updateUser({ user }));
}
}
コンポーネントはFacadeのみに依存し、Storeの実装詳細を知る必要がなくなります。後でStoreをSignalベースに移行しても、コンポーネントの修正は不要です。
4. レイヤードアーキテクチャ
Angularアプリケーションは、サーバーサイドのクリーンアーキテクチャと同様に4層構造で設計できます。
| レイヤー | 責務 | 実装例 |
|---|---|---|
| Presentation | UIの描画・ユーザー入力の受け取り | UserListComponent, LoginPageComponent |
| Application | ユースケースの調整、状態管理、Facadeの実装 | UserFacade, AuthFacade |
| Domain | ビジネスルール・ドメインオブジェクト・インターフェース | User, Order, IUserRepository |
| Infrastructure | 外部システムとの通信・データ永続化 | UserApiService, LocalStorageService |
依存方向のルール
- 依存の方向は外側 → 内側のみ(Presentation → Domain方向)
- DomainレイヤーはAngularやHTTPなどのフレームワークに依存しない
- InfrastructureレイヤーはDomainのインターフェースを実装するが、Domainに依存される
5. Barrel Exports(index.ts)
各featureディレクトリにindex.tsを置き、公開APIを一元管理します。
// features/user/index.ts
export { UserListPageComponent } from './pages/user-list.component';
export { UserFacade } from './services/user.facade';
export type { User, UserRole } from './models/user.model';
// 内部実装(UserApiService など)は export しない
インポート側は詳細なパスを知らずに済みます。
// Before: 詳細なパスを知る必要がある
import { UserFacade } from '../features/user/services/user.facade';
import type { User } from '../features/user/models/user.model';
// After: featureの公開APIのみを参照
import { UserFacade } from '../features/user';
import type { User } from '../features/user';
tsconfig.jsonのパスエイリアスとあわせて使うと、さらに簡潔になります。
// tsconfig.json
{
"compilerOptions": {
"paths": {
"@features/user": ["src/app/features/user/index.ts"],
"@features/dashboard": ["src/app/features/dashboard/index.ts"],
"@shared/*": ["src/app/shared/*"]
}
}
}
6. 状態管理の設計指針
Angularアプリの状態は4種類に分類し、それぞれ適切な管理手段を選びます。
| 状態の種類 | 説明 | 管理手段 |
|---|---|---|
| ローカル状態 | コンポーネント内のみで使用(フォームの入力値、モーダルの開閉など) | signal(), BehaviorSubject |
| 共有状態(Feature内) | 同じfeature内の複数コンポーネントで共有 | Featureレベルのサービス + Signals |
| グローバル状態 | アプリ全体で共有(ログインユーザー情報、テーマなど) | NgRx Store / NgRx SignalStore |
| サーバー状態 | APIから取得したリモートデータ | NgRx Effects + Cache, または RxJS |
// ローカル状態(コンポーネント内)
export class CounterComponent {
count = signal(0);
increment() { this.count.update(v => v + 1); }
}
// Feature状態(サービスで共有)
@Injectable({ providedIn: 'root' })
export class CartService {
private items = signal<CartItem[]>([]);
readonly items$ = this.items.asReadonly();
readonly total = computed(() =>
this.items().reduce((sum, item) => sum + item.price * item.quantity, 0)
);
}
// グローバル状態(NgRx SignalStore)
export const UserStore = signalStore(
{ providedIn: 'root' },
withState({ currentUser: null as User | null, isLoggedIn: false }),
withMethods((store, authService = inject(AuthService)) => ({
async login(credentials: Credentials) {
const user = await authService.login(credentials);
patchState(store, { currentUser: user, isLoggedIn: true });
},
}))
);
React との比較
ReactはuseState/Context/Redux/TanStack Queryなど選択肢が多く、チームごとに構成が異なりがちです。AngularはDIとサービスの仕組みが最初から統一されているため、状態管理の方針も合わせやすいです。
7. マイクロフロントエンド
Module Federation(Webpack 5)を使うと、Angularアプリを複数の独立したマイクロフロントエンドに分割できます。
ng add @angular-architects/module-federation
構成例
shell-app/ # ホスト(シェル)アプリ
webpack.config.js
mfe-dashboard/ # リモートアプリ(ダッシュボードチーム)
webpack.config.js
mfe-orders/ # リモートアプリ(注文管理チーム)
webpack.config.js
// shell-app/src/app/app.routes.ts
export const routes: Routes = [
{
path: 'dashboard',
loadChildren: () =>
loadRemoteModule({
type: 'module',
remoteEntry: 'http://localhost:4201/remoteEntry.js',
exposedModule: './DashboardModule',
}).then(m => m.DashboardModule),
},
];
メリットと注意点
| 観点 | 内容 |
|---|---|
| メリット | チームが独立してデプロイ可能、技術スタック(バージョン)の独立性 |
| 注意点 | 共有依存(@angular/coreなど)のバージョン管理が重要 |
| 適したケース | 複数チームが独立して開発・デプロイする大規模組織 |
8. コーディング規約と一貫性の確保
Angular Style Guide
Angular公式スタイルガイドに従うことで、チーム間でのコードの一貫性が保たれます。主なルール:
- 命名規則: コンポーネントは
feature-name.component.ts、サービスはfeature-name.service.ts - 単一責任の原則: 1ファイル1クラス、400行以内を目安に
- ファイル構成: 装飾子の順序(
@Input→@Output→ ライフサイクルフック → パブリックメソッド → プライベートメソッド)
ESLint の設定
ng add @angular-eslint/schematics
// .eslintrc.json(Angularルールの例)
{
"rules": {
"@angular-eslint/component-selector": [
"error",
{ "type": "element", "prefix": "app", "style": "kebab-case" }
],
"@angular-eslint/directive-selector": [
"error",
{ "type": "attribute", "prefix": "app", "style": "camelCase" }
],
"@angular-eslint/no-empty-lifecycle-method": "error",
"@angular-eslint/use-lifecycle-interface": "error"
}
}
Prettier との統合
npm install --save-dev prettier eslint-config-prettier
// .prettierrc
{
"singleQuote": true,
"trailingComma": "all",
"printWidth": 100
}
Angularのアーキテクチャが持つ優位性
Angularは**Opinionated(主張のあるフレームワーク)**であるため、チームに参加した開発者が「どう書けばいいか」で迷う時間が少なくなります。React/Next.jsのエコシステムでは、状態管理・スタイリング・ルーティングなどを都度チームで決定する必要がありますが、Angularでは最初からベースラインが揃っています。