Angular入門 - 10年以上の歴史を持つエンタープライズ御用達フレームワーク
はじめに
Angularは、Googleが開発・メンテナンスしているTypeScriptベースのフロントエンドフレームワークです。2016年にAngularJS(Angular 1.x)から完全に書き直されて登場し、現在はバージョン21.0.3(2025年12月3日リリース)まで進化しています。
正直なところ、React全盛の今、「Angularってまだ使われてるの?」と思う方もいるかもしれない。でも実際のところ、GitHubで99,500以上のスター、月間ダウンロード数は約1,700万〜1,800万回という数字を見ると、その存在感は健在なんですよね。
個人的には、Angularの「全部入り」な思想が好きで、大規模なプロジェクトで迷いなく開発を進められる点が魅力だと思っています。
特徴・メリット
1. フルスタックフレームワーク
Angularの最大の特徴は、必要なものが最初から揃っていること。
- ルーティング: Angular Router標準搭載
- フォーム: Template-driven FormsとReactive Forms
- HTTP通信: HttpClientモジュール
- 状態管理: Angular Signals(v21で強化)
- テスト: Karma、Jasmine、Protractor対応
ReactやVueだと「状態管理どうする?」「ルーティングは?」と選定で悩むことがあるけど、Angularはその心配がない。これ、意外と時短になるんですよね。
2. TypeScriptファースト
AngularはTypeScriptで書かれており、型安全な開発が前提になっています。
- コンパイル時エラー検出: バグを本番前にキャッチ
- IDE補完: VS Codeとの相性が抜群
- リファクタリング: 大規模コードベースでも安心
30代になって思うのは、型があるコードは半年後の自分を救ってくれるということ。チーム開発でも「この引数何?」みたいな会話が減ります。
3. Angular Signalsによる反応性
v16から導入されたSignalsが、v21でさらに強化されました。
import { signal, computed, effect } from '@angular/core';
// シグナルの定義
const count = signal(0);
// 算出値
const doubleCount = computed(() => count() * 2);
// 副作用
effect(() => {
console.log(`カウントが変わりました: ${count()}`);
});
// 値の更新
count.set(1);
count.update(v => v + 1);
RxJSの学習コストが高いと言われてきたAngularですが、Signalsの登場でReactのuseStateに近い感覚で状態管理できるようになりました。
4. パフォーマンス最適化
- SSR/SSG対応: サーバーサイドレンダリングと静的サイト生成
- 遅延読み込み: 必要なモジュールだけを読み込み
- Hydration: 初期表示の高速化
- Tree Shaking: 未使用コードの自動削除
インストール方法
Angular CLIのインストール
まずはAngular CLIをグローバルにインストールします。
npm install -g @angular/cli
バージョン確認:
ng version
新規プロジェクトの作成
ng new my-angular-app
対話形式でオプションを選択できます:
? Which stylesheet format would you like to use? SCSS
? Do you want to enable Server-Side Rendering (SSR)? Yes
? Do you want to do Standalone components? Yes
プロジェクト作成後:
cd my-angular-app
ng serve
http://localhost:4200 でアプリケーションが起動します。
既存プロジェクトへの追加
ng add @angular/material # UIコンポーネント
ng add @angular/pwa # PWA対応
ng add @angular/ssr # SSR対応
基本的な使い方
スタンドアロンコンポーネント
v14から導入されたスタンドアロンコンポーネントが、現在の推奨スタイルです。
// hello.component.ts
import { Component } from '@angular/core';
import { CommonModule } from '@angular/common';
@Component({
selector: 'app-hello',
standalone: true,
imports: [CommonModule],
template: `
<div class="greeting">
<h1>Hello, {{ name }}!</h1>
<p>カウント: {{ count() }}</p>
<button (click)="increment()">+1</button>
</div>
`,
styles: [`
.greeting {
padding: 20px;
text-align: center;
}
button {
padding: 10px 20px;
font-size: 16px;
cursor: pointer;
}
`]
})
export class HelloComponent {
name = 'Angular';
count = signal(0);
increment() {
this.count.update(v => v + 1);
}
}
入力と出力(Props的なもの)
// child.component.ts
import { Component, input, output } from '@angular/core';
@Component({
selector: 'app-child',
standalone: true,
template: `
<div>
<p>{{ message() }}</p>
<button (click)="sendToParent()">親に送信</button>
</div>
`
})
export class ChildComponent {
// v17.1から導入されたシグナルベースの入力
message = input.required<string>();
// 出力(イベントエミッター)
customEvent = output<string>();
sendToParent() {
this.customEvent.emit('子からのデータ');
}
}
// parent.component.ts
import { Component } from '@angular/core';
import { ChildComponent } from './child.component';
@Component({
selector: 'app-parent',
standalone: true,
imports: [ChildComponent],
template: `
<app-child
[message]="parentMessage"
(customEvent)="handleChildEvent($event)"
/>
`
})
export class ParentComponent {
parentMessage = '親からのメッセージ';
handleChildEvent(payload: string) {
console.log('子から受信:', payload);
}
}
Reactive Formsによるフォーム処理
import { Component } from '@angular/core';
import { ReactiveFormsModule, FormBuilder, Validators } from '@angular/forms';
import { CommonModule } from '@angular/common';
@Component({
selector: 'app-contact',
standalone: true,
imports: [CommonModule, ReactiveFormsModule],
template: `
<form [formGroup]="contactForm" (ngSubmit)="onSubmit()">
<div>
<label>名前</label>
<input formControlName="name" />
@if (contactForm.get('name')?.invalid && contactForm.get('name')?.touched) {
<span class="error">名前は必須です</span>
}
</div>
<div>
<label>メールアドレス</label>
<input formControlName="email" type="email" />
@if (contactForm.get('email')?.errors?.['email']) {
<span class="error">有効なメールアドレスを入力してください</span>
}
</div>
<div>
<label>メッセージ</label>
<textarea formControlName="message"></textarea>
</div>
<button type="submit" [disabled]="contactForm.invalid">送信</button>
</form>
`
})
export class ContactComponent {
contactForm = this.fb.group({
name: ['', Validators.required],
email: ['', [Validators.required, Validators.email]],
message: ['', Validators.required]
});
constructor(private fb: FormBuilder) {}
onSubmit() {
if (this.contactForm.valid) {
console.log(this.contactForm.value);
}
}
}
HTTP通信
import { Component, inject } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { CommonModule, AsyncPipe } from '@angular/common';
interface User {
id: number;
name: string;
email: string;
}
@Component({
selector: 'app-users',
standalone: true,
imports: [CommonModule, AsyncPipe],
template: `
@if (users$ | async; as users) {
<ul>
@for (user of users; track user.id) {
<li>{{ user.name }} ({{ user.email }})</li>
}
</ul>
} @else {
<p>読み込み中...</p>
}
`
})
export class UsersComponent {
private http = inject(HttpClient);
users$ = this.http.get<User[]>('https://api.example.com/users');
}
実践的なユースケース
1. 遅延読み込みによるルーティング
// app.routes.ts
import { Routes } from '@angular/router';
export const routes: Routes = [
{
path: '',
loadComponent: () => import('./home/home.component')
.then(m => m.HomeComponent)
},
{
path: 'dashboard',
loadComponent: () => import('./dashboard/dashboard.component')
.then(m => m.DashboardComponent),
canActivate: [authGuard]
},
{
path: 'admin',
loadChildren: () => import('./admin/admin.routes')
.then(m => m.ADMIN_ROUTES)
}
];
2. 認証ガード
// auth.guard.ts
import { inject } from '@angular/core';
import { Router, CanActivateFn } from '@angular/router';
import { AuthService } from './auth.service';
export const authGuard: CanActivateFn = () => {
const authService = inject(AuthService);
const router = inject(Router);
if (authService.isAuthenticated()) {
return true;
}
return router.createUrlTree(['/login']);
};
3. カスタムディレクティブ
// highlight.directive.ts
import { Directive, ElementRef, HostListener, input } from '@angular/core';
@Directive({
selector: '[appHighlight]',
standalone: true
})
export class HighlightDirective {
highlightColor = input('yellow', { alias: 'appHighlight' });
constructor(private el: ElementRef) {}
@HostListener('mouseenter')
onMouseEnter() {
this.highlight(this.highlightColor());
}
@HostListener('mouseleave')
onMouseLeave() {
this.highlight('');
}
private highlight(color: string) {
this.el.nativeElement.style.backgroundColor = color;
}
}
使用例:
<p [appHighlight]="'lightblue'">ホバーするとハイライトされます</p>
4. サービスによる状態管理
// cart.service.ts
import { Injectable, signal, computed } from '@angular/core';
interface CartItem {
id: number;
name: string;
price: number;
quantity: number;
}
@Injectable({
providedIn: 'root'
})
export class CartService {
private items = signal<CartItem[]>([]);
readonly cartItems = this.items.asReadonly();
readonly totalPrice = computed(() =>
this.items().reduce((sum, item) => sum + item.price * item.quantity, 0)
);
readonly itemCount = computed(() =>
this.items().reduce((sum, item) => sum + item.quantity, 0)
);
addItem(item: Omit<CartItem, 'quantity'>) {
this.items.update(items => {
const existing = items.find(i => i.id === item.id);
if (existing) {
return items.map(i =>
i.id === item.id ? { ...i, quantity: i.quantity + 1 } : i
);
}
return [...items, { ...item, quantity: 1 }];
});
}
removeItem(id: number) {
this.items.update(items => items.filter(i => i.id !== id));
}
clearCart() {
this.items.set([]);
}
}
まとめ
Angularは、その「全部入り」な設計思想で、特に大規模なエンタープライズ開発で真価を発揮するフレームワークです。
個人的に感じるAngularの良さをまとめると:
- 迷わない: ルーティング、フォーム、HTTP通信が標準装備
- 型安全: TypeScriptファーストで大規模開発も安心
- 進化し続ける: Signalsの導入でモダンな書き方に対応
- 安定感: Googleがバックにいる安心感
- 長期サポート: LTSポリシーが明確
正直なところ、個人開発や小規模プロジェクトならReactやVueの方が手軽かもしれない。でも、チームで長期間メンテナンスするプロジェクトなら、Angularの「レールに乗った開発」はコスパ的に悪くない選択だと思います。
11年以上の歴史と、2,195人以上のコントリビューターによる継続的な開発。この実績は伊達じゃないですね。