メインコンテンツまでスキップ

大規模開発のアーキテクチャパターン

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層構造で設計できます。

レイヤー責務実装例
PresentationUIの描画・ユーザー入力の受け取り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では最初からベースラインが揃っています。