이 글은 Thoughtram 블로그의 글을 번역한 것입니다. 번역이 미흡하더라도 너그럽게 이해해주세요.

만일 이 이야기에 흥미가 있다면, 이 슬라이드를 보거나 유튜브 비디오를 봐도 됩니다.

What’s Change Detection anyways?

변화 감지(change detection)의 기본적인 태스크는 프로그램 내부의 상태(stats)를 가져와서 그것을 사용자 인터페이스에 구현하는 것입니다. 이 상태는 objects, arrays, primitives등 모든 종류의 자바스크립트 데이터 구조일 것입니다.

이 stats는 사용자 인터페이스나 DOM 웹사이트의 특정부분의 paragraphs, forms, 링크 또는 버튼 에서 보여질 것입니다. 따라서 기본적으로 우리는 데이터 구조를 입력받아 DOM 출력을 생성하여 사용자가 화면에서 볼 수 있도록 만듭니다. 우리는 이 프로세스를 rendering이라고 부르죠.

하지만, 그것이 런타임에 변경이 발생하거나, DOM이 이미 랜더링된 얼마 후 일 경우에 처리가 까다로워집니다. Model에서 무엇이 바뀌 었는지, 그리고 DOM을 어디에서 업데이트해야하는지 어떻게 알 수 있을까요? DOM 트리에 액세스하는 것은 항상 비용이 많이 들기 때문에 업데이트가 필요한 위치를 알아야 하고, 액세스를 가능한 한 작게 유지해야 합니다.

이 이슈는 여러 가지 방법으로 해결할 수 있습니다. 한 가지 방법은 단순히 HTTP 요청을 하고 전체 페이지를 다시 렌더링하는 것입니다. 또 다른 접근법은 새로운 상태의 DOM을 이전 상태와 구분하여 차이점 만 렌더링하는 것입니다. ReactJS가 가상 DOM을 사용하여 수행하는 것처럼..

이와 관련해서 Tero는 Change and its detection in JavaScript frameworks라는 훌륭한 글을 썼습니다. 만일 당신이 프레임워크들이 어떻게 이 이슈를 해결하는지 좀 더 흥미가 있다면 이 글을 읽어보기를 추천합니다. 본 글에서는 Angular2 이상 버전에 대해서만 집중합니다.

기본적으로 변경 감지의 목표는 항상 데이터와 그 변화를 투영(projecting)하는 것입니다.

무엇이 변화를 일으키는가?

자, 이제 우리는 변화 탐지가 무엇인지 알게되었으므로 정확히 언제 그러한 변화가 일어날 수 있는지 알아보겠습니다. Angular는 View를 업데이트 해야한다는 것을 언제 알 수 있을까요?
undefined

앵귤러 component를 처음 보는 경우 탭 component 작성에 대한 도움말을 읽는 것이 좋습니다.

위의 component는 두 개의 속성(property)을 표시하고 템플릿의 button을 클릭 할 때 변경하는 메서드를 제공합니다. 이 특정 단추를 클릭하는 순간 application 상태(state)가 변경되면 component의 속성이 변경되기 때문입니다. 그 순간에 우리는 view를 업데이트하려고 합니다.

여기에 다른 코드가 있습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
@Component()
class ContactsApp implements OnInit{
contacts:Contact[] = [];
constructor(private http: Http) {}
ngOnInit() {
this.http.get('/contacts')
.map(res => res.json())
.subscribe(contacts => this.contacts = contacts);
}
}

이 component는 컨텐츠 목록을 가지고 있고, 그것이 초기화 되었을때 http 요청을 수행합니다. 이 요청이 다시 들어 오면 목록이 업데이트됩니다. 다시, 이 시점에서 application 상태가 변경되었으므로 view를 업데이트하려고 합니다.

기본적으로 application 상태 변화는 3가지에 의해 발생됩니다.

  • Events - click, submit, …
  • XHR - 원격 데이터로부터 데이터를 가져오는 것
  • Timers - setTimeout(), setInterval()

이것들은 모두 비동기입니다. 따라서 기본적으로 일부 비동기 작업이 수행 될 때마다 application 상태가 변경될 수 있다는 결론을 얻습니다. 이것은 누군가가 Angular에게 view를 업데이트 하라고 말해 줄 필요가 있을 때입니다.

Angular에게 알리는 것은 무엇일까요?

