티스토리 뷰


[번역] 아직도 NgZone이 단순하게 Angular의 변화감지(Change Detection)를 위해서만 필요하다고 생각하시나요?

부제 - NgZone의 구현방법과 사용법

본 포스팅은 Do you still think that NgZone (zone.js) is required for change detection in Angular? 을 번역한 글입니다.

많은 의역이 포함되어 있을 수 있습니다. :)

대부분의 포스팅에서 Zone(zone.js)와 NgZone이 Angular의 변화 감지Change detection과 강한 연관성이 있다고 말합니다. 분명 Angular의 변화 감지 기술에 Zone이 관련되어있는 것은 사실이지만, 기술적으로 부분 집합적인 관계는 아닙니다. 물론 ZoneNgZone은 비동기 오퍼레이션의 결과로 변화 감지Change Detection를 자동으로 발생시키는데 사용됩니다. 하지만 변화 감지는 별도의 메커니즘이기 때문에, ZoneNgZone 없이도 구현할 수 있습니다. 첫 번째 챕터에서, Zone 을 사용하지 않는 Angular를 설명하겠습니다. 그리고 이어지는 두 번째 챕터에서는, Angular와 Zone이 어떻게 NgZone을 통해 상호 작동하는지 알아보겠습니다. 마지막으로는 Google API Client Library(gapi)같은 일부 서드 파티 라이브러리에서 자동 변화 감지가 동작하지 않는 이유에 대해서 알아보겠습니다.

저는 Angular의 변화감지에 대해 깊이 알아보는 포스팅을 여러개 썼습니다. 그리고 이 글로 큰 그림을 완성합니다. 만약 여러분이 변화 감지가 어떻게 동작하는지에 대해 개괄적인 내용을 보고 싶으시다면, 여기(These 5 articles will make you an Angular Change Detection expert)에 나와있는 모든 글을 읽으시는 것을 추천드립니다.

Zone 없는 Angular

Zone 없이 Angular가 동작할 수 있다는 것을 설명하기 위해서, 저는 처음에는 아무 동작도 하지 않는 가짜 Zone(mock zone) 객체를 만드려고 했었습니다. 하지만 Angular 5 버전에서는 쉽게 Zone 없는 Angular를 사용할 수 있도록 해줍니다. Angular5에서는 아무 일도 하지 않는 noop zone 을 사용하는 방법을 제공합니다. 자, 그럼 Zone의 의존성을 제거하는 작업부터 해봅시다. 데모를 위해 stackblitz(웹IDE) 를 사용했습니다. 그리고 stackblitz가 Angular-CLI를 사용하기 때문에, polyfils.ts 파일에 있는 다음 import 를 제거합니다.

역자주: 스택블리츠를 사용하지 않고, 로컬에서 angular-cli를 사용하시면 똑같이 데모를 만들어 보실 수 있습니다. angular-cli의 사용법은 angular-cli를 이용한 Angular2 시작하기 (Quick Start) 를 참고해주세요. >_< (혼틈 셀프 홍보)

/* Zone JS is required by Angular itself. */

import 'zone.js/dist/zone';  // Included with Angular CLI.

Angular에서 noop zone을 사용하도록 설정하는 방법은 아래와 같습니다.

platformBrowserDynamic()
    .bootstrapModule(AppModule, {
        ngZone: 'noop'
    });

이 상태에서 애플리케이션을 실행하면, 변화 감지(Change Detection)이 완전히 동작해서, DOM에 있는 name 컴포넌트 속성을 랜더링 하는 것을 볼 수 있습니다.

이제, setTimeout 함수를 이용해서 속성을 변경하도록 해봅시다.

export class AppComponent  {
    name = 'Angular 4';

    constructor() {
        setTimeout(() => {
            this.name = 'updated';
        }, 1000);
    }

이번에는 변화가 업데이트가 되지 않는 것을 볼 수 있습니다. ngZone 을 사용하지 않기 때문에 그렇습니다. NgZone이 수행하는 자동 변화 감지가 일어나지 않는 것입니다. 그래도 변화감지를 수동으로 발생시킨다면, 이 코드는 잘 동작하게 됩니다. ApplicationRef 를 인젝션하여, tick 메소드를 발생시키면 됩니다.

export class AppComponent  {
    name = 'Angular 4';

