티스토리 뷰


[번역] Zones(zone.js)를 리버스 엔지니어링해서 찾은 것

본 포스팅은 I reverse-engineered Zones (zone.js) and here is what I’ve found 글을 번역한 글입니다.

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

부제 - Zones이란? (Zones은 무엇이며 또 어떻게 사용할까?)

이 포스팅은 NgZone에 대한 포스팅이 아니지만, NgZone의 메커니즘은 Zones(zone.js)을 이용하기 때문에 비슷할 수 있습니다. 이 포스팅에서는 NgZone을 직접 만들 수 있는 방법과 Angular의 NgZone은 어떻게 동작하는지에 대해서 설명합니다. NgZone에 대해서 더 많이 공부하시려면 "당신은 아직도 NgZone은 Angular의 Change Detection을 위해서 필요한 것이라고 생각하시나요?" 라는 포스팅을 읽어보세요.

역자주: 위 포스팅은 [번역] 아직도 NgZone이 단순하게 Angular의 변화감지(Change Detection)를 위해서만 필요하다고 생각하시나요? 에서 번역되었습니다.

Zones은 논리적으로 연결된 여러 개의 비동기 오퍼레이션의 작업을 쉽게 할 수 잇도록 도와주는 새로운 메커니즘입니다. Zones은 각각의 비동기 오퍼레이션을 존(실행영역)에 연결시키는 작업을 합니다. 개발자는 이 바인딩으로 다음의 이점을 누릴 수 있습니다.

  • 다른 언어에서 쓰레드(Thread)의 개념과 유사한 '존'에 연결된 데이터들은, 같은 존안에서는 어떤 비동기 오퍼레이션에서도 접근할 수 있게 됩니다. (데이터 접근성)
  • 클린업이나, 랜더링, 테스트 검증 작업을 수행하기 위해 존안에서 실행된 특징적인 비동기 오퍼레이션을 자동으로 추적합니다. (자동 추적)
  • 존안에서 실행된 전체 시간을 측정할 수 있습니다. 이는 성능 프로파일링에 도움이 됩니다. (쉬운 프로파일링)
  • 최상위 영역에 예외들을 전달하지 않아도, 존 안에서 예상하지 못한 모든 예외(uncaught exception)와 처리하지 못한 Promise의 예외(rejection)들을 다룰 수 있습니다. (예외 처리)

웹에 공개되어 있는 대부분의 포스팅들은 오래된 API를 설명하거나, Zones에 대해서 상당히 단순하게 설명합니다. 따라서 본 포스팅에서는 최신 API와 중요한 API들에 대해 매우 자세하게, 그리고 가능한 구현 레벨에 가깝게 설명할 것입니다. 먼저 API를 설명하고, 비동기 작업 연결 메커니즘을 보여주고, 그 다음으로 위에 언급된 작업(Task)들을 개발자가 사용할 수 있도록 하는 인터셉트 후킹 기법을 설명하겠습니다. 그리고 마지막으로 Zones의 핵심 엔진이 작동하는 방법에 대해 짧게 설명하도록 하겠습니다.

Zones은 현재 Node의 반대로 인해 ECMAScript 표준의 0단계에서 제안 이 머무르고 있는 상태입니다. Zones은 흔히 Zone.js라고 언급되는데, GitHub 저장소NPM 패키지Zone.js라고 이름을 사용하고 있기 때문입니다. 하지만 본 포스팅에서는 스펙에 명시된것처럼 Zone으로 명시하도록 하겠습니다.

역자주: Node의 반대 사유를 보면, 에러처리에 대한 논의가 있는 것 같습니다. 위에 언급한것처럼 존을 사용하면 Uncaught Exception같은 처리되지 않은 예외에 대해서 존에 후킹을 걸어 처리할 수 있게 됩니다. 하지만 이는 Node.js에서 제공하는 process.on('uncaughtException') 라는 프로세스 차원의 예외처리 이벤트와 약간의 모순이 있는 것 같습니다.

Zone API에 관하여

첫 번째로 Zones이 동작하는 것과 관련된 메소드들을 살펴보겠습니다. Zone의 클래스는 다음의 인터페이스를 갖습니다.

class Zone {
  constructor(parent: Zone, zoneSpec: ZoneSpec);
  static get current();
  get name();
  get parent();

