최근 사내 서비스에서 Angular 프레임워크를 사용하며 사용자 이벤트를 처리하던 중 예상치 못한 CPU 사용량 급증 현상을 발견했습니다.
이번 글에서는 문제의 원인을 분석하고, 해결 및 성능 개선 과정을 공유드리려 합니다.
이런 피드백을 듣고 CPU 사용량을 확인해봤습니다.
크롬 브라우저의 작업 관리자(Task Manager) 를 열면
각 탭 및 확장 프로그램(Renderer 프로세스)탭에서 CPU 사용률을 확인할 수 있습니다.
또는
Shift+Esc로 열 수 있음.
특정 화면에서 CPU 사용률이 100% 이상 찍히곤 합니다.
개발자도구의 성능(Performance) 탭에서 원인을 파악할 수 있었습니다.
화면을 녹화(Record)한 후 마우스를 몇 초간 크게 움직인 후 중단했습니다.
이후 다음 과정을 거쳐
- 한 프레임을 자세하게 볼 수 있도록 타임라인을 좁힘
- 프레임마다 찍히는
이벤트: mousemove를 마우스로 클릭- 호출 트리(Call Tree)탭을 선택


tick() / detectChanges() 호출이 반복되는 흔적을 확인할 수 있었습니다.

이 구간에서 Angular의 Zone.js가 마우스 이벤트를 감지할 때마다 루트 컴포넌트부터 자식 컴포넌트까지 전체 컴포넌트 트리를 순회하며 Change Detection(변경 감지) 을 실행하고 DOM 업데이트 여부를 검사하는 과정이 반복되고 있습니다.
이 과정이 불필요하게 매 프레임마다 실행되면서 CPU 사용률이 급격하게 상승하는 현상이 발생하는 것으로 확인했습니다.
The strategy that the default change detector uses to detect changes. When set, takes effect the next time change detection is triggered.
(번역: 기본 변경 감지기가 변경 사항을 감지하는 데 사용하는 전략입니다. 설정하면 다음에 변경 감지가 트리거될 때 적용됩니다.)
CheckAlways 전략을 사용합니다.이런 Angular의 철학도 장단점이 있습니다.
이러한 상황에서 장점은 최대한 활용하고 단점은 제거해야 합니다.
개선의 핵심은 이벤트를 Zone 밖에서 처리해서 CD를 불필요하게 트리거하지 않도록 만드는 것입니다.
Angular 문서에서 성능쪽의 Best Practices 를 뒤져보다 다음과 같은 자료를 찾을 수 있었습니다.
Resolving zone pollution(직역하면 '지역 오염 해결')
해당 문서에서 Zone.js 에 대한 개념을 확인할 수 있습니다.
Zone.js is a signaling mechanism that Angular uses to detect when an application state might have changed.
(번역: Zone.js 는 Angular가 애플리케이션 상태 변경을 감지하는 데 사용하는 신호 메커니즘입니다.)
문서를 더 살펴보면 경우에 따라 예약된 작업 이나 마이크로작업 이 데이터 모델을 변경하지 않아 변경 감지를 실행할 필요가 없다고 나옵니다
그 예시로
requestAnimationFrame, setTimeout또는setInterval이 언급되어 있습니다.
제 상황과 같은 경우에는 작업에 대해 변경 감지를 호출하지 않도록 Angular에 명시적으로 지시할 수 있습니다.
import { Component, NgZone, OnInit } from '@angular/core';
@Component(...)
class AppComponent implements OnInit {
private ngZone = inject(NgZone);
ngOnInit() {
// Change Detection을 막은 상태에서 이벤트 등록
this.ngZone.runOutsideAngular(() => {
window.addEventListener('mousemove', this.handleMove);
});
}
handleMove = (event: MouseEvent) => {
console.log('Moved:', event);
// 필요한 시점에만 Angular Zone으로 복귀
this.ngZone.run(() => {
this.updateUI();
});
};
updateUI() {
console.log('Angular에서 화면 갱신');
}
}
위와 같이 이벤트 처리 로직을 Zone.js의 감지 범위 밖에서 실행하도록 수정한 후
수정 전/후 CPU 사용률을 비교했습니다.
그 결과
마우스 이동 시 발생하던 불필요한 Change Detection 과정이 제거되면서
CPU 사용량이 약 20~30% 정도 감소하는 것을 확인할 수 있었습니다.
불필요했던 이벤트 → 전체 Change Detection 연쇄를 끊으면서 상당 부분 개선되었습니다.
브라우저는 보통 60Hz로 그리므로 한 프레임 예산은 16.7ms입니다.
=> 한 프레임 시간 = 1초(1000) ÷ 초당 프레임 수(60) = 16.7
이 안에 JS 실행 + 스타일/레이아웃/페인트/합성이 모두 들어와야 부드럽게(60FPS) 보입니다.
Angular의 장점(자동 변경 감지)은 생산성을 주지만
이벤트 과다 상황에서 전역 CD가 빈번히 돌면 프레임 예산을 초과하기 쉽습니다.
그래서 “이 이벤트는 UI에 영향이 없다”면 과감히 Zone 밖으로 분리하고
UI 반영이 필요할 때만 명시적으로 Angular로 복귀하는 전략이
성능과 유지보수성 사이에서 가장 현실적인 균형점이었습니다.
@HostListener('document:mousemove')가 매 이벤트마다 Zone.js -> Change Detection(전체 컴포넌트 트리 순회, 이하 CD)를 유발runOutsideAngular()로 옮겨 CD를 막고, 필요한 시점에만 Angular로 복귀