티스토리 뷰


[번역] Angular에서 Zones(zone.js)의 역할

본 포스팅은 Zones in Angular 포스팅을 번역한 글입니다.

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

Zones의 이해 포스팅에서 우리는 Zones 의 능력에 대해 알아 보았습니다. Zones 을 이용하면 비동기적으로 실행되는 코드에 대해 성능을 프로파일링 할 수 있었습니다. Zones는 우리의 비동기적 작업을 후킹할 수 있는 실행영역(Execution Context)의 종류라고 배웠습니다. 만약 위 포스팅을 읽지 않으셨다면, 먼저 읽어보기를 강력하게 추천합니다. 본 포스팅에서는 Angular에서 Zones의 역할이 무엇인지에 대해서만 집중합니다.

역자주: 먼저 읽어보시 마세요. 혹 필요한 사전지식에 대해서는 제가 설명드리겠습니다.. ^^

위에 언급된 Zones의 이해라는 지난 포스팅을 보면, 마지막 파트에서 Zones을 이용하여 비동기코드의 프로파일링을 합니다. 쉽게 말해, 성능테스트를 했는데요. Zones 을 이용하면, 해당 Zones(Javascript에서 실행영역이라고 이해하면 됩니다.)이 실행되는 시점에서 여러 이벤트를 후킹할 수 있습니다. 위 포스팅에서는 beforeTaskafterTask 이벤트에 대해 후킹을 하여, 해당 코드의 실행 시간을 측정하는 예제를 만들었습니다.

특정 Zones(실행영역)을 만들어서 코드를 올리고, Zones의 시작시점과 종료시점에 대한 이벤트를 이용하는 것입니다. Zoens의 강력함에 대해서는 나중에 나중에 다른 포스팅에서 따로 언급하겠습니다. 지금은 'Zones은 자바스크립트의 실행영역을 직접 나누는 것이며, 직접 실행영역을 분리했기 때문에 여러가지 후킹이 가능하다.' 정도로만 이해해주세요.

자바스크립트의 실행영역을 모르신다구요? 그건 아마 먼 미래에도 제가 다루지는 않을 것 같습니다. ㅠ 자바스크립트 문법책을 정독해주세요.

역자주2: Zones는 흔히 zone.js로 불리는 라이브러리입니다. Github 레퍼지토리나 NPM에서 zone.js라고 사용되기 때문인데요. 요새는 zone.js라는 말보다는 .js는 떼고 zone 또는 zones라고 부릅니다. 자바스크립트의 표준화가 되어가고 있는 상황이기 때문에 .js를 빼고있는 추세일지도 모릅니다 ^^

Zones은 Angular에 딱 맞는 옷이다.

ZonesAngular 가 필요로 했던 Chnage Detection(변화감지) 의 완벽한 해결책이었습니다. 혹시 여러분은 Angular가 언제 왜 Change Detction을 수행하는지 궁금하셨던 적이 있나요?

"사장님? 제 어플리케이션에 변화가 생겼어요. 확인해주시겠어요?"

라고 Angular에게 말해주는 건 누구일까요?

이 질문에 대해 알아보기 전에, 우리 어플리케이션에 변화를 만드는 것들이 무엇인지에 대해 생각해봐야 합니다. 또는 무엇이 우리의 어플리케이션의 상태를 변화시킬수 있는지에 대해서요. 어플리케이션의 상태는 다음 세 가지의 이유로 변화가 필요하게 됩니다.

  • 이벤트: click, change, input, submit 등의 유저 이벤트
  • XMLHttpRequest: 원격 서버로부터 데이터를 가져올 때, (Ajax)
  • 타이머: setTimeout, setInterval 함수가 동작할 때

이 세 가지는 흔히 사용되는 기법입니다. 이것들을 뭐라고 부르는지 기억나시나요? 맞아요! 비동기 코드입니다.

왜 기법들이 중요할까요? Angular에서 화면을 업데이트할 필요가 있는 상황들이 위 비동기 기법에 의한 상황들이기 때문입니다. 웹페이지에서 버튼을 클릭할 때, 클릭 이벤트에 대한 핸들러를 실행하는 Angular 컴포넌트를 한 번 살펴봅시다.

@Component({
  selector: 'my-component',
  template: `
    <h3>We love {{name}}</h3>
    <button (click)="changeName()">Change name</button>
  `
})
class MyComponent {

  name:string = 'thoughtram';

  changeName() {
    this.name = 'Angular';
  }
}

만약 (click) 이라는 문법에 대해 생소하시다면, Angular의 템플릿 문법 파해치기 라는 저희의 포스팅을 참고해주세요. 간단히 말해 <button> 엘리먼트의 Click 이벤트에 대한 핸들러를 매핑하는 방법입니다.

