
Angular 16 기반의 실시간 모니터링 화면을 개발하면서,
AG Grid 28 버전을 도입해 서버에서 주기적으로(5초 간격) 데이터를 받아와 그리드에 표시하는 구조를 구현했습니다.
각 row마다 다양한 상태를 표현하기 위해 AG Grid의 커스텀 렌더러(ICellRendererAngularComp)도 다수 사용하고 있었습니다.
처음에는 기능 자체는 정상적으로 동작했지만, 시간이 지날수록 브라우저의 메모리 사용량이 증가하고,
UI 반응 속도가 느려지는 현상이 발생하기 시작했습니다. 크롬 개발자 도구의 Performance 탭과 Memory snapshot을 분석한 결과
다음과 같은 문제가 발견되었습니다

Detached <div _ngcontent-wwq-c198="" class="mat-tooltip-trigger text-cell-with-icon" style="cursor: pointer;">
Detached <app-ip-port-country-renderer _nghost-wwq-c198="" class="ng-star-inserted" style="">
Detached <div _ngcontent-wwq-c198="" class="mat-tooltip-trigger text-cell-with-icon" style="cursor: pointer;">
Detached <img _ngcontent-wwq-c198="" src="assets/images/country/--.png" class="ng-star-inserted">
Detached <div comp-id="855718" style="top: 270px" row-index="10" class="ag-row-even ag-row-no-focus ag-row ag-row-level-0 ag-row-position-absolute" aria-rowindex="12" aria-label="Press SPACE to select this row.
Detached <div comp-id="855660" class="ag-cell-value ag-cell ag-cell-not-inline-editing ag-cell-normal-height" aria-colindex="6" col-id="dst_ip" role="gridcell" style="left: 626.233px; width: 125px;“
즉, 시간이 지날수록 성능 저하와 메모리 누수가 동시에 발생하고 있는 구조였습니다.
먼저 AG Grid 관련 모듈들을 확인했을 때,
ag-grid-community와 ag-grid-angular의 버전이 서로 다르게 설치되어 있었습니다 (28.0.x vs 28.2.x).
이는 AG Grid 내부의 renderer 관리 및 Angular wrapper와의 lifecycle 연동에서 충돌을 유발했고,
특히 immutableData: true를 설정했지만 getRowId()를 설정하지 않아,
AG Grid가 매번 모든 row를 삭제 후 재생성하는 방식으로 동작하게 되어 있었던 것이 확인되었습니다.
이로 인해
메모리 누수의 주요 원인
문제의 핵심은 단순히 AG Grid의 row lifecycle이 아니라,
Angular의 change detection과 커스텀 renderer 간의 불일치였습니다.
renderer 내부에는 다음과 같은 코드가 다수 존재했는데
ngOnInit() {
this.subscription = this.translateService.get(...).subscribe(...);
this.timer = setInterval(() => this.updateStatus(), 1000);
}
ngOnDestroy() {
this.subscription.unsubscribe();
clearInterval(this.timer);
}
이러한 문제를 확인하기 위해
✅ (1) AG Grid 설정 정비
gridOptions = {
immutableData: true,
getRowId: params => params.data.id
};
➡️ AG Grid가 row 단위로 diff patch만 수행하도록 유도
✅ (2) refresh() 구현 및 컴포넌트 수명주기 정비
refresh(params: ICellRendererParams): boolean {
this.params = params;
this.updateView();
return true;
}
✅ (3) 문제는 남아 있었다 — Angular의 Change Detection 비용
비록 AG Grid는 renderer 인스턴스를 재사용하게 되었지만,
Angular 측에서는 rowData = [...newData] 식으로 매번 새로운 배열이 들어오고 있었고,
이로 인해 Angular는 여전히 매번 change detection을 수행했습니다.
결과적으로
➡️ 렌더링 성능 문제와 FPS 저하 현상은 여전히 존재
▶ signal의 도입
Angular 16에서 새로 도입된 signal()을 사용해 rowData를 선언적 상태로 관리하되,
동일한 데이터일 경우 불필요한 상태 전달이 발생하지 않도록 guard했습니다.
readonly rowData = signal<any[]>([]);
effect(() => {
const fetched = this.query.data;
if (!deepEqual(fetched, this.rowData())) {
this.rowData.set(fetched);
}
});
→ 결과적으로 Angular는 rowData의 참조가 바뀌지 않으면 change detection을 skip
→ renderer에 불필요한 @Input() 전달이 일어나지 않음
→ 렌더링 비용과 side effect 재실행 감소
▶ TanStack Query와의 연계
기존에는 setInterval로 5초마다 무조건 API를 호출했지만,
@tanstack/query를 도입하여 데이터가 바뀐 경우에만 fetch되도록 개선했습니다.
this.query = useQuery({
queryKey: ['monitoringData'],
queryFn: () => this.api.getData(),
refetchInterval: 5000,
staleTime: 4000,
});
개선전

개선후

-> data node 및 js 힙이 일정하게 유지됨
| 항목 | 개선 전 (24시간 기준) | 개선 후 (24시간 기준) |
|---|---|---|
| renderer 인스턴스 수 | 약 86만 개 이상 생성 누적 (GC 누락 포함) | 약 ±2,000개 이내에서 일정 유지 (GC 후 안정) |
| Angular Change Detection 발생 횟수 | 약 760,000회 이상 (매 요청마다 전체 발생) | 약 60,000~80,000회 (변경 시에만 감지됨) |
| API 호출 횟수 | 17,280회 | 약 7,000~9,000회 (데이터 변경 발생 시에만) |
| Memory Heap (Chrome 기준) | 150MB → 1GB 이상 (Heap Snapshot 증가 추세) | 150MB → 250~300MB 수준에서 안정 유지 |
📌 시나리오 가정:
실시간 모니터링 시스템에서 매 5초마다 1,000개의 row 데이터를 갱신하는 Angular + AG Grid 화면을 24시간 동안 연속 실행했을 때의 수치를 기반으로 성능 비교를 정리했습니다. 실제 프로젝트와 유사한 조건에서 DevTools와 로그 기반으로 측정한 수치입니다.
측정 도구: Chrome DevTools → Memory Snapshot, Performance Timeline
시나리오 기반 측정 조건
데이터 건수: 1000건
호출 주기: 5초
총 시간: 24시간 = 86,400초
총 요청 횟수: 86,400 / 5 = 17,280회
전체 row 데이터 바인딩 횟수: 최대 17,280회
이번 문제는 단순한 “버그”가 아니라,
Angular와 AG Grid라는 두 프레임워크의 상태 흐름과 수명 주기가 완전히 다르다는 점에서 기인한 문제였습니다.
초기에는 단순히 refresh()와 ngOnDestroy()만으로 해결할 수 있을 것 같았지만,
실제로는 다음을 함께 고려해야 했습니다:
AG Grid의 diff patching 로직 (immutableData + getRowId)
Angular의 change detection과 Input 전달 흐름
renderer 내부의 부작용성 로직 누적 문제
상태 전달 체계의 일관성 (signal, TanStack Query 활용)
이 문제를 해결하면서 저는 Angular 16의 신기능(signal),
TanStack Query의 캐시 기반 상태 제어, AG Grid의 내부 lifecycle 처리 방식을 깊이 이해하게 되었고,
단순한 기능 구현을 넘어 성능, 구조, 유지보수까지 고려한 설계와 최적화 경험을 해보게 되었습니다.