자, 이제 응용 프로그램 상태 변경의 원인을 알았습니다. 그러나 이 특정 순간에 view가 업데이트되어야 한다고 Angular에게 알리는 것은 무엇일까요?

Angular는 우리에게 native API(web의 경우 addEventListener 같은…)를 직접 사용하는 것을 허용합니다. Angular가 DOM을 업데이트 하는 알림을 받기 위해서, 우리가 호출 해야만하는 어떠한 가로채는(interceptor) methods도 없습니다. 이것은 마술일까요?

만일 최신 버젼의 글을 읽었다면, 당신은 이것을 처리하는 Zones을 알것입니다. 실제로, Angular는 NgZone이라는 자체 영역(zone)을 제공합니다. 우리는 Zones in Angular라는 글을 기록 했었습니다.

Angular 소스 코드에서 NgZones, onTurnDone 이벤트를 수신하는 ApplicationRef라는 것이 있습니다. 이 이벤트가 발생하면, 본질적으로 변경 검출을 실시하는 tick() 함수를 실행합니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
// 실제 Angular 코드의 매우 짧은 버젼
class ApplicationRef {
changeDetectorRefs:ChangeDetectorRef[] = [];
constructor(private zone: NgZone) {
this.zone.onTurnDone.subscribe(() => this.zone.run(() => this.tick());
}
tick() {
this.changeDetectorRefs
.forEach((ref) => ref.detectChanges());
}
}

Change Detection

좋습니다. 이제 변경 감지가 트리거되는 시점을 알 수 있지만 어떻게 수행될까요? Angular에서는 각 component마다 고유 한 변경 감지기가 있습니다.

이는 중요한 사실입니다. 이는 각 component에 대해 변경 감지가 수행되는 방법과시기를 개별적으로 제어 할 수 있기 때문입니다!

component 트리의 어딘가에 이벤트가 발생했다고 가정 해 봅시다. 버튼이 클릭되었을 수도 있습니다. 다음에 어떻게 될까요? 영역이 주어진 핸들러를 실행하고 턴이 완료되면 Angular에 알리고, 결국 Angular가 변경 감지를 수행한다는 것을 알았습니다.

각 component에는 고유 한 변경 감지기가 있고, Angular application은 component 트리로 구성되므로 결론적으로 변경 감지기 트리도 있다는 것입니다. 이 트리는 데이터가 항상 위에서 아래로 흐르는 방향 그래프로 볼 수도 있습니다.

데이터가 위에서 아래로 흐르는 이유는 root component부터 모든 단일 component에 대해 항상 변경 감지가 항상 위에서 아래로 수행되기 때문입니다. 단방향 데이터 흐름이 주기(cycles)보다 예측 가능하기 때문에 매우 좋습니다. 우리는 뷰에서 사용하는 데이터의 출처를 항상 알고 있습니다. 이는 해당 component에서만 발생할 수 있기 때문입니다.

또 다른 흥미로운 점은 변경 감지가 단일 패스(single pass) 후에 안정화된다는 것입니다. 즉, component 중 하나가 변경 감지 중에 첫 번째 실행 후 추가적인 부작용이 발생하면 Angular는 오류를 발생시킵니다.

Performance

기본적으로 이벤트가 발생할 때마다 모든 단일 component를 검사해야 하더라도 Angular는 매우 빠릅니다. 몇 milliseconds 내에 수십만 개의 점검을 수행 할 수 있습니다. 이것은 Angular가 VM 친화적인 코드(VM friendly code)를 생성하기 때문에 가능합니다.

무슨 뜻일까요? 각 component마다 고유 한 변경 감지기가 있다는 것은, 각 개별 component의 변경 감지를 처리하는 Angular의 하나의 포괄적인 처리기(generic thing)가 있다는 것과 같은 의미가 아닙니다.

그 이유는 변경 감지기가 동적 방식으로 작성되어야 하기 때문에 모델 구조가 어떻든간에 모든 component를 확인할 수 있기 때문입니다. VM은 최적화 할 수 없다는 이유로 이런 종류의 동적 코드를 좋아하지 않습니다. 객체의 모양이 항상 동일하지는 않기 때문에 다형성(polymorphic)으로 간주됩니다.

Angular는 component 모델의 모양이 무엇인지 정확하게 알고 있기 때문에, 단일 component인 각 component의 런타임에 변경 감지기 클래스를 만듭니다. VM은이 코드를 완벽하게 최적화 할 수 있으므로 실행 속도가 매우 빠릅니다. 좋은 점은 Angular가 자동으로 처리하므로 너무 많이 신경 쓰지 않아도 된다는 것입니다.

Smarter Change Detection

다시 말하지만, Angular는 이벤트가 발생할 때마다 모든 component를 확인해야합니다. 애플리케이션 상태가 변경되었을 수 있기 때문입니다. 그러나 Angular가 상태를 변경 한 application의 부분에 대해서만 변경 감지를 실행하도록 말할 수 있다면 좋지 않을까요?

그렇습니다. 할 수 있습니다! 무언가가 바뀌 었는지 여부에 대한 보장을 제공하는 데이터 구조(data structures)가 있습니다.