역자주: jquery로 치면 $(버튼아이디).click() 과 동일합니다. Angular는 단방향 이벤트 스트림을 지원하는데, () 문법은 Upstream 이벤트라고 생각하시면 됩니다. 즉, 컴포넌트를 정의한 클래스로 단방향 이벤트를 전달하는 것이지요.

컴포넌트의 버튼을 클릭하면, ChangeName()함수가 호출됩니다. 이 함수는 컴포넌트의 name 변수를 변경합니다. 우리는 DOM에 이 변화가 반영되기를 원하며, Angular{{name}} 을 업데이트 할 것입니다. 와우, 마치 마법처럼 변하는군요.

setTimeout() 을 이용하여 name 프로퍼티를 변경하는 다른 예제입니다. 이제 버튼은 제거해도 됩니다.

@Component({
  selector: 'my-component',
  template: `
    <h3>We love {{name}}</h3>
  `
})
class MyComponent implements OnInit {

  name:string = 'thoughtram';

  ngOnInit() {
    setTimeout(() => {
      this.name = 'Angular';
    }, 1000);
  }
}

우리는 프레임워크에 변화가 일어났다는 것을 알려주기 위해서 어떠한 작업도 하지 않아도 됩니다. ng-click도, $timeout 도, $scope.$apply() 도 필요 없습니다.

역자주: ng-click, $timeout, $scope.$apply() 는 모두 angular.js (1.x 버전)에서 사용되는 change detection 기법입니다.

만약 Zones의 이해 를 읽으셨다면, AngularZones를 이용하기 때문에, 이런 작업이 가능하다는 것을 알 수 있을 겁니다. ZonessetTimeout()addEventListner()와 같이 전역적인 비동기 오퍼레이션에 대해 몽키패치를 합니다. 이것은 Angular가 DOM 을 언제 업데이트해야하는지 쉽게 발견할 수 있게 합니다.

사실 VM 이 완료 될 때마다, AngularChange Detection을 실행하게 합니다. 이 코드를 보세요.

ObservableWrapper.subscribe(this.zone.onTurnDone, () => {
  this.zone.run(() => {
    this.tick();
  });
});

tick() {
  // perform change detection
  this.changeDetectorRefs.forEach((detector) => {
    detector.detectChanges();
  });
}

AngularZonesonTurnDonw 이벤트를 발생시킬 때마다, 전체 어플리케이션에 대해서 Change Detection을 수행합니다. 만약 당신이 AngularChange Detection 에만 궁금하다면 이 포스팅(Angular의 Change Detection 설명)을 참조하세요.

역자주: 여러 포스팅들.. 오지게 홍보하네요 -_-;

근데 잠깐만요. onTurnDone 이벤트는 누가 발생시키는 걸까요? 이 이벤트는 Zones의 API는 아닙니다. 그렇죠? 이것은 AngularNgZone 이라는 아이와 관련되어 있습니다.

Angular에서의 NgZone

NgZone 은 실행영역에 대한 추가적인 기능과 확장된 API를 제공하는 포크(fork)된 영역입니다. 확장 API들에는 우리가 구독할 수 있는 다음의 커스텀 이벤트들이 있습니다.

역자주: 여기서 fork는 POSIX에서 자식프로세스를 생성하는 fork와 유사합니다. 자바스크립트내에서 가상의 실행영역을 생성한다고 보면 됩니다.

  • onTurnStart(): Angular의 이벤트가 시작되기 직전에 발생합니다.
  • onTurnDone(): 현재의 작업이 완료되면 이벤트가 발생합니다.
  • onEventDone(): 마지막 onTurnDone 콜백 이후에 발생됩니다. 유효한 어플리케이션의 상태를 테스트하기에 유용합니다.

만약, Observable스트림 이 당신에게 낯설다면, Angular에서 Observable 이용하기를 읽어주세요.

AngularbeforeTaskafterTask 콜백을 사용하는것 대신에 특별한 이벤트를 추가한 이유는 타이머나 다른 Micro task를 추적하기 위해서입니다. 이 이벤트들를 다루기 위한 API로 Observable 을 사용하는것도 아주 좋은 것 같습니다.

역자주: Micro task에 대해서 설명하는 것 또한 하나의 포스팅이 됩니다. 마이크로 태스크는 일반 태스크보다 우선순위가 높은 태스크를 말합니다. 예를 들어 타이머는 일반 태스크이구요. Promise는 마이크로 태스크입니다. 여기서는 여기까지만 이해합시다.

Angular의 Zone 밖에서 코드 실행하기

