Angular Change Detection 성능 향상방법 (OnPush, ChangeDetectionRef)
Angular는 기본적으로 데이터가 단방향 (부모 -> 자식) 으로 흐르도록 설계되었을 거라 가정합니다. 즉 상위 컴포넌트가 변경한 데이터를 하위 컴포넌트에서는 반영만 하고 변경하지 않을 것이라 가정하기에 Change Detection 역시 상위에서 변경을 검사하고 전체적으로 내려가면서 검사를 하게 됩니다.
어떤 데이터가 변경되었을 때, 그 데이터를 사용하지 않는 하위 컴포넌트들에 대해서도 변경 감지가 이뤄집니다. 어플리케이션이 거대해질 수록 이런 변경감지에 따른 성능의 부하는 커지게 되므로, Angular는 컴포넌트와 하위 컴포넌트들이 변경감지 대상에서 제외될 수 있는 방법을 제공하고 있습니다.
이 글에서 Change Detection 성능 향상을 위해 ChangeDetectionStrategy의 OnPush에 대해 알아보고, 그 보다 더 좋은 방법인 ChangeDetectionRef에 대해 살펴보도록 하겠습니다.
ChangeDetectionStrategy
ChangeDetectionStrategy는 열거자로 OnPush와 Default가 있습니다.
Default는 변경감지 프로세스를 컴포넌트와 하위를 순회하도록 하는 것이고, OnPush는 컴포넌트에서 실제 레퍼런스가 변경되었는 지를 확인해서 변경 되었을 경우에만 하위 컴포넌트에게 변경감지 프로세스를 수행하라는 의미입니다.
OnPush 동작 방식을 이해하기 위해 3개의 컴포넌트를 준비 하겠습니다. 부모 컴포넌트와 자식 컴포넌트, 그리고 자식의 자식 컴포넌트를 준비합니다.
- 부모 컴포넌트
소스를 간단 설명하자면 button 클릭 시 change() 함수를 호출하지만 아무런 데이터의 변경은 없으며 <app-child [data]=”data”> 를 통해 child 컴포넌트에게 data를 전달하고 있습니다. 전달하는 data는 object로 단순히 key에 해당하는 value를 변경할 경우, 레퍼런스는 그대로 라는 것을 알고 있어야 합니다.
@Component({
selector: ‘app-root’,
template: `<button (click)="change()">change</button>
<br>
<app-child [data]="data"></app-child>`})export class AppComponent {
data = {name : 'seo'}
change() {}
}
2. 자식 컴포넌트
소스를 보면 부모 컴포넌트에게 받은 data를 childofchild 라는 자식 컴포넌트에게 전달합니다. 컴포넌트 class에서는 Input 데코레이터로 부모가 전달한 데이터를 저장하고 ngDoCheck에서는 동작원리를 위해 console.log를 추가했습니다.
@Component({
selector: 'app-child',
template: `<app-childofchild [data]="data"></app-childofchild>`
})
export class ChildComponent implements DoCheck {
@Input() data: object;
ngDoCheck() {
console.log('child ngDoCheck()')
}
}
3. 자식의 자식 컴포넌트
Input 데코레이터로 data를 전달받고 있으며, ngDoCheck를 구현하고 있습니다.
@Component({
selector: ‘app-childofchild’,
template: ``
})
export class ChildofchildComponent implements DoCheck {
@Input() data: object;
ngDoCheck() {
console.log('child of child do check')
}
}
부모 컴포넌트에서 버튼을 클릭하게 되면 change() 함수는 빈 함수로 데이터를 변경하지 않도록 하였습니다.
change() {}
위 코드를 실행하고 버튼을 3번 눌러보겠습니다.
우리가 자식 컴포넌트와 자식의 자식 컴포넌트에 구현한 ngDoCheck가 호출되는 시점은, 실제 데이터가 변경된 순간이 아니라 변경 감지가 수행된 시점입니다. 따라서 실제 데이터가 변경되지 않았음에도 자식 컴포넌트와 자식의자식 컴포넌트의 변경감지가 실행되었음을 확인할 수 있습니다.
ChangeDetectionStrategy.OnPush
그럼 이제 실제 데이터가 변경되었을 경우, 하위 컴포넌트에게 변경 감지 프로세스를 수행하도록 변경하겠습니다. 위에서 자식 컴포넌트의 코드를 아래와 같이 변경합니다.
@Component({
selector: 'app-child',
template: `<app-childofchild [data]="data"></app-childofchild>`,
changeDetection: ChangeDetectionStrategy.OnPush // 추가 된 코드
})
이 상태에서 실행하여 버튼을 클릭해보면 자식 컴포넌트에만 변경 감지가 수행되고 하위 컴포넌트들은 변경감지가 수행되지 않는것을 확인 할 수 있습니다.
그렇다면 부모 컴포넌트 change() 함수에서 실제 데이터를 변경해 보겠습니다.
export class AppComponent {
data = {name : 'seo'}
change() { this.data.name = this.data.name +'1'; }
}
결과가 예측되시나요? 위와 같이 데이터를 변경하면 자식의 자식컴포넌트는 변경 감지 프로세스를 수행하지 않습니다. 이유는 레퍼런스가 변경 될 경우 값이 변경되었다고 판단하는데, 객체에서 value를 변경하면 레퍼런스는 유지되기 때문입니다. 따라서 정상적으로 변경 감지 프로세스를 수행하기 위해 코드를 아래와 같이 변경합니다.
export class AppComponent {
data = {name : 'seo'}
change() { this.data = {name : this.data.name +'1'}}
}
위 코드는 data의 value를 변경하는 것이 아니라 새로운 레퍼런스를 가리키게 합니다. 따라서 자식 컴포넌트와 자식의 자식 컴포넌트에게도 변경 감지 프로세스가 정상적으로 수행됩니다.
ChangeDetectionStrategy의 OnPush 전략을 통해 컴포넌트의 모든 하위 컴포넌트에게 실제 데이터가 변경되었을 때만 변경 감지 프로세스를 수행시킬 수 있으며 하위 컴포넌트가 많아 질수록 성능 개선에 효과가 있습니다.
ChangeDetectionRef
OnPush 전략의 경우 컴포넌트의 데코레이터를 통해 설정하므로 프로그램 동작 중에 변경할 수 없습니다. ChangeDetectionRef는 변경감지를 세밀하게 할 수 있도록 Angular에서 지원하는 도구입니다. ChangeDetectionRef를 사용하여 변경 감지를 피하기 위해 자식 컴포넌트를 아래와 같이 변경하겠습니다.
@Component({
selector: 'app-child',
template: `{{data.name}}
<app-childofchild [data]="data"></app-childofchild>`
})
export class ChildComponent implements DoCheck {
@Input() data: object;
constructor(private cdr: ChangeDetectorRef) {
cdr.detach();
}
}
검은색으로 굵게 표시된 부분이 추가되었습니다. ChangeDetectionRef를 생성자에서 주입받아 detach 메소드를 실행하고 있습니다. detach 메소드를 호출한 컴포넌트와 하위 컴포넌트는 변경 감지 프로세스 트리에서 분리됩니다.
프로그램 실행 중 해당 컴포넌트와 하위 컴포넌트를 변경 감지 프로세스 트리에 추가하고 싶다면 reattach 메소드를 사용합니다. 위 자식 컴포넌트 소스를 아래와 같이 변경하겠습니다.
@Component({
selector: 'app-child',
template: `{{data.name}}
<button (click)="toggle()">
toggle change detection
</button>
<app-childofchild [data]="data"></app-childofchild>`
})
export class ChildComponent implements DoCheck {
@Input() data: object;
isDetach: boolean = true;
constructor(private cdr: ChangeDetectorRef) {
cdr.detach();
}
toggle() {
this.isDetach = !this.isDetach;
if(this.isDetach)
this.cdr.detach();
else
this.cdr.reattach();
}
}
자식 컴포넌트에 버튼이 추가되었고 클릭될 경우, 자식 컴포넌트를 변경 감지 프로세스 트리에 attach하거나 detach 하고 있습니다. 처음 실행화면에서 detach 상태이기 때문에 change 버튼을 클릭해도 화면에 반영이 되지 않습니다. 이 때 toggle change detection 버튼을 클릭하게 되면 attach 되면서 화면이 갱신됩니다.
위 화면은 change 버튼을 세번 클릭 한 상태로 아직 detach 메소드로 분리되어 있기 때문에 화면에 반영되지 않습니다. 이 상태에서 아래 toggle 버튼을 클릭하면 reattach 메소드에 의해 변경 감지 트리에 붙게되고 정상적으로 데이터가 보여집니다.
위에서 한 작업들은 컴포넌트를 변경 감지 프로세스 트리에서 분리하고 추가하는 방법을 알아봤습니다. ChangeDetectionRef를 통해 변경 감지를 수행할 수도 있습니다. 자식 컴포넌트를 아래와 같이 변경하겠습니다.
@Component({
selector: 'app-child',
template: `{{data.name}}
<button (click)="doDetect()">
toggle change detection
</button>
<app-childofchild [data]="data"></app-childofchild>`
})
export class ChildComponent implements DoCheck {
@Input() data: object;
isDetach: boolean = true;
constructor(private cdr: ChangeDetectorRef) {
cdr.detach();
}
toggle() {
this.isDetach = !this.isDetach;
if(this.isDetach)
this.cdr.detach();
else
this.cdr.reattach();
}
doDetect() {
this.cdr.detectChanges();
}
}
버튼을 클릭하면 doDetect 메소드를 실행시키며, 내부에서 detectChanges 메소드를 호출한다. 초기 실행화면에서 change 버튼 두번을 클릭했다. detach 메소드에 의해 분리되었으므로 화면에 반영되지 않는다.
아래 toggle change detection 버튼을 클릭하면 doDetect() 함수가 실행된다.
detectChanges 메소드에 의해 변경 감지가 발생하면서 데이터가 화면에 보여진다. 비록 변경 감지가 실행되었지만 여전히 컴포넌트는 분리된 상태이며 change 버튼을 클릭해도 화면에 반영되지 않는다. 물론 toggle 버튼을 클릭하면 다시 변경 감지가 발생하여 화면이 갱신된다.
간단 요약
- Angular의 변경 감지는 제일 상위부터 하위로 발생하므로 불 필요한 성능의 저하가 있을 수 있다.
- 변경 감지의 성능을 향상하기 위해 OnPush 전략과 ChangeDetectionRef를 사용할 수 있다.
- OnPush는 하위 컴포넌트가 많을 경우 유용하지만 동적으로 프로그램 실행 중에 변경될 수 없다.
- ChangeDetectionRef를 통해 변경 감지 프로세스 트리에서 분리하거나 붙이거나 강제로 변경 감지를 발생시킬 수 있다.