  • Immutables and Observables 이러한 structure 나 type을 사용하게되면 Angular에 알려줌으로써 변경 감지가 훨씬 더 빠를 수 있습니다. 그럼 어떻게 해야 할까요?

Understanding Mutability

이유 및 방법을 이해하기 위해 불변의 데이터 구조(immutable data structures)가 도움이된다면, 우리는 변경 가능성(mutability)의 의미를 이해할 필요가 있습니다. 다음과 같은 component가 있다고 가정합니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@Component({
template: '<v-card [vData]="vData"></v-card>'
})
class VCardApp {
constructor() {
this.vData = {
name: 'Christoph Burgdorf',
email: 'christoph@thoughtram.io'
}
}
changeData() {
this.vData.name = 'Pascal Precht';
}
}

VCardApp<v-card>를 하위 component로 사용하며 이 component에는 vData 입력 속성이 있습니다. VCardAppvData 속성을 사용하여 해당 component로 데이터를 전달합니다. vData는 두 가지 속성을 가진 객체입니다. 또한 vData의 이름을 변경하는 changeData() 메서드가 있습니다. 여기에 별다른 신기한 점은 없습니다.

중요한 부분은 changeData()name 속성을 변경하여 vData를 변경한다는 점입니다. 해당 속성이 변경 되더라도 vData 참조 자체는 그대로 유지됩니다.

어떤 이벤트로 인해 changeData()가 실행된다고 가정하고, 변경 감지가 수행 될 때 어떤 일이 발생할까요? 먼저 vData.name이 변경된 다음 <v-card>로 전달됩니다. <v-card>의 변경 감지기는 이제 vData가 이전과 여전히 동일한지 확인합니다. 참조(reference)는 변경되지 않았습니다. 그러나 name 속성이 변경되었으므로 Angular는 해당 객체에 대한 변경 감지를 수행합니다.

자바 스크립트에서는 객체가 기본적으로 변경 가능하기 때문에 (프리미티브(primitives) 제외) Angular는 보수적이어야하며 이벤트가 발생할 때마다 모든 component에 대해 변경 감지를 실행해야합니다.

다음은 불변의(immutable) 데이터 구조가 작동하는 곳입니다.

Immutable Objects

변경할 수 없는 객체는 객체 불변을 보장해 줍니다. 즉, 불변 객체를 사용하고 그러한 객체를 변경하고자 할 때, 원래 객체가 변경되지 않기 때문에 우리는 항상 그 변경으로 새로운 참조를 얻습니다.

1
2
3
4
5
6
7
var vData = someAPIForImmutables.create({
name: 'Pascal Precht'
});
var vData2 = vData.set('name', 'Christoph Burgdorf');
vData === vData2 // false

someAPIForImmutables는 변경 불가능한 데이터 구조에 사용하려는 모든 API가 될 수 있습니다. 그러나 예제에서 볼 수 있듯이 단순히 name 속성을 변경할 수는 없습니다. 우리는 그 특별한 변화를 가진 새로운 객체를 얻게 될 것이고 이 객체는 새로운 참조를 가지고 있습니다.

점검 횟수 줄이기(Reducing the number of checks)

Angular는 입력 속성이 변경되지 않을 때 전체 변경 감지 하위 트리를 건너 뛸 수 있습니다. 우리는 방금 “변화”가 “새로운 참조”를 의미한다는 것을 배웠습니다. Angular 앱에서 불변 객체를 사용하는 경우, 입력 값이 변경되지 않은 경우 component가 변경 감지를 건너 뛸 수 있다고 Angular에 알려주면됩니다.

<v-card>를 통해 어떻게 작동하는지 봅시다.
undefined
보시다시피, VCardCmp는 입력 속성에만 의존합니다. 좋습니다. 변경 감지 전략을 다음과 같이 OnPush로 설정하여 입력이 변경되지 않으면 이 component의 하위 트리에 대한 변경 감지를 건너 뛰도록 Angular에 지시 할 수 있습니다.
undefined
이게 전부입니다! 이제 더 큰 component 트리를 상상해보십시오. 불변 객체를 사용하고, Angular에 적절하게 정보가 전달되면 전체 하위 트리를 건너 뛸 수 있습니다.

Observables

이전에 언급 한 바와 같이 Observables는 변경 사항이 언제 발생했는지 확실하게 보장합니다. 불변 객체와는 달리, 변경 사항이있을 때 새로운 참조(references)를 제공하지 않습니다.
대신, Observables은 그들에게 반응하기 위해 우리가 구독 할 수 있는 이벤트를 제공합니다.

만일 우리가 Observables을 사용하고, 우리가 변경 감지 하위트리를 건너뛰기 위해서 OnPush를 사용하기를 원한다 할 때, 이러한 객체의 참조가 변경되지 않으면 어떻게 처리해야 할까요? Angular는 component 트리의 경로에서 특정 이벤트를 검사 할 수 있는 매우 현명한 방법을 제공합니다.

이것이 의미하는 것을 이해하기 위해 아래의 component를 살펴 보겠습니다.
undefined

장바구니가있는 e-commerce 애플리케이션을 구축한다고 가정 해 보겠습니다. 사용자가 장바구니에 제품을 올릴 때마다 우리는 UI에 작은 카운터를 표시하여 카트의 제품 수량을 볼 수 있습니다.

CartBadgeCmp의 역할이 그것입니다. 제품에는 장바구니에 제품이 추가 될 때마다 실행되는 이벤트 스트림 인 counter 와 입력 속성 addItemStream이 있습니다.

우리는 이 글에서 observables이 어떻게 작동하는지에 대해 자세히 설명하지 않을 것입니다. observables에 대해 더 자세히 알고 싶다면 Observables in Angular를 활용하는 방법에 대한 글를 읽어보세요.

또한 변경 검색 전략을 OnPush로 설정하므로, 오직 component의 입력 속성이 변경될 때만 변경 감지가 수행됩니다.

그러나 앞에서 언급했듯이 addItemStream의 참조는 변경되지 않으므로 이 component의 하위 트리에 대한 변경 감지가 수행되지 않습니다. component가 ngOnInit 라이프 사이클 hook에서 해당 스트림을 subscribe하고 카운터를 증가시키기 때문에 이는 문제가됩니다. 이것은 응용 프로그램 상태 변경이며 이를 반영하고 싶습니다.

변경 탐지기 트리가 어떻게 생겼는지 (모든 것을 OnPush로 설정했습니다) 이벤트가 발생하면 변경 감지가 수행되지 않습니다.

이 변화에 대해 Angular에게 어떻게 알릴 수 있습니까? 전체 트리가 OnPush로 설정된 경우에도이 component에 대해 변경 감지를 수행해야한다는 Angular를 어떻게 알 수 있습니까?

걱정 마세요. Angular는 우리에게 적용됩니다. 앞서 학습 한 것처럼 변경 감지는 항상 위에서 아래로 수행됩니다. 따라서 우리는 변경이 발생한 component에 대한 트리의 전체 경로에 대한 변경을 감지하는 방법이 필요합니다. Angular는 어느 경로인지 알 수 없지만 우리는 알수 있습니다.

우리는 markForCheck()라는 API와 함께 제공되는 의존성 삽입을 통해 컴포넌트의 ChangeDetectorRef에 액세스 할 수 있습니다. 이 방법은 우리가 필요로하는 것을 정확히 수행합니다! 다음 변경 감지 실행을 위해 루트까지 component의 경로를 표시합니다.

1
constructor(private cd: ChangeDetectorRef) {}

그런 다음 Angular에 이 component의 경로를 확인할 루트까지 표시하도록 지시합니다.

1
2
3
4
5
6
7
ngOnInit() {
this.addItemStream.subscribe(() => {
this.counter++; // application state changed
this.cd.markForCheck(); // marks path
})
}
}

다됐습니다! observable 이벤트가 시작된 후, 변경 감지 시작 전의 모습은 다음과 같습니다.

이제 변경 감지가 수행되면 단순히 위에서 아래로 이동합니다.

멋지죠? 변경 감지 실행이 끝나면 전체 트리의 OnPush 상태를 복원합니다.