NgZone이 글로벌 존의 포크(fork)이기 때문에, AngularChange Detection을 수행하는 존 안에서 어떠한 작업이 실행될 때나 되지않을 때에 대한 모든 제어를 할 수 있습니다. 이것이 왜 유용할까요? 음.. 우리는 AngularChange Detection을 마법같이 수행하는것을 항상 원하지는 않을 수도 있습니다.

여러번 언급했듯이, Zones 은 브라우저의 비동기적 작업에 대해 꽤 많은 몽키패치가 가능합니다. 그리고 NgZone 은 단지 존의 포크(fork)입니다. 다시 말해 비동기적 작업에 대한 Change Detection을 프레임워크에 알려주기 위한 역할을 수행합니다. 그렇기 때문에 mousemove 같은 이벤트들이 발생할 때 역시, Change Detection이 발생할 것입니다.

우리는 mousemove 같은 이벤트들이 발생할 때마다 Change Detection이 일어나는 것을 원하지 않을 것입니다. 어플리케이션에 매우 나쁜 영향을 줄 것이기 때문이지요. (성능저하)

이것이 바로 NgZone의 부모 존에서 작업을 수행하는 runOutsideAngular() API가 있는 이유입니다. 이 API를 이용하면 onTurnDone 이벤트가 발생하지 않습니다. 따라서 어떤 Change Detection 도 수행되지 않습니다. 이 유용한 기능을 설명하기 위해서 다음의 코드를 봅시다.

@Component({
  selector: 'progress-bar',
  template: `
    <h3>Progress: {{progress}}</h3>
    <button (click)="processWithinAngularZone()">
      Process within Angular zone
    </button>
  `
})
class ProgressBar {

  progress: number = 0;

  constructor(private zone: NgZone) {}

  processWithinAngularZone() {
    this.progress = 0;
    this.increaseProgress(() => console.log('Done!'));
  }
}

별로 특별할 것 없는 코드입니다. 템플릿을 보면, 버튼을 클릭할 때 processWithinAngularZone() 함수를 호출했습니다. 하지만 이 함수에서는 increaseProgress() 함수를 호출합니다. 더 자세히 봅시다.

increaseProgress(doneCallback: () => void) {
  this.progress += 1;
  console.log(`Current progress: ${this.progress}%`);

  if (this.progress < 100) {
    window.setTimeout(() => {
      this.increaseProgress(doneCallback);
    }, 10);
  } else {
    doneCallback();
  }
}

increaseProgress()함수는 progress 변수가 100이 될 때 까지, 10ms 간격으로 재귀호출을 합니다. progress가 100이 되면, doneCallback() 함수를 호출합니다. 우리가 setTimeout 함수를 사용한 방법에 주목합시다.

이 코드를 브라우저에서 실행하면, 우리가 알고있는 것과 같은 결과가 나옵니다. 각 setTimeout() 함수가 호출될 때마다. AngularChange Detection을 수행하여 뷰를 업데이트합니다. 즉, progress변수가 10ms마다 증가하는것을 볼 수 있습니다. 이 코드를 Angular 존 밖에서 이 코드를 실행할 때, 더욱 재밌습니다. 다음의 메소드를 추가해 보죠.

processOutsideAngularZone() {
  this.progress = 0;
  this.zone.runOutsideAngular(() => {
    this.increaseProgress(() => {
      this.zone.run(() => {
        console.log('Outside Done!');
      });
    });
  });
}

processOutsideAngularZone() 은 마찬가지로 increaseProgress() 함수를 호출합니다. 하지만 이번에는 runOutsideAngularZone() 함수를 이용하여 호출하기 때문에, 각 setTimeoutAngular에게 변화를 통지하지 않습니다. 우리는 NgZone 을 컴포넌트에 인젝션하여 Angular의 존 API를 이용할 수 있습니다.

progress가 증가해도 UI는 업데이트되지 않습니다. 하지만, increaseProgress 함수가 완료되면, zone.run()이라는 Angular의 존 안에서 실행되는 또 다른 작업을 실행합니다. 그러면 AngularChange Detection을 수행하고 뷰를 업데이트합니다. 즉, 다시 말해 progress가 증가하는 동안 보이지 않다가, 100이 되어 완료가 되면 UI가 갱신이 되는 것입니다. 아래 코드를 실행해서 확인해보세요.

PLUNKER: https://embed.plnkr.co/fA0Wj7Ja5wncSebtCTss/

Zones은 TC39에서 표준화로 제안되었습니다. 아마 더 자세히 공부할 이유가 되지 않을까 싶습니다.

역자주: TC39는 ECMA명세를 관리하는 ECMA의 기술위원회(Technical Committee)입니다. 자세한 내용은 ECMAScript와 TC39라는 포스팅을 참고해주세요.

참조


댓글
공지사항
최근에 올라온 글
최근에 달린 댓글
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
글 보관함