  fork(zoneSpec: ZoneSpec);
  run(callback, applyThis, applyArgs, source);
  runGuarded(callback, applyThis, applyArgs, source);
  wrap(callback, source);
}

Zones현재 존(Current Zone)이라는 개념을 갖는 다는 것이 무엇보다 중요합니다. 현재 영역은 모든 비동기 오퍼레이션을 전달 받은 비동기 컨텍스트입니다. 이것은 현재 실행된 스택프레임과 비동기 작업을 연결시킨 존(Zone)을 의미합니다. 이 현재 존은 Zone.current 라는 스태틱 게터(static getter)함수로 접근이 가능합니다.

각 존은 name을 갖습니다. 이 이름은 대부분 디버깅을 위해서 사용됩니다. 또한 존을 조작하기 위한 다음의 메소드들이 정의되어 있습니다.

  • z.run(callbak, ...) : 주어진 존 안에서 동기적으로 함수를 호출합니다. 이 함수는 실행된 콜백의 현재 존을 z로 설정합니다. 그리고 콜백의 실행이 완료되고나면 현재 존을 원래의 존으로 되돌립니다. 존 안에서 실행된 콜백은 보통 존에 '들어감(entering)' 이라고 표현합니다.
  • z.runGuarded(callback, ...) : 기본적으로는 run 함수와 동일합니다. 단, 런타임 에러를 캐치하고, 에러를 인터셉트한 메커니즘을 제공한다는 차이점이 있습니다. 만약 에러가 어떤 부모 존에서도 처리되지 않으면, 에러는 다시 throw 됩니다.
  • z.wrap(callback) : 클로저에 z를 담은 새로운 함수를 만듭니다. 그리고 실행 될 때, z.runGuarded(callback) 함수를 호출합니다. 나중에 콜백으로 other.run(callback) 함수를 받는다 해도, other 존이 아니라 여전히 z 존 안에서 실행됩니다. 이 메커니즘은 Javascript에서 Function.prototype.bind 메소드가 동작하는 방법과 유사한 개념입니다.

다음 섹션에서 우리는 fork 메소드에 대해 이야기할 것입니다. 존은 작업의 실행(run), 스케쥴링(schedule), 취소(cancel)에 대한 많은 메소드들도 가지고 있습니다.

class Zone {
  runTask(...);
  scheduleTask(...);
  scheduleMicroTask(...);
  scheduleMacroTask(...);
  scheduleEventTask(...);
  cancelTask(...);
  ...
}

이 메소드들 개발자들이 거의 사용할 일이 없는 로우 레벨의 메소드들 입니다. 따라서 본 포스팅에서는 이것들에 대해 자세히 알아보지는 않을 것입니다. 작업을 스케쥴링하는 것은 존의 내부 오퍼레이션이며, 개발자들은 단순히 setTimeout과 같은 비동기 오퍼레이션을 호출한다고 생각해도 괜찮습니다.

콜 스택에서 존을 유지하기

Javascript VM은 각각의 함수를 그들의 스택 프레임(실행 영역)에서 실행합니다. 만약 다음과 같은 코드를 실행한다면 어떻게 될까요?

function c() {
    // capturing stack trace
    try {
        new Function('throw new Error()')();
    } catch (e) {
        console.log(e.stack);
    }
}

function b() { c() }
function a() { b() }

a();

c 함수 내부에서 다음의 콜 스택을 가지는 것을 볼 수 있습니다.

at c (index.js:3)
at b (index.js:10)
at a (index.js:14)
at index.js:17

Imgur

우리가 호출한 3개의 함수에 대한 스택 프레임과 글로벌 영역의 스택 하나를 가지게 됩니다.

일반적인 Javascript 환경에서는 c 함수에 대한 스택프레임을 a 함수의 스택 프레임에 연결시킬 어떠한 방법도 없습니다. 존은 같은 존에 있는 각 스택 프레임들을 서로 연결시킬 수 있도록 합니다. 예를 들어, 우리는 ac의 스택프레임을 같은 존에 만들어서 효율적으로 연결시킬 수 있습니다. 이제 다음과 같은 스택프레임을 만들 것입니다.

Imgur

