[Angular] CPU 사용량 2~30% 감소시킨 과정: Change Detection의 숨은 비용

김유현·2025년 10월 22일

실무 개선 기록

목록 보기
2/10
post-thumbnail

최근 사내 서비스에서 Angular 프레임워크를 사용하며 사용자 이벤트를 처리하던 중 예상치 못한 CPU 사용량 급증 현상을 발견했습니다.
이번 글에서는 문제의 원인을 분석하고, 해결 및 성능 개선 과정을 공유드리려 합니다.

1. 문제 상황

"마우스만 움직여도 버벅여요"

이런 피드백을 듣고 CPU 사용량을 확인해봤습니다.
크롬 브라우저의 작업 관리자(Task Manager) 를 열면
각 탭 및 확장 프로그램(Renderer 프로세스)탭에서 CPU 사용률을 확인할 수 있습니다.

또는 Shift+Esc 로 열 수 있음.

1-1. 크롬 작업 관리자에서 보인 이상 징후

특정 화면에서 CPU 사용률이 100% 이상 찍히곤 합니다.

  • 여기서 100%는 전체 시스템 기준이 아니라 "단일 코어 기준 상대값" 입니다.
    • e.g. 8코어(16스레드) 시스템이라면 이론상 1600%까지도 보일 수 있음.
  • “100을 넘는다 = 최소 한 코어를 꽉 채울 만큼의 부하가 걸린다”는 의미입니다.

1-2. 체감 증상

  • 마우스를 화면에서 휙휙 움직이기만 해도 페이지가 툭툭 끊기거나(프레임 드랍)
  • 애니메이션/지도 드로잉의 부드러움이 사라지는 현상이 눈에 보였습니다.

2. 원인 파악

2-1. 성능 탭에서 본 타임라인

개발자도구의 성능(Performance) 탭에서 원인을 파악할 수 있었습니다.
화면을 녹화(Record)한 후 마우스를 몇 초간 크게 움직인 후 중단했습니다.

이후 다음 과정을 거쳐

  1. 한 프레임을 자세하게 볼 수 있도록 타임라인을 좁힘
  2. 프레임마다 찍히는 이벤트: mousemove를 마우스로 클릭
  3. 호출 트리(Call Tree)탭을 선택


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

이 구간에서 Angular의 Zone.js가 마우스 이벤트를 감지할 때마다 루트 컴포넌트부터 자식 컴포넌트까지 전체 컴포넌트 트리를 순회하며 Change Detection(변경 감지) 을 실행하고 DOM 업데이트 여부를 검사하는 과정이 반복되고 있습니다.

이 과정이 불필요하게 매 프레임마다 실행되면서 CPU 사용률이 급격하게 상승하는 현상이 발생하는 것으로 확인했습니다.

2-2. 그렇다면 왜 Angular는 매번 전체를 검사할까?

  • Angular의 철학 중 하나는 "자동으로 항상 일관된 UI를 보장하자"입니다.
    • 대표적으로 Angular 공식 문서에는 Angular의 변경 감지(Change Detection) 메커니즘을 “어떤 방식으로 적용할지” 결정하는 전략(Strategy)을 설정할 수 있는 enum 값이 존재합니다.
    • ChangeDetectionStrategy 라는 enum인데 설명은 다음과 같습니다.
      • 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.js를 통해 이벤트/비동기 동작을 감시하고, 무언가 발생하면 "혹시 화면이 바뀌었을지 몰라!"라는 가정하에 Change Detection(변경 감지)을 돌립니다.
    • Change Detection은 루트 컴포넌트부터 자식 컴포넌트까지 트리를 순회하며
      템플릿 바인딩 값이 바뀌었는지 검사하고, 바뀌었다면 DOM을 갱신합니다.

2-3. 장단점

이런 Angular의 철학도 장단점이 있습니다.

  • 장점:
    • UI 동기화 걱정이 없음
      • (데이터만 바꾸면 알아서 화면 반영)
    • 명시적 렌더링 제어가 필요 없음
      • 유지보수나 규모가 큰 프로젝트에서 상태 누락 버그가 적음
    • 초보자 입장에서 직관적
      • 값이 바뀌면 알아서 화면 바뀜
  • 단점:
    • 성능 오버헤드
      • 매번 이벤트 감지 → 변경 감지 사이클 수행
    • 예측 불가성
      • “이 코드가 왜 렌더링을 트리거했지?”를 추적하기 어려움
    • 세밀한 제어가 어려움
      • 고빈도 이벤트(mousemove, scroll, resize)에서 과도한 감지 발생

3. 개선 방안

이러한 상황에서 장점은 최대한 활용하고 단점은 제거해야 합니다.
개선의 핵심은 이벤트를 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에서 화면 갱신');
  }
}

4. 결과

위와 같이 이벤트 처리 로직을 Zone.js의 감지 범위 밖에서 실행하도록 수정한 후
수정 전/후 CPU 사용률을 비교했습니다.

그 결과

마우스 이동 시 발생하던 불필요한 Change Detection 과정이 제거되면서
CPU 사용량이 약 20~30% 정도 감소하는 것을 확인할 수 있었습니다.

불필요했던 이벤트 → 전체 Change Detection 연쇄를 끊으면서 상당 부분 개선되었습니다.


5. 마치며

브라우저는 보통 60Hz로 그리므로 한 프레임 예산은 16.7ms입니다.
=> 한 프레임 시간 = 1초(1000) ÷ 초당 프레임 수(60) = 16.7
이 안에 JS 실행 + 스타일/레이아웃/페인트/합성이 모두 들어와야 부드럽게(60FPS) 보입니다.

Angular의 장점(자동 변경 감지)은 생산성을 주지만
이벤트 과다 상황에서 전역 CD가 빈번히 돌면 프레임 예산을 초과하기 쉽습니다.

그래서 “이 이벤트는 UI에 영향이 없다”과감히 Zone 밖으로 분리하고
UI 반영이 필요할 때만 명시적으로 Angular로 복귀하는 전략이
성능과 유지보수성 사이에서 가장 현실적인 균형점이었습니다.


6. 요약(한 줄 결론)

  • 현상: 마우스 이동만으로도 CPU가 튀는 상황 발생
  • 원인: @HostListener('document:mousemove')가 매 이벤트마다 Zone.js -> Change Detection(전체 컴포넌트 트리 순회, 이하 CD)를 유발
  • 개선: 이벤트 핸들링을 runOutsideAngular()로 옮겨 CD를 막고, 필요한 시점에만 Angular로 복귀
  • 결과: 수정 전/후 CPU 사용량 약 20~30% 감소 (동일 시나리오 측정 기준)

참고

profile
FRONTEND DEVELOPER

0개의 댓글