앱을 느리게 만드는 10가지 Angular 성능 실수를 하고 있나요?
Source: Dev.to
당신은 이 10가지 Angular 성능 실수를 저지르고 있나요?
앱이 느려지는 원인을 찾고 있다면, 아래 목록을 확인해 보세요. 대부분의 실수는 간단한 설정 변경이나 베스트 프랙티스를 적용함으로써 바로 해결할 수 있습니다.
1️⃣ Change Detection 전략을 OnPush 로 설정하지 않음
Angular는 기본적으로 Default 전략을 사용합니다. 이 경우 컴포넌트와 그 자식 모두가 매번 변경 감지를 수행하므로 불필요한 연산이 많이 발생합니다.
해결 방법
@Component({
selector: 'app-example',
templateUrl: './example.component.html',
changeDetection: ChangeDetectionStrategy.OnPush
})
export class ExampleComponent { }
OnPush 로 설정하면 입력 바인딩이 바뀔 때와 이벤트가 발생할 때만 감지가 트리거됩니다.
2️⃣ ngFor 에 trackBy 를 사용하지 않음
리스트가 재렌더링될 때 Angular는 전체 배열을 다시 비교합니다. 큰 리스트에서는 큰 비용이 됩니다.
해결 방법
<li *ngFor="let item of items; trackBy: trackById">
{{ item.name }}
</li>
trackById(index: number, item: any): number {
return item.id;
}
trackBy 를 사용하면 Angular가 객체의 식별자만 비교하므로 DOM 업데이트가 최소화됩니다.
3️⃣ 템플릿 안에서 무거운 연산 수행
템플릿 바인딩에 복잡한 함수 호출이나 연산을 넣으면 매 변경 감지마다 실행됩니다.
예시 (피해야 할 코드)
<div>{{ calculateHeavyStuff(item) }}</div>
해결 방법
- 계산 결과를 컴포넌트에서 미리 구하고, 템플릿에는 단순 바인딩만 남깁니다.
async파이프와 같은 pure pipe 를 활용합니다.
4️⃣ 불필요한 impure pipe 사용
pure: false 로 선언된 파이프는 매 감지 주기마다 실행됩니다.
해결 방법
가능하면 pure 파이프(기본값)를 사용하고, 꼭 필요할 때만 pure: false 로 설정합니다.
5️⃣ 모듈을 lazy‑load 하지 않음
앱 전체 번들에 모든 라우트와 컴포넌트를 포함하면 초기 로드 시간이 크게 늘어납니다.
해결 방법
const routes: Routes = [
{
path: 'feature',
loadChildren: () => import('./feature/feature.module')
.then(m => m.FeatureModule)
}
];
라우트가 실제로 요청될 때만 해당 모듈을 로드하도록 구성합니다.
6️⃣ AOT(Ahead‑Of‑Time) 컴파일을 사용하지 않음
JIT 모드에서는 브라우저가 런타임에 컴파일을 수행하므로 초기 로드가 느려집니다.
해결 방법
ng build --configuration production --aot
AOT 빌드는 컴파일 결과를 미리 생성해 번들 크기를 줄이고 실행 속도를 높입니다.
7️⃣ RxJS 구독을 해제하지 않음
구독을 남겨두면 메모리 누수와 불필요한 change detection이 발생합니다.
해결 방법
private destroy$ = new Subject<void>();
ngOnInit() {
this.someService.getData()
.pipe(takeUntil(this.destroy$))
.subscribe(data => this.data = data);
}
ngOnDestroy() {
this.destroy$.next();
this.destroy$.complete();
}
또는 async 파이프를 사용해 자동으로 구독을 관리합니다.
8️⃣ 큰 이미지와 비동기 로드를 무시
이미지 파일이 크면 네트워크 대역폭을 잡아먹고, 페이지 렌더링을 지연시킵니다.
해결 방법
- 이미지 압축 및 WebP 같은 최신 포맷 사용
loading="lazy"속성 추가- CDN을 활용해 전송 속도 최적화
9️⃣ CSS와 전역 스타일을 과도하게 사용
전역 스타일은 매 컴포넌트마다 재계산되므로 렌더링 비용이 증가합니다.
해결 방법
- 컴포넌트별 SCSS 혹은 CSS 모듈 사용
- 불필요한
* {}선택자를 피하고, BEM 같은 네이밍 규칙 적용
🔟 빌드 최적화 옵션을 무시
Angular CLI는 여러 최적화 플래그를 제공하지만, 기본 설정만으로는 충분하지 않을 수 있습니다.
추천 옵션
ng build --configuration production \
--optimization true \
--output-hashing all \
--source-map false \
--named-chunks false \
--extract-licenses false
위 옵션들은 코드 압축, 파일 해시, 소스맵 제거 등을 통해 최종 번들의 크기와 로드 시간을 크게 줄여줍니다.
마무리
위 10가지 실수를 점검하고 바로 적용한다면, Angular 애플리케이션의 반응 속도와 사용자 경험이 눈에 띄게 개선될 것입니다. 성능 최적화는 한 번에 끝나는 작업이 아니라 지속적인 프로파일링과 리팩터링이 필요합니다.
Tip: Chrome DevTools의 Performance 탭과 Angular DevTools 확장 프로그램을 활용해 병목 현상을 직접 확인해 보세요.
성공적인 최적화 여정을 응원합니다! 🚀
Overview
피크 사용량에서 Angular 앱이 지연되어 사용자를 좌절시키고 지표가 급락하는 상황을 상상해 보세요. 기본 변경 감지와 *ngFor 같은 구식 습관이 종종 원인입니다. 하지만 Angular 17+은 signals, zoneless apps, @for/@if 제어‑흐름, 그리고 Angular 21의 실험적 Signal Forms를 통해 더 깔끔한 반응성을 제공하며 이를 해결합니다.
이 가이드는 Angular 21+ 해결책을 적용한 상위 10가지 성능 실수를 다루며, @for/@if와 signal‑기반 form()을 활용한 코드 스니펫과 Angular DevTools와 같은 도구를 소개합니다. 성능 저하를 디버깅하는 초보자, signal로 확장하는 전문가, 그리고 최신 Angular가 유지율을 높이는 이유를 보고 싶은 이해관계자 모두에게 적합합니다.
1️⃣ 기본 변경 감지
Angular의 기본 변경 감지는 Zone.js를 통해 모든 이벤트마다 전역적으로 트리거되어 전체 컴포넌트 트리를 스캔하고, 깊은 계층 구조를 가진 복잡한 앱에서 CPU 사이클을 낭비합니다.
해결 방법:
- OnPush 전략과 함께 signals를 사용합니다.
- Signals는 세밀하고 zoneless 업데이트를 제공하여 변경된 서브 트리만을 대상으로 하여 불필요한 검사를 크게 줄입니다.
광범위한 스윕에서 정밀한 반응성으로의 전환은 대규모 애플리케이션의 성능을 크게 향상시킵니다.
2️⃣ OnPush 및 Immutable Data
기본 전략에 의존하면 변경되지 않은 컴포넌트라도 매 사이클마다 모든 컴포넌트를 검사하여 런타임 오버헤드가 증가합니다.
OnPush는 입력이 변하거나 이벤트가 발생하지 않는 한 손대지 않은 서브‑트리를 건너뛰지만, 불변 데이터를 요구합니다 – 객체를 변형하면 Angular가 참조 동등성(Object.is())을 사용하기 때문에 조용히 실패합니다.
신호와 결합하면 입력 변경 시 자동으로 더티 마킹이 이루어져 수동 조정이 필요 없습니다.
3️⃣ Manual Subscriptions vs. Async Pipe
컴포넌트에서 수동으로
.subscribe()를 사용하면 추가 상태 변수가 생성되어 전체 감지 사이클을 트리거하고, 메모리 누수와 중복 검사를 증가시킵니다.
Fix:
- async pipe를 사용하세요 – 자동으로 구독/구독 해제하고, 방출될 때만 OnPush 컴포넌트를 dirty 상태로 표시하며, Zone.js 패치를 사용하지 않고
toSignal()과 원활하게 통합되어 반응형 스트림을 처리합니다.
수동 구독을 없애고 파이프가 무거운 작업을 처리하도록 하여 코드를 더 간결하게 만드세요.
4️⃣ 계산된 신호 vs. 템플릿 워처
복잡한 템플릿에서 중첩된 속성 체인이 워처를 생성하여 지속적으로 재평가되며, 전역 실행 시 성능을 크게 저하시킵니다.
수정:
- computed signals를 사용하여 상태를 지연 평가하고—의존성이 변할 때만 재계산—불필요한 워처를 없애고 결과를 캐시하여 속도를 높입니다.
필터링된 목록, 합계 등을 메모이제이션하여 템플릿을 깔끔하게 유지합니다.
5️⃣ 예시: 사용자 리스트 컴포넌트 리팩터링
❌ 이전 – 기본 Change Detection & 수동 상태
@Component({ changeDetection: ChangeDetectionStrategy.Default })
export class UserList {
protected users: User[] = [];
protected loading = false;
ngOnInit() {
this.loading = true;
this.userService.getUsers().pipe(
take(1),
tap((users) => this.users = users),
finalize(() => this.loading.set(false))
).subscribe();
}
}
✅ 이후 – OnPush + Signals + @for/@if
import { ChangeDetectionStrategy, Component, computed, signal, input } from '@angular/core';
import { take, tap, finalize } from 'rxjs/operators';
@Component({
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
@if (loading()) { Loading... }
@for (user of filteredUsers(); track user.id) {
- {{ user.name }}
}
`
})
export class UserList {
/** Signal inputs auto‑track */
readonly users = input([]);
readonly filter = input('');
protected loading = signal(false);
protected filteredUsers = computed(() =>
this.users().filter(u => u.name.includes(this.filter()))
);
constructor(private userService: UserService) {
// Start loading immediately before subscription
this.loading.set(true);
this.userService.getUsers().pipe(
take(1), // Prevent unnecessary work if source is chatty
tap((users) => this.users.set(users)),
finalize(() => this.loading.set(false))
).subscribe();
}
}
결과: DevTools 프로파일러가 70 % 이상의 검사 감소를 보여줍니다—필터를 조정하거나 데이터를 로드할 때만 리스트가 업데이트됩니다. Signals + OnPush는 불필요한 사이클을 없애 앱을 더 빠르게 느끼게 합니다.
6️⃣ 동적 리스트 – @for vs. *ngFor
동적 리스트는 적절한 트래킹 없이 구식
*ngFor를 사용할 경우 매 업데이트마다 전체 DOM을 재생성하게 되어 대용량 데이터셋에서 성능 저하가 발생합니다.
- Angular 17은
@for구문을 도입했으며, 트랙 표현식을 반드시 지정하도록 요구합니다(느슨한 루프에 대한 내장 가드레일이라고 생각하면 됩니다). - 개발자들은 여전히
trackBy없이*ngFor에 집착해, 하나의 아이템을 추가하는 작은 변화에도 Angular가 전체 리스트를 해체하고 다시 구축하게 합니다. 벤치마크에 따르면 100 개 이상의 아이템 작업이 트래킹 없이 밀리초에서 초 단위로 급증합니다.
해결 방법:
@for (item of items; track item.id) {
- {{ item.name }}
}
- 임포트가 필요 없습니다. 커뮤니티 테스트에서는 대규모 리스트에서 약 90 % 빠른 차이점 계산을 보고했습니다.
7️⃣ 번들 부피 증가 – 트리 쉐이킹 및 예산
- 전체 라이브러리(예: Moment.js)를 트리‑쉐이킹 없이 가져오면
main.js크기가 500 KB를 초과해 초기 로드가 지연됩니다—특히 모바일에서 고통스럽습니다. - Angular의 esbuild(v17부터 기본) 는 죽은 코드를 적극적으로 제거하지만, production budgets 를
angular.json에 활성화 해야 부피 증가를 조기에 감지할 수 있습니다:
{
"projects": {
"your-app": {
"architect": {
"build": {
"configurations": {
"production": {
"budgets": [
{
"type": "initial",
"maximumWarning": "2mb",
"maximumError": "5mb"
}
]
}
}
}
}
}
}
}
- Gzip 또는 Brotli 로 번들을 80 % 압축하면 2 MB 파일을 훨씬 작은 크기로 줄일 수 있습니다.
- 무거운 모듈을 lazy‑load 하여 초기 번들을 가볍게 유지합니다:
import('./admin/admin.component').then(m => m.AdminComponent);
- 이는 엔터프라이즈 앱의 로드 시간을 50‑70 % 단축합니다.
ng generate @angular/core:control-flow를 실행해 루프를 마이그레이션한 뒤, source‑map‑explorer 로 번들을 분석하면 빠른 개선점을 찾을 수 있습니다.
9️⃣ Quick Demo – *ngFor → @for
1 000‑item 리스트에서
*ngFor="let item of items; trackBy: trackById"를@for (item of items; track item.id)로 교체합니다.
- DevTools 프로파일러가 재렌더링 시간이 **500 ms+**에서 50 ms 이하로 감소한 것을 보여줍니다.
- 이제 Angular는 변경된 항목만 스마트하게 추가/업데이트합니다.
🔟 정리되지 않은 RxJS 구독
정리되지 않은 RxJS 구독은 파괴된 컴포넌트를 메모리에 남겨 두어 단일 페이지 애플리케이션에서 메모리 누수를 일으키며, 시간이 지남에 따라 성능 저하를 초래합니다.
- 해결책: 자동으로 구독을 해제하는 async pipe(또는
toSignal)을 선호하거나,takeUntilDestroyed(Angular 17+)를 사용해 구독을 수동으로 관리합니다.
📈 요약
- Signals + OnPush = 세밀하고, 영역 없는 반응성.
@for/@if= 내장된 고성능 제어 흐름.- Async pipe /
toSignal= 안전하고 메모리 누수가 없는 구독. - Computed signals = 지연되고 메모이제이션된 상태.
- Lazy loading, budgets, and esbuild = 번들을 가볍게 유지.
이러한 패턴을 채택하면 Angular 앱이 더 빠르고, 확장성이 높으며, 미래에 대비하게 됩니다—사용자를 만족시키고 이해관계자를 감동시킵니다. 🚀
📉 Observable 정리 및 메모리 누수
Angular 16+에서는 컴포넌트가 파괴될 때 자동으로 Observable을 구독 해제하는 **takeUntilDestroyed()**를 도입해 대부분의 수동 정리를 없앴습니다. Signals는 구독 의존성을 더욱 줄이고, Angular v21+ 실험적 Signal Forms는 Observable 사용을 완전히 제거할 수 있습니다.
- Typical problem – 구독 해제를 잊으면 수동
ngOnDestroy로직을 강제하게 되며, 이는 오류가 발생하기 쉽습니다 (mergeMap같은 고차 연산자를 사용할 때 특히). - Modern fixes
// RxJS pipe with automatic cleanup
this.dataService.getItems()
.pipe(takeUntilDestroyed())
.subscribe();
// Convert to a signal (auto‑unsubscribes on destroy)
items = toSignal(this.dataService.getItems());
toSignal()는 컴포넌트 파괴 시 구독을 해제합니다.effect()는 내장된 정리 기능으로 부수 효과를 처리합니다.
📝 폼 및 변경 감지
[(ngModel)]이나 기존 리액티브 폼을 과도하게 사용하면 검증을 위한 무한한 변경 감지 사이클과 옵저버블 체인이 생성됩니다.
실험적인 form() API (Angular 21)로 전환하세요:
import { form, signalControl } from '@angular/forms';
export class MyComponent {
form = form({
email: signalControl('') // type‑safe, signal‑based control
});
}
- Benefits: 추론된 타입 안전성, 간단한 상태 관리, 구독 보일러플레이트 불필요, 대시보드나 복잡한 UI에서 메모리 오버헤드 감소.
🌐 직접 DOM 접근
document.querySelector()를 사용하면 Angular의 변경 감지를 우회하고 SSR/하이드레이션 안전성을 깨뜨립니다.
추천 접근 방식
import { viewChild, effect, isPlatformBrowser } from '@angular/core';
import { ElementRef } from '@angular/common';
export class MyComponent {
element = viewChild('myElement');
constructor() {
effect(() => {
if (isPlatformBrowser && this.element()) {
this.element()!.nativeElement.focus();
}
});
}
}
viewChild()는 타입‑안전하고 반응형 접근을 제공합니다.- SSR‑안전한 쿼리를 위해
isPlatformBrowser()와 함께 사용합니다.
🔀 RxJS vs. Signals for Derived State
RxJS 파이프를 사용해 파생 상태를 과도하게 엔지니어링하면 디버깅이 어려운 “스트림 얽힘”이 발생하고 메모리 사용량이 많아집니다.
UI 상태에는 signals를 선호하세요:
import { computed, effect } from '@angular/core';
export class CounterComponent {
count = signal(0);
doubleCount = computed(() => this.count() * 2);
// 구독 없이 부수 효과
logEffect = effect(() => console.log('Count:', this.count()));
}
- RxJS는 HTTP 스트림이나 복잡한 이벤트 파이프라인에 사용합니다.
- signals는 UI 상태 파생에 사용합니다 — 더 빠르고, 세밀한 업데이트가 가능하며, 구독이 필요 없습니다.
🚀 마이그레이션 경로
-
레거시 옵저버블 래핑
const items = toSignal(this.obs$, { initialValue: null });takeUntilDestroyed()와 결합하여 브리지 역할을 합니다. -
Signal 형태 채택하여
valueChanges옵저버블을 없앱니다. -
Angular DevTools로 검증 – 메모리 누수를 찾아내고 컴포넌트가 실제로 파괴‑클린인지 확인합니다.
⚡️ Angular 17+ 성능 향상
- 새
@for구문이 기존*ngFor를 대체합니다. - OnPush + Signals를 사용한 세밀한 변경 감지.
- **
@defer**는 무거운 컴포넌트의 지연 로드를 가능하게 합니다. - Signal Forms는 폼 처리를 간소화합니다.
이러한 개선을 통해 로드 시간이 **50 – 90 %**까지 단축되고 Core Web Vitals이 크게 향상됩니다.
업그레이드 단계
# Convert templates to the new control‑flow syntax
ng g @angular/core:control-flow
- 컴포넌트를 OnPush 변경 감지로 전환합니다.
- 반응형 업데이트를 위해 signals를 채택합니다.
- 지연 로드를 위해
@defer를 사용합니다. - 폼을 Signal Forms로 마이그레이션합니다.
Chrome DevTools 또는 Lighthouse로 영향을 측정합니다.
📚 추가 읽을거리
- Angular 성능 모범 사례
- Signals 가이드
- Zoneless 변경 감지 문서
이 리소스들은 Angular 앱을 더 빠르고 가볍게, 미래에 대비하도록 만들면서 Core Web Vitals를 향상시키는 방법을 보여줍니다.
📦 비즈니스를 위한 도구 및 템플릿
사용 가능한 랜딩 페이지, 자동화 스타터, 템플릿을 모아 빠르게 시작할 수 있도록 정리했습니다.
- ScaleSail.io – 툴킷을 확인해 보세요.
- LinkedIn – 실용적인 Angular 및 최신 프론트엔드 인사이트를 팔로우하세요.
- Twitter / X – 빠른 팁과 실제 프로젝트 이야기를 받아보세요.
여러분의 지원이 프론트엔드와 스타트업 커뮤니티를 위한 실용적인 가이드와 도구를 만들게 합니다. 함께 계속 만들어 나가요! 🚀