잠깐만 기다리세요. 1분만 뒤에 어떻게 만드는지 말씀드릴게요.

zone.fork를 이용하여 자식 존 생성하기

존에서 가장 많이 사용되는 기능 중 하나는 fork 함수를 통해서 새로운 존을 생성하는 것입니다. 존을 복제(fork) 하면 새로운 자식 존이 생성되고, 복제된 존의 parent는 기존의 존이 됩니다.

const c = z.fork({name: 'c'});
console.log(c.parent === z);  // true

fork 메소드의 핵심은 클래스를 이용하여 단순히 새로운 존을 만드는 것입니다.

new Zone(targetZone, zoneSpec);

ac 함수를 같은 존에서 실행하여 서로 연결시키기 위한 우리의 목표를 완료하기 위해서, 먼저 존을 생성해야 합니다. 이제 위에서 보여준 fork 메소드를 사용하게습니다.

const zoneAC = Zone.current.fork({name: 'AC'});

fork 메소드에 전달한 오브젝트는 존 명세(ZoneSpec)라고 불립니다. 그리고 다음의 속성들이 있습니다.

interface ZoneSpec {
    name: string;
    properties?: { [key: string]: any };
    
    onFork?: ( ... );
    onIntercept?: ( ... );
    onInvoke?: ( ... );
    onHandleError?: ( ... );
    onScheduleTask?: ( ... );
    onInvokeTask?: ( ... );
    onCancelTask?: ( ... );
    onHasTask?: ( ... );
    ...
}

name은 존의 이름으로 정의 되며, properties는 존안의 데이터들을 연결시키기 위해 사용됩니다. 다른 모든 속성들은 부모 존이 자식 존의 특정 오퍼레이션을 인터셉트할 수 있게 해주는 '후킹' 속성입니다. 이것은 fork가 존을 계층적으로 생성한다는 점과 존을 조작할 수 있는 모든 메소드들은 후킹을 시도하는 부모 존에 의해 인터셉트될 수 있다는 점을 이해하기 위해 중요한 요소입니다. 이따가 우리는 비동기 오퍼레이션 사이에 데이터를 공유하기 위해 properties를 사용하는 방법과 구현된 작업을 추적하기 위해 후킹하는 방법에 대해 알아볼 것입니다.

일단 자식 존을 하나 더 만듭시다.

const zoneB = Zone.current.fork({name: 'B'});

이제 우리는 두개의 존이 생겼습니다. 그리고 각 존 안에서 함수들을 실행할 수 있게 되어습니니다. 존 안에서 함수를 실행하기 위해서는 zone.run() 메소드를 사용합니다.

zone.run을 이용하여 존을 변경하기

존에 연결된 특정 스택 프레임을 만들기 위해서 run 메소드를 이용하겠습니다. 이미 말했듯이, run 메소드는 명세된 존 안에서 동기적으로 콜백을 실행합니다. 그리고 콜백이 종료되면 원래의 존으로 복구합니다.

그럼, 이제 예제를 수정해서 실행해 봅시다.

 function c() {
    console.log(Zone.current.name);  // AC
}
function b() {
    console.log(Zone.current.name);  // B
    zoneAC.run(c);
}
function a() {
    console.log(Zone.current.name);  // AC
    zoneB.run(b);
}
zoneAC.run(a);

이제 매 콜 스택마다 존에 연결되어 있습니다.

Imgur

위 코드에서 보시다시피, 각 함수는 사용할 존을 직접적으로 정의하여 run 메소드로 실행했습니다. 아마도 당신은 run 메소드를 사용하지 않고, 존 안에서 단순히 함수를 호출하게 되면 어떻게 되는지 의문이 생기실 겁니다.

모든 함수의 호출과 함수 내부에서 스케쥴링된 비동기 작업은 해당 함수와 동일한 존에서 실행된다는 것을 이해하세요. 이것은 매우 중요합니다.

우리는 항상 루트 존을 갖는 존 환경을 알고 있습니다. 만약 우리가 zone.run() 함수로 존을 변경하지 않는다면, 아마 모든 함수들은 root 존에서 실행될 것입니다. 다음 코드를 보시죠

function c() {
    console.log(Zone.current.name);  // <root>
}
function b() {
    console.log(Zone.current.name);  // <root>
    c();
}
function a() {
    console.log(Zone.current.name);  // <root>
    b();
}
a();

