더 나은 사용자 경험을 위해 더 빠른 변경 감지를 구현합니다.
Angular는 데이터 모델 변경사항이 앱의 뷰에 반영되도록 변경 감지 메커니즘을 주기적으로 실행합니다. 변경 감지는 수동으로 또는 비동기식 이벤트 (예: 사용자 상호작용 또는 XHR 완료)를 통해 트리거될 수 있습니다.
변경 감지는 강력한 도구이지만 자주 실행할 경우 많은 계산을 트리거하고 기본 브라우저 스레드를 차단할 수 있습니다.
이 게시물에서는 애플리케이션의 일부를 건너뛰고 필요한 경우에만 변경 감지를 실행하여 변경 감지 메커니즘을 제어하고 최적화하는 방법을 알아봅니다.
Angular의 변경 감지 내부
Angular의 변경 감지 작동 방식을 이해하기 위해 샘플 앱을 살펴보겠습니다.
앱 코드는 이 GitHub 저장소에서 찾을 수 있습니다.
앱에는 회사 내 두 부서(영업 및 R&D)의 직원이 나열되며 다음 두 가지 구성요소가 있습니다.
AppComponent
: 앱의 루트 구성요소EmployeeListComponent
의 두 인스턴스(판매용 1개, R&D용 1개)
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 를 클릭하고 앱의 텍스트 상자 중 하나에 입력합니다. 몇 초 후에 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
변경 감지 전략을 도입하여 구성요소의 하위 트리를 프루닝합니다.- 프레임워크가 계산된 값의 캐싱을 수행할 수 있도록 과도한 계산을 순수 파이프로 이동합니다.