더 나은 사용자 경험을 위해 빠른 변경 감지를 구현합니다.
Angular는 변경 감지 메커니즘을 주기적으로 실행하여 데이터 모델의 변경사항이 앱의 뷰에 반영됩니다. 변경 감지는 수동으로 또는 비동기 이벤트 (예: 사용자 상호작용 또는 XHR 완료)를 통해 트리거될 수 있습니다.
변경 감지는 강력한 도구이지만, 매우 자주 실행하면 많은 계산을 실행하고 기본 브라우저 스레드를 차단할 수 있습니다.
이 게시물에서는 애플리케이션의 일부를 건너뛰고 필요할 때만 변경 감지를 실행하여 변경 감지 메커니즘을 제어하고 최적화하는 방법을 알아봅니다.
Angular의 변경 감지 내부
Angular의 변경 감지 작동 방식을 이해하기 위해 샘플 앱을 살펴보겠습니다.
앱의 코드는 이 GitHub 저장소에서 확인할 수 있습니다.
이 앱에는 회사의 두 부서(영업 및 R&D)에 속한 직원이 나열되며, 두 가지 구성요소가 있습니다.
- 앱의 루트 구성요소인
AppComponent
EmployeeListComponent
의 인스턴스 2개(영업용 및 R&D용)
AppComponent
템플릿에서 EmployeeListComponent
의 두 인스턴스를 확인할 수 있습니다.
<app-employee-list
[data]="salesList"
department="Sales"
(add)="add(salesList, $event)"
(remove)="remove(salesList, $event)"
></app-employee-list>
<app-employee-list
[data]="rndList"
department="R&D"
(add)="add(rndList, $event)"
(remove)="remove(rndList, $event)"
></app-employee-list>
각 직원에는 이름과 숫자 값이 있습니다. 앱이 직원의 숫자 값을 비즈니스 계산으로 전달하고 결과를 화면에 시각화합니다.
이제 EmployeeListComponent
를 살펴보겠습니다.
const fibonacci = (num: number): number => {
if (num === 1 || num === 2) {
return 1;
}
return fibonacci(num - 1) + fibonacci(num - 2);
};
@Component(...)
export class EmployeeListComponent {
@Input() data: EmployeeData[];
@Input() department: string;
@Output() remove = new EventEmitter<EmployeeData>();
@Output() add = new EventEmitter<string>();
label: string;
handleKey(event: any) {
if (event.keyCode === 13) {
this.add.emit(this.label);
this.label = '';
}
}
calculate(num: number) {
return fibonacci(num);
}
}
EmployeeListComponent
는 직원 목록과 부서 이름을 입력으로 허용합니다. 사용자가 직원을 삭제하거나 추가하려고 하면 구성요소가 상응하는 출력을 트리거합니다. 또한 이 구성요소는 비즈니스 계산을 구현하는 calculate
메서드를 정의합니다.
다음은 EmployeeListComponent
의 템플릿입니다.
<h1 title="Department">{{ department }}</h1>
<mat-form-field>
<input placeholder="Enter name here" matInput type="text" [(ngModel)]="label" (keydown)="handleKey($event)">
</mat-form-field>
<mat-list>
<mat-list-item *ngFor="let item of data">
<h3 matLine title="Name">
{{ item.label }}
</h3>
<md-chip title="Score" class="mat-chip mat-primary mat-chip-selected" color="primary" selected="true">
{{ calculate(item.num) }}
</md-chip>
</mat-list-item>
</mat-list>
이 코드는 목록의 모든 직원을 반복하고 각 직원에 대해 목록 항목을 렌더링합니다. 또한 입력과 EmployeeListComponent
에 선언된 label
속성 간의 양방향 데이터 결합을 위한 ngModel
지시문도 포함되어 있습니다.
앱은 EmployeeListComponent
의 두 인스턴스를 사용하여 다음과 같은 구성요소 트리를 구성합니다.
AppComponent
는 애플리케이션의 루트 구성요소입니다. 하위 구성요소는 EmployeeListComponent
의 두 인스턴스입니다. 각 인스턴스에는 부서의 개별 직원을 나타내는 항목 (E1, E2 등) 목록이 있습니다.
사용자가 EmployeeListComponent
의 입력 상자에 신입 직원의 이름을 입력하기 시작하면 Angular는 AppComponent
부터 전체 구성요소 트리의 변경 감지를 트리거합니다. 즉, 사용자가 텍스트를 입력하는 동안 Angular가 각 직원과 관련된 숫자 값을 반복적으로 다시 계산하여 마지막 확인 이후 변경되지 않았는지 확인합니다.
속도가 얼마나 느릴 수 있는지 확인하려면 StackBlitz에서 최적화되지 않은 버전의 프로젝트를 열고 직원 이름을 입력해 봅니다.
예시 프로젝트를 설정하고 Chrome DevTools의 성능 탭을 열어 fibonacci
함수로 인해 속도가 느려지는지 확인할 수 있습니다.
- `Control+Shift+J` (또는 Mac의 경우 `Command+Option+J`)를 눌러 DevTools를 엽니다.
- 실적 탭을 클릭합니다.
Performance 패널의 왼쪽 상단에 있는 Record 를 클릭하고 앱의 텍스트 상자 중 하나에 입력을 시작합니다. 몇 초 후 녹화 를 다시 클릭하여 녹화를 중지합니다. Chrome DevTools가 수집한 모든 프로파일링 데이터를 처리하면 다음과 같은 내용이 표시됩니다.
목록에 직원이 많은 경우 이 프로세스로 인해 브라우저의 UI 스레드가 차단되고 프레임 드롭이 발생하여 사용자 환경이 저하될 수 있습니다.
구성요소 하위 트리 건너뛰기
사용자가 영업 EmployeeListComponent
에 텍스트 입력을 입력할 때 R&D 부서의 데이터는 변경되지 않는다는 것을 알 수 있습니다. 따라서 구성요소에서 변경 감지를 실행할 이유가 없습니다. R&D 인스턴스가 변경 감지를 트리거하지 않도록 하려면 EmployeeListComponent
의 changeDetectionStrategy
를 OnPush
로 설정합니다.
import { ChangeDetectionStrategy, ... } from '@angular/core';
@Component({
selector: 'app-employee-list',
template: `...`,
changeDetection: ChangeDetectionStrategy.OnPush,
styleUrls: ['employee-list.component.css']
})
export class EmployeeListComponent {...}
이제 사용자가 텍스트를 입력하면 변경 감지가 해당 부서에 대해서만 트리거됩니다.
기존 애플리케이션에 적용된 최적화는 여기에서 확인할 수 있습니다.
OnPush
변경 감지 전략에 관한 자세한 내용은 공식 Angular 문서를 참고하세요.
이 최적화의 효과를 확인하려면 StackBlitz의 애플리케이션에 신입 사원을 입력합니다.
순수 파이프 사용
이제 EmployeeListComponent
의 변경 감지 전략이 OnPush
로 설정되어 있지만 Angular는 사용자가 해당 텍스트를 입력하면 부서 내 모든 직원의 숫자 값을 여전히 다시 계산합니다.
이 동작을 개선하려면 순수 파이프를 활용하면 됩니다. 순수 파이프와 불순 파이프 모두 입력을 허용하고 템플릿에서 사용할 수 있는 결과를 반환합니다. 둘의 차이점은 순수 파이프는 이전 호출에서 다른 입력을 받는 경우에만 결과를 다시 계산한다는 것입니다.
앱은 직원의 숫자 값을 기반으로 표시할 값을 계산하여 EmployeeListComponent
에 정의된 calculate
메서드를 호출합니다. 계산을 순수 파이프로 이동하면 Angular는 인수가 변경될 때만 파이프 식을 다시 계산합니다. 프레임워크는 참조 검사를 실행하여 파이프의 인수가 변경되었는지 확인합니다. 즉, 직원의 숫자 값이 업데이트되지 않으면 Angular는 재계산을 수행하지 않습니다.
다음은 비즈니스 계산을 CalculatePipe
라는 파이프로 이동하는 방법입니다.
import { Pipe, PipeTransform } from '@angular/core';
const fibonacci = (num: number): number => {
if (num === 1 || num === 2) {
return 1;
}
return fibonacci(num - 1) + fibonacci(num - 2);
};
@Pipe({
name: 'calculate'
})
export class CalculatePipe implements PipeTransform {
transform(val: number) {
return fibonacci(val);
}
}
파이프의 transform
메서드는 fibonacci
함수를 호출합니다. 파이프가 순수한 것을 볼 수 있습니다. Angular는 달리 지정하지 않는 한 모든 파이프를 순수한 것으로 간주합니다.
마지막으로 EmployeeListComponent
템플릿 내부의 표현식을 업데이트합니다.
<mat-chip-list>
<md-chip>
{{ item.num | calculate }}
</md-chip>
</mat-chip-list>
작업이 끝났습니다. 이제 사용자가 부서와 관련된 텍스트를 입력할 때 앱이 개별 직원의 숫자 값을 다시 계산하지 않습니다.
아래 앱에서 입력 과정이 얼마나 매끄러워졌는지 확인할 수 있습니다.
마지막 최적화의 효과를 확인하려면 StackBlitz에서 이 예를 사용해 보세요.
원래 애플리케이션의 순수 파이프 최적화 코드는 여기에서 확인할 수 있습니다.
결론
Angular 앱에서 런타임 속도 저하가 발생하는 경우:
- Chrome DevTools로 애플리케이션을 프로파일링하여 속도 저하의 원인을 확인하세요.
OnPush
변경 감지 전략을 도입하여 구성요소의 하위 트리를 프루닝합니다.- 프레임워크가 계산된 값을 캐싱할 수 있도록 과도한 계산을 순수한 파이프로 이동합니다.