Angular 신호: Angular 응답성의 미래
Angular 16을 통해 팀은 프레임워크 역사상 가장 중요한 혁신 중 하나를 도입했습니다. 나 신호. 이 반응형 프리미티브는 근본적인 패러다임 전환을 나타냅니다. Zone.js 기반 변경 감지 시스템에서 Angular를 세분화된 응답성, 실제로 영향을 받는 구성 요소만 상태 변경이 업데이트됩니다.
이 시리즈의 첫 번째 기사에서는 현대적인 각도 우리는 신호를 심층적으로 탐구할 것입니다: 이론부터 실습까지, 단순한 반응 상태 생성부터 고급 패턴까지 생산. RxJS 및 Zone.js에 이미 익숙하다면 Signals가 이를 어떻게 더 쉽게 만드는지 알게 될 것입니다. 탁월한 성능을 유지하면서 상태 관리를 대폭 강화합니다.
최신 Angular 시리즈 개요
| # | Articolo | 집중하다 |
|---|---|---|
| 1 | 현재 위치 - 각도 신호 | 세분화된 응답성 |
| 2 | 영역 없는 변경 감지 | Zone.js 삭제 |
| 3 | 새로운 템플릿 @if, @for, @defer | 현대적인 제어 흐름 |
| 4 | 독립형 구성 요소 | NgModule이 없는 아키텍처 |
| 5 | 신호 형태 | 신호가 포함된 반응형 양식 |
| 6 | SSR 및 증분 수화 | 서버 측 렌더링 |
| 7 | Angular의 핵심 웹 바이탈 | 성능 및 지표 |
| 8 | 각도 PWA | 프로그레시브 웹 앱 |
| 9 | 고급 종속성 주입 | DI 트리 셰이크 가능 |
| 10 | Angular 17에서 21로 마이그레이션 | 마이그레이션 가이드 |
왜 시그널인가? Zone.js 문제
Signals가 도입된 이유를 이해하려면 먼저 문제를 이해해야 합니다. 그들이 해결한다는 것. Angular는 창립 이래로 Zone.js 가로채다 비동기 이벤트(클릭, 타이머, HTTP 요청) 및 루프 자동 트리거 변화 감지. 이 접근 방식은 편리하기는 하지만 다음과 같은 중요한 제한 사항이 있습니다.
Zone.js의 한계
- 글로벌 오버헤드: Zone.js는 모든 브라우저 비동기 API(setTimeout, Promise, addEventListener)를 패치하여 번들에 가중치를 추가합니다(최대 13KB 축소).
- 변경 감지 범위가 너무 넓음: 상태가 변경되지 않은 경우에도 각 비동기 이벤트는 전체 구성 요소 트리 확인을 트리거합니다.
- 비호환성: 일부 타사 라이브러리는 Zone.js(웹 구성 요소, 일부 기본 라이브러리)에서 제대로 작동하지 않습니다.
- 복잡한 디버깅: 스택 추적이 Zone.js 프레임에 의해 오염되어 디버깅이 어려워집니다.
- 확장성: 수백 개의 구성요소가 있는 애플리케이션에서는 전역 변경 감지로 인해 병목 현상이 발생합니다.
신호는 시스템을 도입하여 이러한 문제를 해결합니다. 그래프 기반 응답성: 각 신호는 어떤 소비자가 신호에 의존하는지 정확히 알고 있으며, 신호의 가치가 언제 변하는지, 해당 소비자에게만 알림이 전송되고 업데이트됩니다. 폴링이나 전역 확인이 없습니다.
신호 대 Zone.js: 비교
| 나는 기다린다 | Zone.js | 신호 |
|---|---|---|
| 세분성 | 전체 구성요소 트리 | 템플릿의 단일 바인딩 |
| 트리거 | 모든 비동기 이벤트 | 값이 변경된 경우에만 |
| 번들 크기 | +13KB(zone.js) | 추가 0KB |
| 추적 | 암시적(원숭이 패치) | 명시적(종속성 그래프) |
| 디버깅 | 오염된 스택 추적 | 스택 추적 정리 |
| 게으른 평가 | No | 예(계산됨) |
신호 분석
Un 신호 이는 값을 둘러싼 반응형 래퍼입니다. 로 생각하시면 됩니다
값이 변경되면 소비자에게 자동으로 알리는 변수입니다. 기능
signal() 만들기 WritableSignal또는 다음과 같은 신호를 사용할 수 있습니다.
읽고 썼다.
신호 생성 및 읽기
import { signal } from '@angular/core';
// Creazione di un WritableSignal con valore iniziale
const count = signal(0); // WritableSignal<number>
const name = signal('Angular'); // WritableSignal<string>
const items = signal<string[]>([]); // Tipo esplicito con generics
// LEGGERE il valore: chiama il signal come funzione
console.log(count()); // 0
console.log(name()); // 'Angular'
console.log(items()); // []
// SCRIVERE un nuovo valore: usa .set()
count.set(5);
console.log(count()); // 5
// AGGIORNARE basandosi sul valore corrente: usa .update()
count.update(current => current + 1);
console.log(count()); // 6
// AGGIORNARE oggetti/array: usa .update() per immutabilita
items.update(list => [...list, 'Nuovo elemento']);
console.log(items()); // ['Nuovo elemento']
왜 읽기 위해 함수 호출을 사용합니까?
구문 count() 그것은 단지 문체적인 선택이 아닙니다.
Angular는 다음을 수행할 수 있습니다. 자동으로 종속성을 추적합니다. 언제
내부에서 신호를 읽습니다. computed() 또는 effect(), 각도
해당 읽기를 종속성으로 기록합니다. 다음과 같은 간단한 속성을 사용했다면
count.value, 이 자동 추적은 불가능합니다.
WritableSignal과 ReadonlySignal 비교
Angular는 두 가지 유형의 신호를 구별합니다. 에이 WritableSignal 메소드를 공개합니다
.set() e .update(), 동안 Signal (읽기 전용 신호)
읽기만 허용합니다. 이러한 구별은 캡슐화에 매우 중요합니다.
import { signal, Signal, WritableSignal } from '@angular/core';
// Signal privato e scrivibile nel servizio
private _counter: WritableSignal<number> = signal(0);
// Esponi solo la versione read-only ai consumatori
readonly counter: Signal<number> = this._counter.asReadonly();
// Metodo pubblico controllato per modificare lo stato
increment(): void {
this._counter.update(v => v + 1);
}
// I consumatori possono leggere ma NON scrivere
// this.counterService.counter() // OK - lettura
// this.counterService.counter.set(5) // ERRORE - non esiste .set()
평등과 통지
기본적으로 신호는 값이 다음에 따라 변경될 때만 소비자에게 알립니다.
Object.is(). 옵션을 사용하여 비교를 사용자 정의할 수 있습니다. equal:
// Signal con confronto personalizzato
const user = signal(
{ name: 'Federico', age: 30 },
{
equal: (a, b) => a.name === b.name && a.age === b.age
}
);
// Questo NON notifica i consumatori (stesso contenuto)
user.set({ name: 'Federico', age: 30 });
// Questo notifica i consumatori (contenuto diverso)
user.set({ name: 'Federico', age: 31 });
계산된 신호: 파생 상태
기능 computed() 값이 다음과 같은 신호를 생성합니다. 자동으로 파생
다른 신호에서. 이는 계산된 속성과 반응적으로 동일합니다. 값은 다음과 같습니다.
종속성 중 하나가 변경된 경우에만 다시 계산되고 결과가 저장됩니다.
(캐시됨) 다음 업데이트까지.
import { signal, computed } from '@angular/core';
const firstName = signal('Federico');
const lastName = signal('Calo');
const age = signal(30);
// Computed: ricalcolato SOLO quando firstName o lastName cambiano
const fullName = computed(() => `${firstName()} ${lastName()}`);
console.log(fullName()); // 'Federico Calo'
// Computed con logica condizionale
const canVote = computed(() => age() >= 18);
console.log(canVote()); // true
// Computed che dipende da altri computed (catena)
const greeting = computed(() =>
`Ciao ${fullName()}, ${canVote() ? 'puoi votare' : 'non puoi votare'}`
);
console.log(greeting()); // 'Ciao Federico Calo, puoi votare'
// Quando cambia firstName, fullName e greeting si aggiornano
// ma canVote NO (non dipende da firstName)
firstName.set('Marco');
console.log(fullName()); // 'Marco Calo'
console.log(greeting()); // 'Ciao Marco Calo, puoi votare'
계산()의 주요 기능
- 게으른 평가: 값은 종속성이 변경될 때가 아니라 누군가 읽을 때만 계산됩니다.
- 메모: 종속성이 변경되지 않은 경우 읽기는 다시 계산하지 않고 캐시된 값을 반환합니다.
- 자동 추적: 파생 함수 실행 중에 종속성이 발견되었습니다.
- 동적 종속성: 논리에 다음이 포함된 경우
if, 해당 단계에서 실제로 읽은 종속성만 추적됩니다. - 읽기 전용: 계산된 신호에는 메서드가 없습니다.
.set()o.update()
경고: 계산()에는 부작용이 없습니다.
전달된 함수 computed() 그래야만 해 순수한: 그러면 안 된다
다른 신호 수정, HTTP 호출 수행, DOM에 쓰기 또는 작업 수행
부작용이 있습니다. Angular는 언제든지 함수를 다시 실행할 수 있습니다.
값이 변경되었는지 확인하십시오. 부작용이 있는 경우에는 다음을 사용하십시오. effect().
효과: 반응성 부작용
기능 effect() 신호를 읽을 때마다 부작용을 수행합니다.
그 안에서 그들은 가치를 변화시킵니다. 상태를 동기화하는 이상적인 메커니즘입니다.
외부 세계와의 신호: 로깅, localStorage, 분석, DOM 조작.
import { signal, effect, Component } from '@angular/core';
@Component({
selector: 'app-logger',
standalone: true,
template: `<button (click)="increment()">Count: {{ count() }}</button>`
})
export class LoggerComponent {
count = signal(0);
constructor() {
// L'effect viene eseguito immediatamente e poi ogni volta
// che count() cambia valore
effect(() => {
console.log(`Il contatore vale: ${this.count()}`);
// Output: "Il contatore vale: 0" (subito)
// Output: "Il contatore vale: 1" (dopo click)
});
}
increment() {
this.count.update(v => v + 1);
}
}
효과 정리
효과가 재생되거나 파괴되면 리소스를 정리해야 할 수도 있습니다.
타이머, 구독 또는 청취자와 같은. Angular는 콜백을 제공합니다 onCleanup
이 목적을 위해:
import { signal, effect } from '@angular/core';
const pollingInterval = signal(5000);
effect((onCleanup) => {
const interval = pollingInterval();
const timerId = setInterval(() => {
console.log('Polling dati dal server...');
}, interval);
// Questa funzione viene chiamata PRIMA della prossima esecuzione
// e quando il componente viene distrutto
onCleanup(() => {
clearInterval(timerId);
console.log('Timer pulito');
});
});
효과에 대한 주입 컨텍스트
effect() 이내에 호출되어야 합니다. 주입 컨텍스트,
즉, 구성 요소, 지시문 또는 서비스의 생성자에서 또는 내부에서
공장 기능 inject(). 외부에서 효과를 만들어야 하는 경우
생성자에서 명시적으로 전달합니다.Injector:
import { signal, effect, inject, Injector, Component } from '@angular/core';
@Component({
selector: 'app-deferred',
standalone: true,
template: `<button (click)="startTracking()">Avvia tracking</button>`
})
export class DeferredEffectComponent {
private injector = inject(Injector);
data = signal('');
startTracking() {
// Passiamo l'injector esplicitamente
effect(() => {
console.log('Dati aggiornati:', this.data());
}, { injector: this.injector });
}
}
신호 입력: 새로운 @Input()
Angular 17.1부터 데코레이터는 @Input() 현대적인 대안을 기반으로
신호: 함수 input(). 신호 입력은 다음과 같습니다. 읽기 전용
반응형 그래프에 원활하게 통합되어 다음과 같이 직접 사용할 수 있습니다.
computed() e effect().
import { Component, input, computed } from '@angular/core';
@Component({
selector: 'app-user-card',
standalone: true,
template: `
<div class="card">
<h3>{{ displayName() }}</h3>
<span class="badge" [class.active]="isActive()">
{{ isActive() ? 'Attivo' : 'Inattivo' }}
</span>
</div>
`
})
export class UserCardComponent {
// Input opzionale con valore di default
name = input('Anonimo'); // InputSignal<string>
// Input obbligatorio (il parent DEVE fornirlo)
userId = input.required<number>(); // InputSignal<number>
// Input con alias (nome diverso nel template del parent)
active = input(false, { alias: 'isUserActive' });
// Computed che dipendono dagli input - tutto reattivo!
displayName = computed(() =>
`${this.name()} (ID: ${this.userId()})`
);
isActive = computed(() => this.active());
}
// Uso nel parent:
// <app-user-card [name]="'Federico'" [userId]="42" [isUserActive]="true" />
@Input()에 비해 input()의 장점
- 유형 안전:
input.required()부모가 값을 제공하지 않으면 컴파일 오류가 발생합니다. - 기본 응답성: 값은 신호이므로 다음과 직접 작동합니다.
computed()eeffect() - OnChanges 없음: 더 이상 구현할 필요가 없습니다.
ngOnChanges입력 변화에 반응하기 위해 - 통합 변환: 옵션을 사용하여 입력 값을 변환할 수 있습니다.
transform - 불변성: 신호 입력은 읽기 전용이므로 하위 구성요소의 우발적인 변경을 방지합니다.
입력 변환
import { Component, input, booleanAttribute, numberAttribute } from '@angular/core';
@Component({
selector: 'app-settings',
standalone: true,
template: `
<p>Colonne: {{ columns() }}</p>
<p>Disabilitato: {{ disabled() }}</p>
`
})
export class SettingsComponent {
// Converte automaticamente la stringa "3" nel numero 3
columns = input(1, { transform: numberAttribute });
// Converte la presenza dell'attributo in boolean
// <app-settings disabled /> => true
disabled = input(false, { transform: booleanAttribute });
}
신호 출력: 새로운 @Output()
입력과 유사하게 Angular는 다음을 도입합니다. output() 현대적인 대안으로
장식가에게 @Output(). 기능 output() 반환합니다
OutputEmitterRef 방법으로 .emit() 이벤트를 내보냅니다.
import { Component, input, output } from '@angular/core';
@Component({
selector: 'app-todo-item',
standalone: true,
template: `
<div class="todo">
<span>{{ text() }}</span>
<button (click)="onComplete()">Completa</button>
<button (click)="onDelete()">Elimina</button>
</div>
`
})
export class TodoItemComponent {
// Input
text = input.required<string>();
todoId = input.required<number>();
// Output - tipizzati e type-safe
completed = output<number>(); // OutputEmitterRef<number>
deleted = output<number>(); // OutputEmitterRef<number>
onComplete() {
this.completed.emit(this.todoId());
}
onDelete() {
this.deleted.emit(this.todoId());
}
}
// Uso nel parent:
// <app-todo-item
// [text]="'Studiare Signals'"
// [todoId]="1"
// (completed)="handleComplete($event)"
// (deleted)="handleDelete($event)"
// />
모델 신호: 양방향 바인딩
기능 model() 지원하는 특수 신호를 생성합니다.
양방향 바인딩 부모와 자식 구성 요소 사이. 현대의 진화다
조합의 @Input() + @Output() 컨벤션과 함께
valueChange.
import { Component, model } from '@angular/core';
@Component({
selector: 'app-rating',
standalone: true,
template: `
<div class="stars">
@for (star of stars; track star) {
<span
(click)="value.set(star)"
[class.filled]="star <= value()">
★
</span>
}
</div>
`
})
export class RatingComponent {
// model() crea un WritableSignal con two-way binding
value = model(0); // ModelSignal<number>
stars = [1, 2, 3, 4, 5];
}
// Uso nel parent con banana-in-a-box syntax:
// <app-rating [(value)]="userRating" />
// Quando il child chiama value.set(3), userRating nel parent si aggiorna
model()이 내부적으로 작동하는 방식
model() 자동으로 입력과 출력을 모두 생성합니다.
에 대한 model ~라고 불리는 value, Angular는 암시적으로 생성합니다.
입력 value 그리고 출력 valueChange. 당신이 전화할 때
value.set() o value.update(), 구성 요소 출력
이벤트가 자동으로 valueChange, 변경 사항을 상위 항목에 전파합니다.
신호 쿼리: viewChild() 및 viewChildren()
데코레이터 @ViewChild() e @ViewChildren() 그들은 지금 가지고 있다
신호 기반 대응물: viewChild() e viewChildren().
이 함수는 다음과 같은 경우 자동으로 업데이트되는 신호를 반환합니다.
참조된 요소가 변경됩니다.
import {
Component, viewChild, viewChildren,
ElementRef, AfterViewInit, effect
} from '@angular/core';
@Component({
selector: 'app-gallery',
standalone: true,
template: `
<input #searchInput type="text" placeholder="Cerca..." />
<div class="gallery">
@for (img of images(); track img.id) {
<img #galleryItem [src]="img.url" [alt]="img.title" />
}
</div>
`
})
export class GalleryComponent {
images = signal([
{ id: 1, url: '/img1.jpg', title: 'Foto 1' },
{ id: 2, url: '/img2.jpg', title: 'Foto 2' },
]);
// viewChild restituisce un Signal<ElementRef | undefined>
searchInput = viewChild<ElementRef>('searchInput');
// viewChild.required restituisce Signal<ElementRef> (no undefined)
// requiredInput = viewChild.required<ElementRef>('searchInput');
// viewChildren restituisce Signal<readonly ElementRef[]>
galleryItems = viewChildren<ElementRef>('galleryItem');
constructor() {
// Reagisci quando gli elementi della gallery cambiano
effect(() => {
const items = this.galleryItems();
console.log(`Gallery ha ${items.length} elementi`);
});
// Focus automatico sull'input di ricerca
effect(() => {
const input = this.searchInput();
if (input) {
input.nativeElement.focus();
}
});
}
}
신호 쿼리의 장점
- AfterViewInit 없음: 수명 주기 후크는 더 이상 필요하지 않습니다. 참조를 사용할 수 있으면 효과가 자동으로 반응합니다.
- 반응형 업데이트: 요소 목록이 변경되는 경우(예:
@for), 신호viewChildren업데이트된다 - 유형 안전:
viewChild.required()참조가 존재함을 보장하고 검사를 제거합니다.undefined - 구성성: 당신은 사용할 수 있습니다
computed()쿼리에서 데이터를 추출하려면
생산을 위한 실제 패턴
개별 API를 살펴본 후 일상적으로 사용하게 될 구체적인 패턴을 살펴보겠습니다. 프로덕션 Angular 애플리케이션에서.
패턴 1: 신호가 포함된 양식 상태
import { Component, signal, computed } from '@angular/core';
@Component({
selector: 'app-search',
standalone: true,
template: `
<input
[value]="query()"
(input)="onSearch($event)"
placeholder="Cerca prodotti..." />
<div class="filters">
<select (change)="onCategoryChange($event)">
<option value="">Tutte le categorie</option>
<option value="electronics">Elettronica</option>
<option value="books">Libri</option>
</select>
</div>
<p>Trovati {{ filteredProducts().length }} risultati</p>
@for (product of filteredProducts(); track product.id) {
<div class="product-card">
<h4>{{ product.name }}</h4>
<span>{{ product.price | currency:'EUR' }}</span>
</div>
}
`
})
export class SearchComponent {
// Stato reattivo del form
query = signal('');
category = signal('');
products = signal<Product[]>([]);
// Stato derivato: filtraggio reattivo
filteredProducts = computed(() => {
const q = this.query().toLowerCase();
const cat = this.category();
return this.products().filter(p =>
p.name.toLowerCase().includes(q) &&
(!cat || p.category === cat)
);
});
onSearch(event: Event) {
const value = (event.target as HTMLInputElement).value;
this.query.set(value);
}
onCategoryChange(event: Event) {
const value = (event.target as HTMLSelectElement).value;
this.category.set(value);
}
}
interface Product {
id: number;
name: string;
price: number;
category: string;
}
패턴 2: 서비스와 공유된 상태
import { Injectable, signal, computed } from '@angular/core';
interface CartItem {
productId: number;
name: string;
price: number;
quantity: number;
}
@Injectable({ providedIn: 'root' })
export class CartService {
// Stato privato
private _items = signal<CartItem[]>([]);
private _discount = signal(0);
// Stato pubblico read-only
readonly items = this._items.asReadonly();
readonly discount = this._discount.asReadonly();
// Stato derivato
readonly itemCount = computed(() =>
this._items().reduce((sum, item) => sum + item.quantity, 0)
);
readonly subtotal = computed(() =>
this._items().reduce((sum, item) => sum + item.price * item.quantity, 0)
);
readonly total = computed(() => {
const sub = this.subtotal();
const disc = this._discount();
return sub - (sub * disc / 100);
});
readonly isEmpty = computed(() => this._items().length === 0);
// Azioni pubbliche
addItem(product: Omit<CartItem, 'quantity'>) {
this._items.update(items => {
const existing = items.find(i => i.productId === product.productId);
if (existing) {
return items.map(i =>
i.productId === product.productId
? { ...i, quantity: i.quantity + 1 }
: i
);
}
return [...items, { ...product, quantity: 1 }];
});
}
removeItem(productId: number) {
this._items.update(items =>
items.filter(i => i.productId !== productId)
);
}
applyDiscount(percent: number) {
this._discount.set(Math.min(percent, 50)); // Max 50%
}
clearCart() {
this._items.set([]);
this._discount.set(0);
}
}
이 패턴이 잘 작동하기 때문에
이 서비스는 모든 구성 요소에 주입될 수 있습니다. 읽는 모든 구성 요소
cartService.total() 템플릿이 자동으로 업데이트됩니다.
카트가 바뀔 때. 수동 구독이 필요하지 않습니다.
async pipe, 필요하지 않습니다 Subject o BehaviorSubject.
이는 순수하고 반응적이며 캡슐화가 보장됩니다. asReadonly().
패턴 3: API에서 데이터 로드
import { Component, signal, computed, inject, OnInit } from '@angular/core';
import { HttpClient } from '@angular/common/http';
interface ApiState<T> {
data: T | null;
loading: boolean;
error: string | null;
}
@Component({
selector: 'app-users-list',
standalone: true,
template: `
@if (state().loading) {
<div class="spinner">Caricamento...</div>
}
@if (state().error; as error) {
<div class="error">Errore: {{ error }}</div>
}
@if (state().data; as users) {
<ul>
@for (user of users; track user.id) {
<li>{{ user.name }} - {{ user.email }}</li>
}
</ul>
}
`
})
export class UsersListComponent implements OnInit {
private http = inject(HttpClient);
state = signal<ApiState<User[]>>({
data: null,
loading: false,
error: null
});
// Computed di convenienza
isLoading = computed(() => this.state().loading);
hasError = computed(() => this.state().error !== null);
ngOnInit() {
this.loadUsers();
}
loadUsers() {
// Imposta loading
this.state.set({ data: null, loading: true, error: null });
this.http.get<User[]>('/api/users').subscribe({
next: (users) => {
this.state.set({ data: users, loading: false, error: null });
},
error: (err) => {
this.state.set({
data: null,
loading: false,
error: err.message || 'Errore sconosciuto'
});
}
});
}
}
interface User {
id: number;
name: string;
email: string;
}
RxJS와의 상호 운용성: toSignal() 및 toObservable()
Angular는 RxJS를 버리지 않습니다. 기능 덕분에 두 세계가 공존하고 협력합니다.
패키지의 상호 운용성 @angular/core/rxjs-interop.
이러한 함수는 신호의 명령형 세계와 선언적 세계 사이의 다리입니다.
Observable의.
toSignal(): Observable에서 Signal로
import { Component, inject } from '@angular/core';
import { toSignal } from '@angular/core/rxjs-interop';
import { HttpClient } from '@angular/common/http';
import { interval } from 'rxjs';
@Component({
selector: 'app-dashboard',
standalone: true,
template: `
<p>Utenti: {{ users()?.length ?? 'Caricamento...' }}</p>
<p>Timer: {{ timer() }} secondi</p>
`
})
export class DashboardComponent {
private http = inject(HttpClient);
// Converti un HTTP Observable in Signal
// Il Signal sarà undefined finchè la risposta non arriva
users = toSignal(this.http.get<User[]>('/api/users'));
// Con valore iniziale (evita undefined)
timer = toSignal(interval(1000), { initialValue: 0 });
// Con requireSync per Observable che emettono subito (es. BehaviorSubject)
// config = toSignal(this.configService.config$, { requireSync: true });
}
toObservable(): 신호에서 관찰 가능으로
import { Component, signal, inject, effect } from '@angular/core';
import { toObservable, toSignal } from '@angular/core/rxjs-interop';
import { debounceTime, distinctUntilChanged, switchMap } from 'rxjs/operators';
import { HttpClient } from '@angular/common/http';
@Component({
selector: 'app-autocomplete',
standalone: true,
template: `
<input
[value]="searchTerm()"
(input)="onInput($event)"
placeholder="Cerca..." />
@for (result of results(); track result.id) {
<div>{{ result.name }}</div>
}
`
})
export class AutocompleteComponent {
private http = inject(HttpClient);
searchTerm = signal('');
// Converti Signal in Observable, applica operatori RxJS,
// poi riconverti in Signal
results = toSignal(
toObservable(this.searchTerm).pipe(
debounceTime(300),
distinctUntilChanged(),
switchMap(term =>
term.length > 2
? this.http.get<SearchResult[]>(`/api/search?q=${term}`)
: []
)
),
{ initialValue: [] as SearchResult[] }
);
onInput(event: Event) {
const value = (event.target as HTMLInputElement).value;
this.searchTerm.set(value);
}
}
interface SearchResult {
id: number;
name: string;
}
toSignal()과 toObservable()을 사용하는 경우
| 대본 | 미국 | 이유 |
|---|---|---|
| 템플릿의 HTTP | toSignal() |
대체 async 파이프, 청소기 |
| 디바운스/스로틀 | toObservable() + RxJS |
RxJS 연산자는 시간 흐름에 이상적입니다. |
| UI 로컬 상태 | signal() 직접 |
토글, 카운터, 양식에는 RxJS가 필요하지 않습니다. |
| 웹소켓/SSE | toSignal() |
템플릿의 스트림을 Signal로 변환 |
| 여러 신호 결합 | computed() |
다음보다 간단함 combineLatest |
Zone.js에서 마이그레이션: 실용 가이드
기존 애플리케이션을 Signals로 마이그레이션하는 데는 전체 재작성이 필요하지 않습니다. 한 가지 접근 방식을 취할 수 있습니다 증분, 구성 요소 변환 한 번에. 여기에 실용적인 전략이 있습니다.
1단계: 간단한 속성을 신호로 변환
// PRIMA (Zone.js)
@Component({ ... })
export class CounterComponent {
count = 0;
label = 'Contatore';
increment() {
this.count++;
}
}
// Template: {{ count }} - {{ label }}
// DOPO (Signals)
@Component({ ... })
export class CounterComponent {
count = signal(0);
label = signal('Contatore');
increment() {
this.count.update(v => v + 1);
}
}
// Template: {{ count() }} - {{ label() }}
2단계: getter를 계산()으로 변환
// PRIMA
get fullName(): string {
return `${this.firstName} ${this.lastName}`;
}
get isValid(): boolean {
return this.email.includes('@') && this.name.length > 0;
}
// DOPO
fullName = computed(() => `${this.firstName()} ${this.lastName()}`);
isValid = computed(() =>
this.email().includes('@') && this.name().length > 0
);
3단계: 구독을 효과 또는 toSignal()로 변환
// PRIMA (sottoscrizione manuale)
@Component({ ... })
export class UserProfileComponent implements OnInit, OnDestroy {
user: User | null = null;
private destroy$ = new Subject<void>();
constructor(private userService: UserService) {}
ngOnInit() {
this.userService.currentUser$
.pipe(takeUntil(this.destroy$))
.subscribe(user => this.user = user);
}
ngOnDestroy() {
this.destroy$.next();
this.destroy$.complete();
}
}
// DOPO (con toSignal - zero boilerplate)
@Component({ ... })
export class UserProfileComponent {
private userService = inject(UserService);
// Si sottoscrive automaticamente, si pulisce automaticamente
user = toSignal(this.userService.currentUser$);
}
마이그레이션 중에 주의하세요
- 업데이트 템플릿: Signal로 변환된 각 속성에는 다음이 필요합니다.
()템플릿에서:{{ count }}된다{{ count() }} - toSignal()에는 주입 컨텍스트가 필요합니다. 생성자 또는 클래스 수준에서 초기화된 필드에서 호출되어야 합니다.
- 동일한 구성요소를 혼합하지 마십시오. Signal과 같은 메타 속성과 간단한 변수와 같은 메타 속성을 사용하지 마세요. 전체 구성 요소를 한 번에 마이그레이션하는 것이 좋습니다.
- 시험: 읽으려면 테스트를 업데이트해야 합니다.
component.count()대신에component.count
모범 사례 및 일반적인 실수
수개월 간의 프로덕션 사용 후 Angular 커뮤니티가 통합되었습니다. 일련의 모범 사례. 여기에 가장 중요한 것들이 있습니다.
신호의 8가지 황금률
- 미국
signal()지역 주별 - 토글, 카운터, 양식 상태: 항상 단순한 변수보다 신호를 선호합니다. - 미국
computed()파생 상태별 - 계산할 수 있는 상태는 절대 복제하지 마세요. 다른 신호에 의존하는 경우 계산됩니다. - 미국
effect()아껴서 - 실제 부작용(로깅, localStorage, 분석)에만 해당됩니다. 신호를 서로 동기화하는 데 사용하지 마십시오. - 폭로하다
asReadonly()- 서비스에서는 내부 Signal을 비공개 및 쓰기 가능으로 만들고 읽기 전용 버전만 노출합니다. - 당신은 선호
input()@Input()으로 - 신호 입력이 더욱 안전하고 반응성이 뛰어나며 유형이 안전합니다. - 미국
computed()대신에effect()상태를 도출하다 - 결과가 파생된 값인 경우 계산된 값을 사용합니다. 계산이 아닌 효과와 행동을 위한 것 - 내부 신호를 변경하지 마십시오
computed()- 함수는 순수해야 합니다. 계산된 내부의 신호에 쓰면 예측할 수 없는 동작이 발생합니다. - 재귀 방지 - 효과 자체가 읽는 신호에 쓰는 효과는 무한 루프를 생성합니다.
피해야 할 안티패턴
// ERRORE 1: Modificare un Signal dentro computed()
const a = signal(1);
const b = signal(0);
const sum = computed(() => {
b.set(a() * 2); // MAI! Side effect in computed
return a() + b();
});
// ERRORE 2: Dimenticare le parentesi nel template
// <p>{{ count }}</p> <-- Mostra la funzione, non il valore!
// <p>{{ count() }}</p> <-- Corretto
// ERRORE 3: Mutare l'oggetto invece di crearne uno nuovo
const user = signal({ name: 'Fed', age: 30 });
// NON fare questo:
user().name = 'Marco'; // La mutation non viene rilevata!
// Fai invece:
user.update(u => ({ ...u, name: 'Marco' }));
// ERRORE 4: Loop infinito con effect
const counter = signal(0);
effect(() => {
// Legge e scrive lo stesso Signal = loop!
counter.set(counter() + 1);
});
// ERRORE 5: toSignal() fuori dall'injection context
@Component({ ... })
export class BadComponent {
// Questo funziona (campo di classe)
data = toSignal(this.http.get('/api/data'));
onClick() {
// Questo NON funziona (metodo, no injection context)
const result = toSignal(this.http.get('/api/other'));
}
}
신호를 사용하지 말아야 할 경우
RxJS는 다음과 같은 경우에 여전히 최선의 선택입니다.
- 복잡한 스트림: WebSocket, 서버 전송 이벤트, 재시도를 통한 폴링
- 임시 연산자: debounceTime, throttleTime, 지연, bufferTime
- 조건부 흐름: 복잡한 로직을 포함하는 switchMap, 배기 맵, 병합 맵
- 경쟁 조건: 이전 요청을 삭제해야 하는 경우(switchMap)
- 배압: 고주파 스트림 관리
경험 법칙: 문제가 다음과 관련이 있는 경우 시간 또는 경쟁, RxJS를 사용하세요. 그 사람에 관한 것이라면 상태, 신호를 사용하세요.
요약 및 다음 단계
이 기사에서 우리는 Angular의 신호 시스템을 심층적으로 살펴보았습니다. 동기부여부터 구문까지, 실용적인 패턴부터 마이그레이션까지. 주요 사항은 다음과 같습니다.
주요 개념
signal()반응형 상태 만들기.set()e.update()computed()상태를 게으르게 도출하고 기억함effect()자동 정리를 통해 반응형 부작용을 수행합니다.input(),output()emodel()그들은 고전적인 데코레이터를 대체합니다viewChild()eviewChildren()쿼리를 반응형으로 만들기toSignal()etoObservable()RxJS와 Signals의 세계를 연결합니다.asReadonly()서비스의 캡슐화를 보장합니다.
시리즈의 다음 기사에서는 영역 없는 변경 감지, 어떻게 되는지 살펴보겠습니다 신호를 유일한 것으로 활용하여 애플리케이션에서 Zone.js를 완전히 제거합니다. 반응성 메커니즘 및 성능의 상당한 개선 획득 그리고 번들 크기.