네, 이런 경우입니다. 다이어그램은 다음과 같이 됩니다.

Imgur

이번에는 a 함수에서만 zoneAB.run 을 사용해봅시다. bcAB존에서 실행될 것입니다.

const zoneAB = Zone.current.fork({name: 'AB'});

function c() {
    console.log(Zone.current.name);  // AB
}

function b() {
    console.log(Zone.current.name);  // AB
    c();
}

function a() {
    console.log(Zone.current.name);  // <root>
    zoneAB.run(b);
}

a();

Imgur

위 코드에서 우리는 AB 존안에서 b 함수를 명시적으로 호출했습니다. 그리고 c 함수도 `AB 존 안에서 실행이 되었습니다.

비동기 태스크에서 존을 유지하기

자바스크립트 개발에서 두드러지는 특징 중 하나는 비동기 프로그래밍이라는 것입니다. 아마도 대부분의 신규 자바스크립트 개발자들은 setTimeout 메소드와 같이 함수의 실행을 연기하는 비동기적 패러다임에 익숙할것입니다. 존은 setTimeout 같은 비동기 오퍼레이션을 호출할 수 있습니다. 이런 함수를 특히 매크로태스크라고 부릅니다. 그리고 마이크로태스크라는 태스크의 다른 종류도 있습니다. Promise.then과 같른 함수가 마이크로태스크입니다. 이런 용어들은 브라우저의 내부에서 사용됩니다. Jake ArchibaldTasks, microtasks, queues and schedules 포스팅에서 이를 자세히 설명합니다.

역자주: 지난번에 마이크로태스크(Microtask)에 이어 매크로태스크(Macrotask)가 나왔습니다. 매크로태스크는 일반 태스크라고 생각하시면 됩니다. 매크로테스크의 종류로는 setTimeout, setInterval 등의 함수가 있고, 마이크로태스크는 대표적으로 Promise가 있습니다. 마이크로태스크의 우선순위가 더 높기 때문에 큐에서 같이 대기중이라면, 먼저 실행이 되게 됩니다. 마이크로태스크에 대해서 더 관심이 있으신 분은 위 연결된 링크인 Tasks, microtasks, queues and schedules를 참조하세요.

다음 코드는 위 본문에서 나오는 코드입니다.

console.log('script start');

setTimeout(function() {
  console.log('setTimeout');
}, 0);

Promise.resolve().then(function() {
  console.log('promise1');
}).then(function() {
  console.log('promise2');
});

console.log('script end');

마이크로태스크의 우선순위가 더 높기 때문에 이 코드를 실행하면 다음과 같은 결과가 나옵니다.

script start
script end
promise1
promise2
setTimeout

이제 setTimeout 과 같은 비동기 작업을 존이 어떻게 다루는지 알아보겠습니다. 우리는 c 함수를 즉시 호출하는 대신에 setTimeout 함수의 콜백을 통해 호출하는 것으로 변경하겠습니다. 이 함수는 약 2초뒤에 분리된 콜스택에서 실행될 것입니다.

const zoneBC = Zone.current.fork({name: 'BC'});

function c() {
    console.log(Zone.current.name);  // BC
}

function b() {
    console.log(Zone.current.name);  // BC
    setTimeout(c, 2000);
}

function a() {
    console.log(Zone.current.name);  // <root>
    zoneBC.run(b);
}

a();

우리는 위에서 존 안에서 호출한 함수는 같은 존에서 실행된다는 것을 배웠습니다. 그리고 이 행동은 비동기 오퍼레이션에도 똑같이 적용됩니다. 우리가 비동기 태스크를 사용해서 콜백함수를 정의했다면, 이 콜백 함수는 비동기 태스크를 실행한 존과 같은 존에서 실행이 됩니다.

따라서 다음과 같은 그림을 그릴 수 있습니다.

Imgur

매우 훌륭합니다. 그런데, 이 다이어그램은 중요한 세부 구현을 감추고 있습니다. 내부적으로 존은 실행될 각 태스크에 대한 정확한 존을 복구해야만 합니다. 그러기 위해서 태스크가 실행된 존은 무엇인지 기억해야합니다. 존은 태스크에 연결된 존의 레퍼런스를 유지함으로써 이 정보를 기억합니다. 그리고 이 존은 루트 존의 핸들러로부터 태스크를 호출하는데 사용됩니다.

이는 매 비동기 작업에 대한 콜스택은 항상 정확한 존을 복구하기 위한 태스크와 연결된 정보를 가지는 루트존에서 시작한다는 것을 의미합니다. 그리고 이 존에서 태스크를 호출합니다. 따라서 좀 더 정확한 그림은 여기 있습니다.

Imgur

비동기 태스크에서 컨택스트 전파

존에는 개발자가 유용하게 사용할 수 있는 흥미로운 능력이 여러 개 있습니다. 그 중 하나는 컨택스트 전파Context propagation입니다. 간단하게 말해서 이것은 데이터를 존에 넣어놓으면, 이 존안에서 실행된 어떤 태스크들이라도 이 데이터에 접근할 수 있다는 것을 의미합니다.

우리의 마지막 예제를 만들어 봅시다. 비동기 태스크인 setTimeout을 통해 데이터를 유지하는 방법에 대해 설명하겠습니다. 우리는 이미 새로 존을 복제(fork)할 때, zoneSpec 오브젝트를 인자로 사용하는 것을 배웠습니다. 이 오브젝트는 옵셔널한 속성인 properties를 가질 수 있었습니다. 존에 데이터를 연결시키기 위해서 이 속성을 사용해봅시다.

const zoneBC = Zone.current.fork({
    name: 'BC',
    properties: {
        data: 'initial'
    }
});

그리고 zone.get 메소드를 사용하면 이 데이터에 접근할 수 있습니다.

function a() {
    console.log(Zone.current.get('data')); // 'initial'
}

function b() {
    console.log(Zone.current.get('data')); // 'initial'
    setTimeout(a, 2000);
}

zoneBC.run(b);

properties 오브젝트는 얕은 불변Shallow-immutable입니다. 얕은 불변이란 오브젝트에 속성을 추가하거나 제거할 수 없다는 것을 의미합니다. 이것은 존이 오브젝트를 변경하기 위한 어떤 메소드도 제공하지 않기 때문에 중요합니다. 따라서 위 예제에서 우리는 properties.data에 다른 값을 할당할 수 없습니다.

하지만 properties.data에 원시primitive타입 대신에 오브젝트를 전달한다면 우리는 데이터를 수정할 수 있게 됩니다.

const zoneBC = Zone.current.fork({
    name: 'BC',
    properties: {
        data: {
            value: 'initial'
        }
    }
});

function a() {
    console.log(Zone.current.get('data').value); // 'updated'
}

function b() {
    console.log(Zone.current.get('data').value); // 'initial'
    Zone.current.get('data').value = 'updated';
    setTimeout(a, 2000);
}

zoneBC.run(b);

그리고 fork메소드를 사용하여 생성된 자식 존 역시 부모존의 properties를 상속한다는 점도 아주 재미있습니다. 코드를 보시죠.

const parent = Zone.current.fork({
    name: 'parent',
    properties: { data: 'data from parent' }
});

const child = parent.fork({name: 'child'});

child.run(() => {
    console.log(Zone.current.name); // 'child'
    console.log(Zone.current.get('data')); // 'data from parent'
});

중요한 태스크 추적하기

존은 또한 중요한 비동기 매크로태스크나 마이크로태스크를 추적하는 유용하고 매우 흥미로운 능력이 있습니다. 존은 큐 안에 있는 모든 중요한 태스크들을 유지합니다. 큐의 상태가 바뀔때마다 알림을 받기 위해서 zoneSpec에 있는 onHasTask 라는 후킹 함수를 이용할 수 있습니다.

onHasTask(delegate, currentZone, targetZone, hasTaskState);

부모 존은 자식 존의 이벤트를 가로챌 수 있기 때문에, 이벤트를 가로챈 존과 태스크 큐(task queue)에서 바뀐 존을 구별하기 위해서 currentZonetargetZone 파라미터가 필요합니다. 예를 들어, 현재 존의 이벤트를 가로챘는지 확인하기 위해서는 두 파라미터를 다음과 같이 비교하면 됩니다.

// We are only interested in event which originate from our zone
if (currentZone === targetZone) { ... }

후킹함수의 마지막 파라미터는 태스크 큐의 상태를 가지고 있는 hasTaskState 입니다.

type HasTaskState = {
    microTask: boolean;
    macroTask: boolean;
    eventTask: boolean;
    change: 'microTask'|'macroTask'|'eventTask';
}

만약 존 안에서 setTimeout 호출했다면, hasTaskState 오브젝트는 다음의 값을 전달합니다.

{
    microTask: false; 
    macroTask: true; 
    eventTask: false; 
    change: 'macroTask';
}

위 상태는 큐 안에 팬딩된 매크로태스크가 있으며, macroTask로부터 변화가 생겼다는 것을 말해줍니다.

다음의 코드를 봅시다.

const z = Zone.current.fork({
    name: 'z',
    onHasTask(delegate, current, target, hasTaskState) {
        console.log(hasTaskState.change);          // "macroTask"
        console.log(hasTaskState.macroTask);       // true
        console.log(JSON.stringify(hasTaskState));
    }
});

function a() {}

function b() {
    // synchronously triggers `onHasTask` event with
    // change === "macroTask" since `setTimeout` is a macrotask
    setTimeout(a, 2000);
}

z.run(b);

이 코드의 실행 결과는 다음과 같습니다.

macroTask
true
{
    "microTask": false,
    "macroTask": true,
    "eventTask": false,
    "change": "macroTask"
}

2초후에 실행이 완료되면 onHasTask는 다시 발생합니다.(trigger)

macroTask
false
{
    "microTask": false,
    "macroTask": false,
    "eventTask": false,
    "change": "macroTask"
}

하지만 한 가지 주의사항이 있습니다. onHasTask 후킹은 전체의 태스크 큐비어있거나(empty) 혹은 비어있지 않은(non-empty) 상태를 추적할때만 사용할 수 있습니다. 각각의 태스크 큐를 추적할 때는 사용할 수 없습니다. 다음의 코드를 실행해 보시죠.

let timer;

const z = Zone.current.fork({
    name: 'z',
    onHasTask(delegate, current, target, hasTaskState) {
        console.log(Date.now() - timer);
        console.log(hasTaskState.change);
        console.log(hasTaskState.macroTask);
    }
});

function a1() {}
function a2() {}

function b() {
    timer = Date.now();
    setTimeout(a1, 2000);
    setTimeout(a2, 4000);
}

z.run(b);

그럼 이런 결과가 나옵니다.

1
macroTask
true

4006
macroTask
false

2초 뒤에 완료 되는 첫 번째 setTimeout 태스크에 대한 이벤트는 볼 수 없습니다. 첫 번째의 setTimeout이 실행 될 때와, 태스크 큐의 상태가 비어있지 않은non-empty상태에서 비어있는empty상태로 변화될 때 onHasTask 후킹이 발생합니다. 그래서 두 번째 setTimeout 콜백이 완료되는 4초 뒤에 발생합니다.

만약 개별 태스크에 대해서 추적하고 싶다면, onScheduleTaskonInvokeTask 후킹함수가 필요합니다.

onScheduleTask 와 onInvokeTask

zoneSpec은 개별 테스크들을 추적할 때 사용할 수 있는 두 가지 훅을 정의합니다.

  • onScheduleTask: setTimeout 같은 비동기 오퍼레이션이 탐지될 때마다 실행됩니다.
  • onInvokeTask: setTimeout(callback) 같이 비동기 오퍼레이션으로 전달된 콜백이 동작될 때 실행됩니다.

개별 태스크들을 추적하기 위해 이 훅들을 사용하는 방법을 보겠습니다.

 
let timer;

const z = Zone.current.fork({
    name: 'z',
    onScheduleTask(delegate, currentZone, targetZone, task) {
      const result = delegate.scheduleTask(targetZone, task);
      const name = task.callback.name;
      console.log(
          Date.now() - timer, 
         `task with callback '${name}' is added to the task queue`
      );
      return result;
    },
    onInvokeTask(delegate, currentZone, targetZone, task, ...args) {
      const result = delegate.invokeTask(targetZone, task, ...args);
      const name = task.callback.name;
      console.log(
        Date.now() - timer, 
       `task with callback '${name}' is removed from the task queue`
     );
     return result;
    }
});

function a1() {}
function a2() {}

function b() {
    timer = Date.now();
    setTimeout(a1, 2000);
    setTimeout(a2, 4000);
}

z.run(b);

그리고 위 코드를 실행한 결과는 다음과 같습니다.

1 “task with callback ‘a1’ is added to the task queue”
2 “task with callback ‘a2’ is added to the task queue”
2001 “task with callback ‘a1’ is removed from the task queue”
4003 “task with callback ‘a2’ is removed from the task queue”

onInvoke로 존의 진입을 가로채기

명시적으로 z.run()을 호출하거나 암묵적으로 태스크를 호출할 때 존에 진입합니다. 바로 앞에서 존 내부적으로 비동기 태스크에 연결된 콜백이 실행 될 때, 존의 진입을 가로챌 수 있는 onInvokeTask 훅을 설명했습니다. onInvokez.run()을 실행하여 존에 진입할 때마다 통지를 받을 수 있는 훅입니다.

다음 예제는 이 훅을 사용하는 방법입니다.

const z = Zone.current.fork({
    name: 'z',
    onInvoke(delegate, current, target, callback, ...args) {
        console.log(`entering zone '${target.name}'`);
        return delegate.invoke(target, callback, ...args);
    }
});

function b() {}

z.run(b);

출력 결과는 다음과 같습니다.

entering zone ‘z’

Zone.current는 내부적으로 어떻게 동작하는가

현재 존(Current zone)은 이것처럼 클로저 내부에서 _currentZoneFrame 변수를 사용하여 추적하며, Zone.current 게터 함수로 값을 반환받습니다. 따라서 존을 변경하기 위해서는 단순히 __currentZoneFrame 변수를 업데이트하면 됩니다. 이미 우리가 배운것처럼 z.run()을 실행하거나 태스크를 호출하는 것으로 존을 바꿀 수 있습니다.

run 메소드가 변수를 업데이트하는 코드는 이곳에 있습니다. (아래는 코드를 발췌)

class Zone {
   ...
   run(callback, applyThis, applyArgs,source) {
      ...
      _currentZoneFrame = {parent: _currentZoneFrame, zone: this};

runTask이곳에서 변수를 업데이트 합니다. (아래는 코드를 발췌)

class Zone {
   ...
   runTask(task, applyThis, applyArgs) {
      ...
      _currentZoneFrame = { parent: _currentZoneFrame, zone: this };

runTask 메소드는 각 태스크를 가지는 invokeTask 메소드에 의해 호출된 메소드입니다.

class ZoneTask {
    invokeTask() {
         _numberOfNestedTaskFrames++;
      try {
          self.runCount++;
          return self.zone.runTask(self, this, arguments);

모든 태스크는 생성될 때, zone 속성 안에 zone으로써 저장됩니다. 그리고 이 존은 정확히 invokeTask 안에서 runTask를 호출하는 존입니다. (self는 태스크의 인스턴스를 참조합니다.)

self.zone.runTask(self, this, arguments);

추가 자료

추가로 다른 좋은 자료들도 소개합니다. 만약 존에 대해 더 공부하고 싶으시면 참조해 보세요.

깃허브에서 존과 관련된 많은 질문들에 대해 답변 해준 JiaLi에게 정말 감사를 드립니다.

읽어주셔서 감사합니다! 만약 이 포스팅이 좋았으면 박수버튼을 눌러주세요. (이글은 번역본이므로 아래 참조 원문으로 가셔서 눌러주세요 >_<) 박수는 저에게 많은 의미가 있고 이 이야기를 더 많은 사람들이 볼 수 있게 됩니다. 더 고급진 포스팅들을 보고싶으시면 TwitterMedium에서 팔로우를 해주세요!

역자주: 정말 긴 글이었습니다. zone에 대해서 제가 직접 포스팅하고 싶은 욕심도 있지만, 이 포스팅은 보자마자 번역해야겠다라고 생각이 든 글이었습니다. 글이 너무너무 좋았어요. 더 번역되었으면 하는게 있으시면 언제든지 댓글이나 방명록을 통해 남겨주세요. 감사합니다 ^^

참조



댓글
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
«   2024/03   »
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
글 보관함