Angular 19 Zoneless: Zone.js 없이 구현하는 성능과 변경 감지
Angular의 zoneless 변경 감지는 Zone.js를 완전히 제거하여 더 작은 번들 크기, 빠른 렌더링, 그리고 시그널을 통한 명시적 반응성을 제공합니다. 이 심층 가이드는 Angular 19의 provideExperimentalZonelessChangeDetection부터 Angular 20+ 안정화 API까지 Zone.js에서 zoneless Angular로의 마이그레이션 경로를 다룹니다.

Angular의 zoneless 변경 감지는 standalone 컴포넌트 도입 이후 프레임워크에서 가장 중요한 아키텍처 변화를 나타냅니다. Zone.js를 완전히 제거함으로써 Angular 애플리케이션은 더 작은 번들 크기(약 33KB 감소), 불필요한 변경 감지 사이클 30-40% 감소, 그리고 Zone 관련 노이즈가 없는 깔끔한 스택 트레이스를 얻게 됩니다.
Angular 18에서 zoneless가 실험적으로 도입되었습니다. Angular 19는 provideExperimentalZonelessChangeDetection()으로 실험적 API를 개선했습니다. Angular 20은 provideZonelessChangeDetection()으로 안정화되었습니다. Angular 21은 새 프로젝트의 기본값으로 zoneless를 설정합니다.
Zone.js 변경 감지의 내부 동작 원리
zoneless를 이해하기 전에 Zone.js 메커니즘에 대한 명확한 설명이 필요합니다. Zone.js는 모든 비동기 브라우저 API를 몽키 패칭합니다: setTimeout, setInterval, Promise.then, addEventListener, XMLHttpRequest 등 수십 개의 API를 가로챕니다. 이러한 API 중 하나가 완료될 때마다 Zone.js는 Angular에 알리고, Angular는 전체 컴포넌트 트리에서 변경 감지를 실행합니다.
이 접근 방식에는 근본적인 결함이 있습니다: Zone.js는 애플리케이션 상태가 실제로 변경되었는지에 대한 통찰력이 없습니다. 순수하게 애니메이션 타이밍을 위해 사용된 setTimeout도 여전히 전체 변경 감지 사이클을 트리거합니다. 수백 개의 컴포넌트가 있는 대규모 애플리케이션에서 이러한 오버헤드는 측정 가능해집니다.
import { ApplicationConfig } from '@angular/core';
import { provideZoneChangeDetection } from '@angular/core';
export const appConfig: ApplicationConfig = {
providers: [
provideZoneChangeDetection({ eventCoalescing: true }),
// Zone.js는 약 130개 이상의 브라우저 API를 패치합니다
// 모든 비동기 콜백이 변경 감지를 트리거합니다
]
};이벤트 병합(Angular 14에서 도입)은 여러 이벤트를 단일 변경 감지 사이클로 배치하여 일부 오버헤드를 줄이지만, 핵심 문제는 여전히 남아 있습니다: 변경 감지가 필요한 것보다 훨씬 더 자주 실행됩니다.
Angular 19와 20에서 Zoneless 변경 감지 활성화하기
마이그레이션 경로는 Angular 버전에 따라 다릅니다. Angular 19는 실험적 API를 사용하고, Angular 20은 안정화된 버전을 제공합니다.
import { ApplicationConfig } from '@angular/core';
import { provideExperimentalZonelessChangeDetection } from '@angular/core';
export const appConfig: ApplicationConfig = {
providers: [
provideExperimentalZonelessChangeDetection(),
// 더 이상 Zone.js 패칭이 없습니다
]
};import { ApplicationConfig } from '@angular/core';
import { provideZonelessChangeDetection } from '@angular/core';
export const appConfig: ApplicationConfig = {
providers: [
provideZonelessChangeDetection(),
]
};프로바이더를 전환한 후, angular.json의 빌드 및 테스트 타겟 모두에서 polyfills 배열에서 zone.js를 제거한 다음 패키지를 제거합니다:
# angular.json의 빌드 및 테스트 타겟에서 zone.js polyfill 제거
# 그런 다음 패키지 제거
npm uninstall zone.js번들 크기 감소는 즉각적입니다: Zone.js는 즉시 로드되는 코드의 약 33KB 원본(10KB gzip)을 차지합니다.
Zoneless 모드에서 변경 감지를 트리거하는 것
Zone.js가 모든 비동기 작업을 가로채지 않으면 Angular는 명시적 알림에 의존합니다. 프레임워크는 다음 조건 중 하나가 발생할 때 변경 감지를 예약합니다:
- 템플릿에서 읽은 시그널이 값을 업데이트함
ChangeDetectorRef.markForCheck()가 호출됨 (AsyncPipe에 의해 자동으로)ComponentRef.setInput()을 통해 컴포넌트 입력이 변경됨- 템플릿 또는 호스트 리스너 콜백이 실행됨 (클릭, 입력 등)
- 이전에 더티했던 뷰가 컴포넌트 트리에 첨부됨
import { Component, signal, computed } from '@angular/core';
@Component({
selector: 'app-counter',
template: `
<div class="counter">
<button (click)="decrement()">-</button>
<span>{{ count() }}</span>
<button (click)="increment()">+</button>
<p>Double: {{ doubled() }}</p>
</div>
`
})
export class CounterComponent {
// 시그널 업데이트는 템플릿에 자동으로 알립니다
count = signal(0);
doubled = computed(() => this.count() * 2);
increment() {
this.count.update(v => v + 1);
// markForCheck() 필요 없음 - 시그널이 알림 처리
}
decrement() {
this.count.update(v => v - 1);
}
}시그널은 zoneless 모드의 자연스러운 동반자입니다. 시그널 값이 변경되면 Angular는 정확히 어떤 템플릿이 이에 의존하는지 알고 해당 뷰에 대해서만 타겟 변경 감지를 예약합니다.
ChangeDetectionStrategy.OnPush를 채택하는 것은 zoneless 호환성을 위한 권장 단계이지만 엄격하게 필수는 아닙니다. 기본 변경 감지 전략은 여전히 zoneless 모드에서 작동합니다. 그러나 OnPush는 컴포넌트가 입력이 변경되거나 markForCheck()가 호출될 때만 다시 렌더링되도록 보장하여 zoneless 멘탈 모델과 자연스럽게 정렬됩니다.
기존 애플리케이션 마이그레이션: 일반적인 함정
Zone.js에서 zoneless로의 전환은 기존 애플리케이션에 대해 거의 한 줄 변경이 아닙니다. Zone.js의 암묵적 동작에 의존했던 여러 패턴은 명시적 처리가 필요합니다.
setTimeout과 setInterval은 더 이상 업데이트를 트리거하지 않습니다
Zone.js 모드에서 setTimeout 콜백은 자동으로 변경 감지를 트리거합니다. zoneless 모드에서는 그렇지 않습니다.
@Component({
selector: 'app-user-status',
template: `<span>{{ statusMessage }}</span>`
})
export class UserStatusComponent {
statusMessage = 'Loading...';
ngOnInit() {
setTimeout(() => {
// Zone.js는 여기서 CD를 트리거했을 것입니다 - zoneless는 아닙니다
this.statusMessage = 'Ready';
}, 2000);
}
}import { Component, signal } from '@angular/core';
@Component({
selector: 'app-user-status',
template: `<span>{{ statusMessage() }}</span>`
})
export class UserStatusComponent {
statusMessage = signal('Loading...');
ngOnInit() {
setTimeout(() => {
// 시그널 업데이트는 Angular에 자동으로 알립니다
this.statusMessage.set('Ready');
}, 2000);
}
}Reactive Forms는 명시적 알림이 필요합니다
FormControl.setValue() 또는 patchValue()를 통한 폼 상태 변경은 zoneless 모드에서 자동으로 변경 감지를 트리거하지 않습니다. 두 가지 접근 방식이 이를 해결합니다: 폼 옵저버블을 markForCheck()에 연결하거나 시그널을 통해 폼 데이터를 노출합니다.
import { Component, inject, signal } from '@angular/core';
import { FormControl, ReactiveFormsModule } from '@angular/forms';
import { toSignal } from '@angular/core/rxjs-interop';
import { debounceTime, distinctUntilChanged } from 'rxjs';
@Component({
selector: 'app-search',
imports: [ReactiveFormsModule],
template: `
<input [formControl]="searchControl" placeholder="Search..." />
<p>Results for: {{ searchTerm() }}</p>
`
})
export class SearchComponent {
searchControl = new FormControl('');
// 옵저버블을 시그널로 변환하여 자동 템플릿 업데이트
searchTerm = toSignal(
this.searchControl.valueChanges.pipe(
debounceTime(300),
distinctUntilChanged()
),
{ initialValue: '' }
);
}Angular 면접 준비가 되셨나요?
인터랙티브 시뮬레이터, flashcards, 기술 테스트로 연습하세요.
Zone.js 없는 서버 사이드 렌더링
zoneless Angular를 사용한 SSR은 특별한 주의가 필요합니다. Zone.js는 이전에 Angular가 직렬화에 적합한 "안정적인" 상태에 도달했는지 판단하는 데 도움을 주었습니다. 이것이 없으면 PendingTasks 서비스가 그 역할을 채웁니다.
import { Component, inject, signal } from '@angular/core';
import { PendingTasks } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { firstValueFrom } from 'rxjs';
@Component({
selector: 'app-data-loader',
template: `
@if (data()) {
<div>{{ data()!.title }}</div>
} @else {
<div>Loading...</div>
}
`
})
export class DataLoaderComponent {
private http = inject(HttpClient);
private pendingTasks = inject(PendingTasks);
data = signal<{ title: string } | null>(null);
ngOnInit() {
// PendingTasks.run()은 완료될 때까지 SSR 직렬화를 방지합니다
this.pendingTasks.run(async () => {
const result = await firstValueFrom(
this.http.get<{ title: string }>('/api/data')
);
this.data.set(result);
});
}
}PendingTasks 없이 Angular는 비동기 데이터가 로드되기 전에 페이지를 직렬화하여 클라이언트에 빈 콘텐츠를 보냅니다.
성능 벤치마크: Zone.js vs Zoneless
Zone.js 제거로 인한 성능 향상은 세 가지 범주로 나뉩니다:
| 지표 | Zone.js | Zoneless | 개선 | |--------|---------|----------|-------------| | 초기 번들 크기 | +33KB 원본 / +10KB gzip | 0KB 오버헤드 | 100% 감소 | | 변경 감지 사이클 (일반적인 앱) | 상호작용당 150-300회 | 상호작용당 5-15회 | 80-95% 감소 | | 스택 트레이스 깊이 | 8-12개의 추가 Zone 프레임 | 깔끔한 네이티브 트레이스 | 즉각적인 명확성 | | Time to Interactive (TTI) | 기준선 | 15-25% 더 빠름 | Zone 부트스트랩 제거 |
가장 극적인 개선은 무거운 비동기 활동이 있는 애플리케이션에서 나타납니다: HTTP 폴링, WebSocket 연결, 타이머, 복잡한 이벤트 핸들러. 이들 각각은 이전에 zoneless 모드가 완전히 제거하는 불필요한 변경 감지 사이클을 트리거했습니다.
일부 서드파티 라이브러리는 여전히 내부적으로 Zone.js에 의존합니다. 모달 대화 상자, 특정 Web Component 래퍼, 일부 애니메이션 라이브러리는 zoneless 모드에서 예기치 않게 동작할 수 있습니다. 프로덕션에 zoneless를 배포하기 전에 항상 라이브러리 통합을 철저히 테스트하십시오.
전환에서 살아남는 NgZone API
일반적인 오해: Zone.js를 제거하면 모든 NgZone 참조를 제거해야 합니다. 이것은 잘못되었습니다. NgZone.run()과 NgZone.runOutsideAngular()는 zoneless 애플리케이션과 호환되며 공유 라이브러리에 유지되어야 합니다. 이들을 제거하면 여전히 Zone.js를 사용하고 해당 라이브러리를 소비하는 애플리케이션에서 성능 저하를 일으킬 수 있습니다.
그러나 세 가지 NgZone 옵저버블은 제거해야 합니다:
NgZone.onMicrotaskEmpty- zoneless 모드에서 방출되지 않음NgZone.onUnstable- zoneless 모드에서 방출되지 않음NgZone.onStable- zoneless 모드에서 방출되지 않음
이러한 옵저버블을 사용한 타이밍 종속 로직을 @angular/core의 afterNextRender() 또는 afterEveryRender()로 교체하십시오.
Angular 19 실험적에서 Angular 21 기본값까지
zoneless API 진화는 명확한 안정화 경로를 따릅니다:
- Angular 18.1:
provideExperimentalZonelessChangeDetection()이 실험적으로 도입됨 - Angular 19: 실험적 API 개선, 광범위한 생태계 테스트
- Angular 20:
provideZonelessChangeDetection()으로 이름 변경, 안정화로 승격 - Angular 20.2: API 완전히 안정화되어 동작 변경 예상 없음
- Angular 21: zoneless가
ng new프로젝트의 기본값이 되며 프로바이더 호출 불필요
Angular 19를 사용하는 팀의 업그레이드 경로는 간단합니다: Angular 20으로 업데이트하고, provideExperimentalZonelessChangeDetection을 provideZonelessChangeDetection으로 교체하고, zone.js polyfill을 제거합니다. Angular 19는 2026년 5월 19일에 지원 종료되므로 이 마이그레이션은 시간에 민감합니다.
Angular 면접을 준비하고 계신가요? SharpSkill의 Angular 면접 질문 모듈은 면접관이 점점 더 묻는 zoneless 시나리오를 포함한 변경 감지 패턴을 심도 있게 다룹니다. 더 광범위한 개요는 Angular 면접 질문 상위 25개 가이드를 참조하십시오. Angular 시그널 모듈은 zoneless를 가능하게 하는 반응성 모델도 다룹니다.
결론
- Zone.js를 제거하면 33KB의 번들 무게를 제거하고 130개 이상의 브라우저 API를 가로채던 몽키 패칭 레이어를 제거합니다
- Angular 시그널은 zoneless를 실용적으로 만드는 명시적 반응성 모델을 제공하며, 상태가 변경될 때 템플릿에 자동으로 알립니다
- 마이그레이션은
setTimeout/setInterval패턴, reactive forms, 타이밍 종속 로직을 시그널 기반 또는markForCheck()접근 방식으로 변환해야 합니다 - SSR 애플리케이션은 Zone.js 안정성 감지를 대체하기 위해
PendingTasks를 채택해야 합니다 NgZone.run()과NgZone.runOutsideAngular()는 이전 버전과의 호환성을 위해 공유 라이브러리에 보존되어야 합니다- Angular 19의 실험적 API(
provideExperimentalZonelessChangeDetection)는 Angular 20의 안정화된provideZonelessChangeDetection()에 직접 매핑되어 업그레이드가 이름 변경 작업이 됩니다 - 서드파티 라이브러리 호환성은 여전히 주요 위험 요소이며 프로덕션 배포 전에 철저한 테스트가 필요합니다
연습을 시작하세요!
면접 시뮬레이터와 기술 테스트로 지식을 테스트하세요.
태그
공유
관련 기사

Angular 18 Signals 완전 가이드: 새로운 리액티브 API와 Zoneless 변경 감지
Angular 18의 Signals, Zoneless 변경 감지, signal 기반 API를 해설합니다. input(), model(), viewChild()의 실전 사용법을 코드 예제와 함께 소개합니다.

Angular 스탠드얼론 컴포넌트: 마이그레이션 가이드와 모범 사례 2026
NgModule 기반 Angular 애플리케이션을 스탠드얼론 컴포넌트로 마이그레이션하는 완전 가이드입니다. 공식 CLI 3단계 마이그레이션, 지연 로딩, 라우팅, Angular 21의 모범 사례를 다룹니다.

Angular 면접 질문 TOP 25: 성공을 위한 완벽 가이드
2026년 가장 많이 묻는 Angular 면접 질문 25선. 상세한 답변과 코드 예시, Angular 개발자 포지션을 잡기 위한 팁을 제공합니다.