    constructor(app: ApplicationRef) {
        setTimeout(()=>{
            this.name = 'updated';
            app.tick();
        }, 1000);
    }

이제 성공적으로 랜더링을 업데이트하는 것을 볼 수 있습니다.

이 파트를 요약하자면, 위 설명의 포인트는 ZoneNgZone이 변화 감지의 일부분이 전혀 아니라는 것입니다. 이들은 app.tick() 을 자동으로 호출하는 자동 변화 감지를 발생시키기 위한 매우 편리한 매커니즘일뿐입니다. 잠시 후에 이 매커니즘에 대해 알아보겠습니다.

NgZone이 Zone을 사용하는 방법

저의 Zone에 대한 이전 포스팅에서 Zone이 제공하는 API와 내부 동작에 대해 알아보았습니다. 여기서 저는 zone을 복제하는 핵심 개념과 각각의 존에서 태스크를 실행하는 것에 대해서 설명했습니다. 이 개념들이 여기서 언급됩니다.

또 저는 Zone이 제공하는 두 가지 능력에 대해서도 설명했습니다. 문맥 전파(Context propagation)에 대한 것과 중요한 비동기 태스크를 추적하는 것에 대한 것이었습니다. Angular는 태스크를 추적하는 매커니즘에 강하게 의존하는 NgZone 클래스를 구현합니다. NgZone 은 단지 자식 존을 복제하는 것에 대한 래퍼(wrapper)입니다.

function forkInnerZoneWithAngularBehavior(zone: NgZonePrivate) {
    zone._inner = zone._inner.fork({
        name: 'angular',
        ...

복제된 존은 _inner 속성에 저장됩니다. 그리고 보통 Angular zone으로써 참조합니다. 이 존은 NgZone.run() 을 실행 할 때, 사용되는 존입니다.

run(fn, applyThis, applyArgs) {
    return this._inner.run(fn, applyThis, applyArgs);
}

Angular zone을 복제한 순간에 현재 존은 _outer 속성에 저장됩니다. 그리고 이 존은 NgZone.runOutsideAngular() 메소드를 실행할 때, 사용되는 존입니다.

runOutsideAngular(fn) {
    return this._outer.run(fn);
}

이 메소드는 성능에 영향을 끼칠 수 있는 작업을 실행할 때 종종 사용됩니다. Angular zone의 밖에서 실행되게 함으로써, 변화감지가 발생하는 것을 피할 수 있습니다.

NgZone은 중요한 마이크로 태스크나 매크로 태스크가 있는지 여부를 알려주는 isStable이라는 속성을 가지고 있습니다. 또한 NgZone은 다음 4개의 이벤트를 정의합니다.

이벤트설명
onUnstableAngular Zone에 진입할 때 발생합니다. 이 이벤트는 작업 턴(VM Turn)의 처음 한 번만 발생합니다.
onMicrotaskEmpty현재 작업 큐에 더 이상 마이크로 태스크가 존재하지 않을 때 발생합니다. 이 이벤트는 Angular가 마이크로 태스크가 큐에 들어 온 변화 감지를 수행하는 것을 도와줍니다. 따라서 이 이벤트는 각 작업 턴(VM Turn)마다 여러 번 발생할 수 있습니다.
onStable마지막 onMicrotaskEmtpy 실행되어, 더 이상 마이크로 태스크가 존재하지 않을 때 발생합니다. 이것은 곧 작업 턴(VM Turn)을 종료할 것임을 의미합니다. 이 이벤트는 한 번만 발생합니다.
onError에러가 있을 때, 발생합니다.

Angular는 변화 감지를 자동으로 발생시키기 위해서 ApplicationRef 내부에서 onMicrotaskEmpty 이벤트를 사용합니다.

this._zone.onMicrotaskEmpty.subscribe(
    {next: () => { this._zone.run(() => { this.tick(); }); }});

아직 이전 섹션에서 배운게 기억난다면, tick() 메소드는 어플리케이션의 변화감지를 실행할 때 사용하는 메소드라고 알고 계실 겁니다.

NgZone이 onMicrotaskEmpty 이벤트를 구현하는 방법

자, 이제 NgZoneonMicrotaskEmpty 이벤트를 구현하는 방법에 대해서 알아보겠습니다. 이 이벤트는 checkStable 함수에서 발생합니다.

function checkStable(zone: NgZonePrivate) {
  if (zone._nesting == 0 && !zone.hasPendingMicrotasks && !zone.isStable) {
    try {
      zone._nesting++;
      zone.onMicrotaskEmpty.emit(null); // <----------- 바로 여기!!!!!!!!!!!!

그리고 이 함수는 일반적으로 다음 세 가지 존의 후킹 메소드에서 호출됩니다.

  • onHasTask
  • onInvokeTask
  • onInvoke

역자주: 저자의 이전 포스팅에서 설명한 내용입니다. 저 역시 번역한 글이 있습니다.

onHasTask는 microTask, macroTask, eventTask, change 라는 4개의 상태를 전달해줬습니다. 이 상태로 큐 안에 태스크가 존재하는지 확인할 수 있었지만, 단지 '큐가 비었는지, 아닌지'를 판단했기 때문에 하나의 작업턴에서 여러 개의 태스크가 실행되는 것을 알 수는 없었습니다.

onInvokeTask는 위 개별 태스크를 추적하지 못하는 단점을 보완할 수 있는 함수로써, setTimeout(callback) 같은 비동기 함수에서 인자로 넘겨받은 콜백함수가 실행될 때, 발생했습니다.

마지막으로 onInvoke는 존에 진입할 때, 이벤트가 발생하는 것이었습니다. '존에 진입한다' 라는 것은, z.run() 을 호출하거나 존 내부에서 특정 함수를 호출할 때 였습니다.

Zone에 대해 설명한 이전 포스팅에서 설명한 것처럼, 마지막 두 개의 훅은 마이크로태스크 큐에 변화가 있을 때 발생합니다. 따라서 Angular는 훅이 동작할 때마다, stable 검사를 수행해야만 합니다. 또한 onHasTask 훅도 전체의 큐가 변화하는 것을 추적하므로 검사를 수행할 수 있습니다.

흔한 함정

변화 감지에 관련되어 스택오버플로우에 올라오는 가장 빈번한 질문중 하나는, 왜 일부 서드파티 라이브러리를 사용할 때, 컴포넌트가 변화를 감지하지 못하는지 입니다. 예를 들어 이런 질문들이 있습니다. 이러한 문제에 대해 가장 일반적인 해결책은 Angular zone 내부에서 콜백을 실행하도록 하는 것입니다.

gapi.load('auth2', () => {
    zone.run(() => {
        ...

하지만, 왜 존이 훅 중 하나의 통지를 받는 요청을 등록하지 못하는지 궁금합니다. 그리고 NgZone 자동으로 변화감지를 발생시키지 못하는 것에 대한 것입니다.

이걸 이해하기위해서, 저는 gapi 의 minified된 소스를 파보았고, gapi가 네트워크 요청을 받기 위해 JSONP를 사용한다는 것을 발견했습니다. 이 접근법은 존에 의해 패치/추적되는 XMLHttpReqeustFetch API 같이 일반적인 Ajax를 사용하는 방법이 아닙니다. 대신에 URL을 가지는 script 태그를 생성하고, 서버로부터 가져오는 데이터를 담는 스크립트가 요청 될 때 발생하는 전역 콜백을 정의합니다. 이것은 존에 의해 패치되거나 탐지될 수 없습니다. 따라서 Angular는 이 기술을 사용한 요청에 대해 까맣게 잊어버리게 되는 것입니다.

여기에 gapi 최소화 버전에서 추출한 코드 스니펫이 있습니다.

Ja = function(a) {
    var b = L.createElement(Z);
    b.setAttribute(“src”, a);
    a = Ia();
    null !== a && b.setAttribute(“nonce”, a);
    b.async = “true”;
    (a = L.getElementsByTagName(Z)[0]) ? 
        a.parentNode.insertBefore(b, a) : 
        (L.head || L.body || L.documentElement).appendChild(b)
}

z 변수는 script 와 똑같고, 파라미터인 a 는 요청 URL을 가지고 있습니다.

https://apis.google.com/_.../cb=gapi.loaded_0

URL의 마지막 세그먼트는 gapi.loaded_0 이라는 전역 콜백입니다.

typeof gapi.loaded_0 
"function"

읽어주셔서 감사합니다. 이 포스티잉 좋았다면 박수를 눌러주시구요. 더 많은 이야기가 읽고 싶으시면, 제 TwitterMidium을 팔로우 해주세요.

참고



댓글
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
«   2024/05   »
1 2 3 4
5 6 7 8 9 10 11
12 13 14 15 16 17 18
19 20 21 22 23 24 25
26 27 28 29 30 31
글 보관함