Angular 16부터 도입된 signal은 단순한 상태 저장소 이상의 의미를 갖습니다.
기존의 변경 감지 방식이 Zone.js에 의존하는 반면, signal은 Zone 의존 없이도 반응형 상태를 유지하며 UI를 갱신합니다.
특히 Zone 밖, Zoneless, 서드파티 라이브러리 이벤트 등 Angular의 기본 변경 감지가 닿지 않는 영역에서 빛을 발합니다.
왜 signal을 써야 할까?
1. Zone 밖에서도 안전한 상태 갱신
Angular는 기본적으로 Zone.js를 통해 비동기 이벤트를 감지합니다. 하지만 NgZone 밖, Zoneless, Web Worker, 서드파티 이벤트 같은 경우 Angular는 상태 변화를 인식하지 못합니다. signal은 Zone 의존 없이도 UI를 갱신하므로 이러한 상황에서 안정적입니다.
2. 미세 렌더링(Fine‑grained)
참조한 부분만 다시 그려, 큰 화면에서도 필요한 조각만 갱신합니다.
3. 동기(Sync) 접근으로 단순화
구독/해제 없이 즉시 값을 읽습니다. 템플릿/테스트가 간결해집니다.
<div>현재 페이지: {{ page() }}</div>
// 테스트
store.page.set(3);
expect(fixture.nativeElement.textContent).toContain('3');
4. 파생 상태 관리의 용이성
computed를 통해 다른 상태를 기반으로 한 파생 데이터를 쉽게 생성하고 자동 동기화할 수 있습니다.
items = signal<Product[]>([]);
expensiveTotal = computed(() => items().reduce((s, i) => s + heavy(i), 0));
5. 부수효과의 선언적 제어
effect는 의존 신호가 바뀔 때만 실행되고 정리(destroy)도 용이.
6. 성능 도구: batch, untracked
- batch: 여러 업데이트를 1회의 재계산/렌더로 묶기
- untracked: 의존성으로 추적하지 않고 읽기
import { batch, untracked } from '@angular/core';
batch(() => { a.set(1); b.set(2); }); // 재계산 1회
const v = untracked(() => debugFlag()); // 의존성 미포함
7. 보일러플레이트 절감 & 가독성
// 참조 불변성 깨져서 반응 안 할 수 있음
const s = signal({ count: 0 });
s().count++;
// 새 객체로 갱신
s.update(p => ({ ...p, count: p.count + 1 }));
활용 예시
고빈도 데이터 스트림 처리 (WebSocket + runOutsideAngular)
price = signal<number>(0);
connectPriceStream() {
this.ngZone.runOutsideAngular(() => {
this.priceSocket.on('price-update', (data) => {
this.price.set(data.newPrice); // Zone 밖에서도 UI 갱신
});
});
}
- 장점: 초당 수십~수백 번 변하는 데이터도 최소 렌더링으로 반영.
서드파티 차트와 연동
selectedPoint = signal<Point | null>(null);
initChart(el: HTMLElement) {
this.ngZone.runOutsideAngular(() => {
const chart = someChartLib.init(el);
chart.on('pointClick', (pt: Point) => {
this.selectedPoint.set(pt);
});
});
}
- 장점: 무거운 차트 이벤트를 Angular 변경 감지에 불필요하게 태우지 않음.
복합 상태 관리 + 파생 값
rawOrders = signal<Order[]>([]);
filteredOrders = computed(() => this.rawOrders().filter(o => o.status === 'pending'));
totalPendingPrice = computed(() =>
this.filteredOrders().reduce((sum, o) => sum + o.price, 0)
);
- 장점: 필터링·집계 같은 파생 값이 자동으로 동기화.
Web Worker 연계
result = signal<string>('');
setupWorker() {
const worker = new Worker(new URL('./calc.worker', import.meta.url));
worker.onmessage = (e) => this.result.set(e.data);
}
- 장점: CPU 연산을 메인 스레드와 분리하면서 UI 자동 반영.
서비스 + signal 상태 스토어 패턴
@Injectable({ providedIn: 'root' })
export class OrderStore {
private readonly _orders = signal<Order[]>([]);
readonly pendingOrders = computed(() => this._orders().filter(o => o.status === 'pending'));
loadOrders() {
this.api.fetchOrders().then(data => this._orders.set(data));
}
markAsComplete(id: string) {
this._orders.update(list => list.map(o => o.id === id ? { ...o, status: 'done' } : o));
}
}
@Component({
selector: 'app-orders',
template: `
<div *ngFor="let order of store.pendingOrders()">
{{ order.name }} - {{ order.price | currency }}
</div>
`
})
export class OrdersComponent {
store = inject(OrderStore);
ngOnInit() { this.store.loadOrders(); }
}
'개발 공부 > Angular' 카테고리의 다른 글
| Angular inject() (0) | 2025.09.01 |
|---|---|
| Signals vs RxJS (0) | 2025.08.23 |
| Angular의 내장 파이프(Built-in Pipes) (2) - Custom (1) | 2025.07.27 |
| Angular의 내장 파이프(Built-in Pipes) (1) (0) | 2025.07.25 |
| Angular 20 - *ngIf, *ngFor, *ngSwitch Deprecation (0) | 2025.07.18 |