# Angular 19 Zoneless: Zone.js 없이 구현하는 성능과 변경 감지
> Angular의 zoneless 변경 감지는 Zone.js를 완전히 제거하여 더 작은 번들 크기, 빠른 렌더링, 그리고 시그널을 통한 명시적 반응성을 제공합니다. 이 심층 가이드는 Angular 19의 provideExperimentalZonelessChangeDetection부터 Angular 20+ 안정화 API까지 Zone.js에서 zoneless Angular로의 마이그레이션 경로를 다룹니다.
- Published: 2026-04-27
- Updated: 2026-04-27
- Author: SharpSkill
- Tags: angular, zoneless, change-detection, performance, signals, zone-js
- Reading time: 10 min
---
Angular의 zoneless 변경 감지는 standalone 컴포넌트 도입 이후 프레임워크에서 가장 중요한 아키텍처 변화를 나타냅니다. Zone.js를 완전히 제거함으로써 Angular 애플리케이션은 더 작은 번들 크기(약 33KB 감소), 불필요한 변경 감지 사이클 30-40% 감소, 그리고 Zone 관련 노이즈가 없는 깔끔한 스택 트레이스를 얻게 됩니다.
> **Zoneless 타임라인**
>
> 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`도 여전히 전체 변경 감지 사이클을 트리거합니다. 수백 개의 컴포넌트가 있는 대규모 애플리케이션에서 이러한 오버헤드는 측정 가능해집니다.
```typescript
// app.config.ts - 전통적인 Zone.js 설정 (Angular 18-19)
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은 안정화된 버전을 제공합니다.
```typescript
// app.config.ts - Angular 19 (실험적)
import { ApplicationConfig } from '@angular/core';
import { provideExperimentalZonelessChangeDetection } from '@angular/core';
export const appConfig: ApplicationConfig = {
providers: [
provideExperimentalZonelessChangeDetection(),
// 더 이상 Zone.js 패칭이 없습니다
]
};
```
```typescript
// app.config.ts - Angular 20+ (안정화)
import { ApplicationConfig } from '@angular/core';
import { provideZonelessChangeDetection } from '@angular/core';
export const appConfig: ApplicationConfig = {
providers: [
provideZonelessChangeDetection(),
]
};
```
프로바이더를 전환한 후, `angular.json`의 빌드 및 테스트 타겟 모두에서 polyfills 배열에서 `zone.js`를 제거한 다음 패키지를 제거합니다:
```bash
# angular.json의 빌드 및 테스트 타겟에서 zone.js polyfill 제거
# 그런 다음 패키지 제거
npm uninstall zone.js
```
번들 크기 감소는 즉각적입니다: Zone.js는 즉시 로드되는 코드의 약 33KB 원본(10KB gzip)을 차지합니다.
## Zoneless 모드에서 변경 감지를 트리거하는 것
Zone.js가 모든 비동기 작업을 가로채지 않으면 Angular는 명시적 알림에 의존합니다. 프레임워크는 다음 조건 중 하나가 발생할 때 변경 감지를 예약합니다:
- 템플릿에서 읽은 시그널이 값을 업데이트함
- `ChangeDetectorRef.markForCheck()`가 호출됨 (`AsyncPipe`에 의해 자동으로)
- `ComponentRef.setInput()`을 통해 컴포넌트 입력이 변경됨
- 템플릿 또는 호스트 리스너 콜백이 실행됨 (클릭, 입력 등)
- 이전에 더티했던 뷰가 컴포넌트 트리에 첨부됨
```typescript
// counter.component.ts - 시그널 기반 변경 감지
import { Component, signal, computed } from '@angular/core';
@Component({
selector: 'app-counter',
template: `
{{ count() }}
Double: {{ doubled() }}
`
})
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는 정확히 어떤 템플릿이 이에 의존하는지 알고 해당 뷰에 대해서만 타겟 변경 감지를 예약합니다.
> **OnPush는 권장되지만 필수는 아닙니다**
>
> `ChangeDetectionStrategy.OnPush`를 채택하는 것은 zoneless 호환성을 위한 권장 단계이지만 엄격하게 필수는 아닙니다. 기본 변경 감지 전략은 여전히 zoneless 모드에서 작동합니다. 그러나 OnPush는 컴포넌트가 입력이 변경되거나 `markForCheck()`가 호출될 때만 다시 렌더링되도록 보장하여 zoneless 멘탈 모델과 자연스럽게 정렬됩니다.
## 기존 애플리케이션 마이그레이션: 일반적인 함정
Zone.js에서 zoneless로의 전환은 기존 애플리케이션에 대해 거의 한 줄 변경이 아닙니다. Zone.js의 암묵적 동작에 의존했던 여러 패턴은 명시적 처리가 필요합니다.
### setTimeout과 setInterval은 더 이상 업데이트를 트리거하지 않습니다
Zone.js 모드에서 `setTimeout` 콜백은 자동으로 변경 감지를 트리거합니다. zoneless 모드에서는 그렇지 않습니다.
```typescript
// user-status.component.ts - 이전: Zone.js에 의존
@Component({
selector: 'app-user-status',
template: `{{ statusMessage }}`
})
export class UserStatusComponent {
statusMessage = 'Loading...';
ngOnInit() {
setTimeout(() => {
// Zone.js는 여기서 CD를 트리거했을 것입니다 - zoneless는 아닙니다
this.statusMessage = 'Ready';
}, 2000);
}
}
```
```typescript
// user-status.component.ts - 이후: 시그널 기반 접근 방식
import { Component, signal } from '@angular/core';
@Component({
selector: 'app-user-status',
template: `{{ statusMessage() }}`
})
export class UserStatusComponent {
statusMessage = signal('Loading...');
ngOnInit() {
setTimeout(() => {
// 시그널 업데이트는 Angular에 자동으로 알립니다
this.statusMessage.set('Ready');
}, 2000);
}
}
```
### Reactive Forms는 명시적 알림이 필요합니다
`FormControl.setValue()` 또는 `patchValue()`를 통한 폼 상태 변경은 zoneless 모드에서 자동으로 변경 감지를 트리거하지 않습니다. 두 가지 접근 방식이 이를 해결합니다: 폼 옵저버블을 `markForCheck()`에 연결하거나 시그널을 통해 폼 데이터를 노출합니다.
```typescript
// search.component.ts - zoneless와 함께하는 Reactive forms
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: `
Results for: {{ searchTerm() }}
`
})
export class SearchComponent {
searchControl = new FormControl('');
// 옵저버블을 시그널로 변환하여 자동 템플릿 업데이트
searchTerm = toSignal(
this.searchControl.valueChanges.pipe(
debounceTime(300),
distinctUntilChanged()
),
{ initialValue: '' }
);
}
```
## Zone.js 없는 서버 사이드 렌더링
zoneless Angular를 사용한 SSR은 특별한 주의가 필요합니다. Zone.js는 이전에 Angular가 직렬화에 적합한 "안정적인" 상태에 도달했는지 판단하는 데 도움을 주었습니다. 이것이 없으면 `PendingTasks` 서비스가 그 역할을 채웁니다.
```typescript
// data-loader.component.ts - SSR 호환 비동기 로딩
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()) {
{{ data()!.title }}
} @else {
Loading...
}
`
})
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 면접 질문](/ko/technologies/angular/interview-questions/change-detection) 모듈은 면접관이 점점 더 묻는 zoneless 시나리오를 포함한 변경 감지 패턴을 심도 있게 다룹니다. 더 광범위한 개요는 [Angular 면접 질문 상위 25개](/ko/blog/angular/top-25-angular-interview-questions) 가이드를 참조하십시오. [Angular 시그널 모듈](/ko/technologies/angular/interview-questions/angular-signals)은 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()`에 직접 매핑되어 업그레이드가 이름 변경 작업이 됩니다
- 서드파티 라이브러리 호환성은 여전히 주요 위험 요소이며 프로덕션 배포 전에 철저한 테스트가 필요합니다
---
Source: SharpSkill (https://sharpskill.dev), tech interview preparation for your real stack.
HTML version of this page: https://sharpskill.dev/ko/blog/angular/angular-19-zoneless-change-detection-performance