ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • 새로운 Angular engine, Ivy Compiler 에 대하여 알아봅시다.
    Angular 2019. 11. 4. 15:58

     

    현재  Angular v8.2.11 에는 Ivy Preview 버전이 탑재 되어 있습니다.

    원래 계획으로는 v8 에 적용 될 예정이었지만 자동 마이그레이션에서 버그가 생각보다 많이 나온 것 같네요.

    Angular v9 도 10월 초에 공개될 예정이었지만, 계속 늦어지고 있는 걸 보니...

    (11월 말 공개예정)

     

    사실 Ivy Compiler 자체의 문제보다 Angular 는 이전부터 전버전에 관하여 손쉽게 마이그레이션을 할 수 있도록 

    update 명령을 제공하는데, 이 부분에서 생각보다 많은 시간이 소요되는 것 같습니다.

     

    Angular js 에서 Angular v2 ++ 버전으로 이동하면서 많은 비난과 욕을 먹었다보니, 마이그레이션에 집착하는 것 같습니다?

     

    본론으로 들어가서 Angualr Ivy Compiler 를 보기전 Ivy 전에는 어땠는지 간단하게 살펴봅니다.

    Angular Compiler 는 현재 크게 총 2번의 변화를 겪었는데 (v2 버전 이후) v2 ~ v3, v4 ~ v7 그리고 v8 부터는 

    Ivy preview 가 탑재되었습니다.

     

    현재 번들사이즈를 살펴봅시다.

    간단하게 프로젝트를 만들어 봅니다.

     

     

    Angular Cli 8.2.1  버전 기준

    ng new compiler-test
    cd compiler-test
    ng build

     

    프로젝트를 만들고 빌드를 한 뒤 용량을 살펴 봅시다.

    vendor 파일의 용량은 약 3.7 mb 입니다.

    실제로 build 된 파일을 돌려봅시다. 

     

    npm i -g http-server
    http-server dist/compile-test

     

     

    크롬에서 실행 한 결과 예상대로 약 4MB 조금 안되는 크기를 로드 하였습니다.

    4MB 라니 너무 큰 것 아닌가요? 네 큽니다.

    현재 빌드 된 파일에는 제품 버전이 아닌 개발 버전으로 Angular Compiler 가 포함되어있습니다.

    Angular 와 관련된 모든 명령 (etc 라이브러리, rxjs 등) 이 있어야 하기 때문에 용량이 클 수 밖에 없습니다.

    개발모드에서는 AOT를  적용하지 않습니다.

     

    그럼 AOT 와 트리쉐이킹이 적용 된 제품버전으로 빌드 해볼까요?

    ng build --prod
    http-server dist/compile-test

    사이즈가 4mb 에서 173kb 까지 줄었네요.

    추가로 Ivy 를 활용한 번들사이즈를 보기 전 현재 컴파일러가 무엇을 하고 있는지, 어떤식으로 동작하고 있는지

    살짝 살펴봅시다.

     

     

     

    # 현재 까지의 렌더링 엔진

     

    AngularJS ( v2 버전 이하) 에서는 DOM 의 작성은 브라우저에게 위임했습니다.

    브라우저 에게 HTML 분석을 맡기다보니, 문제가 발생했습니다.

    브라우저마다 HTML 분석 방식이 다를 수 있는거죠.

    그럼 컴파일러는 해당 브라우저 별 예외사항까지 처리 해야 합니다.

     

    그리고 일부 브라우저는 HTML 구문의 오류가 있다면 자체적으로 이를 해결하려 합니다. (ex: 태그를 닫거나 이동시키거나..)

    엔진 별로 오류 처리 방식이 다르고, 정확한 오류를 발견하기 힘들고, 오류를 발견해도 정확한 위치를 말해주지 않습니다.

     

    그리고 HTML 은 html 태그 이름 및 속성은 대소문자를 구분하지 않습니다.

    document.createElement('h1').nodeName

     

    위 코드의 결과로 "H1" 이 나오게 됩니다.

     

    서버 렌더링시에는 어떻게 할까요? 해당 HTML 구문을 적절하게 분석해서 HTML 로 나타내고,

    검색엔진에도 나타낼 수 있도록 만들어주는 브라우저! 가 서버에서 필요하다는 결론이 나옵니다.

    힘들겠죠?

     

    이런 부분의 해결을 위해 컴파일러가 존재합니다.

    컴파일러는 브라우저를 대체하고 Html 을 분석하고 모든 브라우저에서

    일관적으로 분석할 수 있도록 만들어 줍니다. 

     

    서버 렌더링 시 에도 브라우저가 필요없이 실행이 가능합니다.

    디버깅시에도 자세한 오류 정보를 표시 해줍니다.

     

    컴파일러의 역활을 대략적으로 알았다면 이제 실제 Angular 에서 어떻게 되는지 봅시다.

     

    app Component 를 간단하게 수정 합시다.

    import { Component } from '@angular/core';
    
    @Component({
      selector: 'app-root',
      template: `
        <h1>{{title}}</h1>
      `,
      styleUrls: ['./app.component.css']
    })
    export class AppComponent {
      title = 'compile-test';
    }
    

     

    Ivy 를 적용하지 않고 실행 해 봅시다.

    node_modules/.bin/ngc

     

    해당 명령을 실행 후에 컴파일된 파일을 살펴 봅시다.

    // dist/out-tsc/src/app/app.component.ngfactory.js
    
    /**
     * @fileoverview This file was generated by the Angular template compiler. Do not edit.
     *
     * @suppress {suspiciousCode,uselessCode,missingProperties,missingOverride,checkTypes}
     * tslint:disable
     */
    import * as i0 from "./app.component.css.shim.ngstyle";
    import * as i1 from "@angular/core";
    import * as i2 from "./app.component";
    var styles_AppComponent = [i0.styles];
    var RenderType_AppComponent = i1.ɵcrt({ encapsulation: 0, styles: styles_AppComponent, data: {} });
    export { RenderType_AppComponent as RenderType_AppComponent };
    export function View_AppComponent_0(_l) {
      return i1.ɵvid(
        0,
        [(_l()(), i1.ɵeld(0, 0, null, null, 1, "h1", [], null, null, null, null, null)), (_l()(), i1.ɵted(1, null, ["", ""]))]
        , null
        , function (_ck, _v) { var _co = _v.component; var currVal_0 = _co.title; _ck(_v, 1, 0, currVal_0); });
    }
    export function View_AppComponent_Host_0(_l) {
      return i1.ɵvid(
        0,
        [(_l()(), i1.ɵeld(0, 0, null, null, 1, "app-root", [], null, null, null, View_AppComponent_0, RenderType_AppComponent))
          , i1.ɵdid(1, 49152, null, 0, i2.AppComponent, [], null, null)], null, null);
    }
    
    var AppComponentNgFactory = i1.ɵccf("app-root", i2.AppComponent, View_AppComponent_Host_0, {}, {}, []);
    export { AppComponentNgFactory as AppComponentNgFactory };
    //# sourceMappingURL=app.component.ngfactory.js.map

    Angular 는 컴포넌트마다 Factory 파일이 같이 생성됩니다.

    해당 Factory 파일은 개발한 컴포넌트의 메타데이터가 정의 되어있습니다.

    정말 Deep 하게 뜯어 본다고 모든 코드를 볼 필요는 없고 중요한 부분만 떼서 봅시다.

     

    View_AppComponent_0  부분이 실제로 AppComponent 생성을 담당하고 

    View_AppComponent_Host_0 은 AppComponent 를 호출하는 부모, 즉 여기선  'app-root' 겠죠.

     

    여기서  i1.ɵvid() 는 view 표현을 담당합니다.

    flag, element node, diretive, update 관련 함수를 파라미터로 받습니다.

     

    함수이름에 세타가 자주 쓰이는 이유는 Angular 전용함수 라는 표시로 사용하며,

    사용자가 임의로 함수를 호출하지 못하도록 하기 위함입니다.

    실제로 일반적인 개발 중에 세타를 쓰는 경우는 없죠.

     

    대충 자세히 (?) 살펴보면 

    export function View_AppComponent_0(_l) {
      return i1.ɵvid(
        0,
        [(_l()(), i1.ɵeld(0, 0, null, null, 1, "h1", [], null, null, null, null, null)), (_l()(), i1.ɵted(1, null, ["", ""]))]
        , null
        , function (_ck, _v) { var _co = _v.component; var currVal_0 = _co.title; _ck(_v, 1, 0, currVal_0); });
    }

     

    첫번째 파라미터 flag 즉 해당 컴포넌트를 구분하기 위한 아이디 를 받습니다. view id 는 0 이 됩니다.

    두번째 파라미터로 Template 에 설정한 Elements 들, 우리가 설정한 h1 태그 가 설정되어있죠,

    ɵeld 함수가 입력한 element 를 정의하는 거겠죠,

    세번째 directive 는 사용하지 않았으니 null,

    네번째 에서 설정된 변수가 변경되었을때 값을 업데이트 하는 함수가 들어있습니다. 여기선 title 변수겠죠.

     

    이런식으로 Angular 는 컴파일 되면 해당 컴포넌트의 표현과 로직을 포함한 factory 파일이 별도로 같이 생성됩니다.

    위 예제는 정말 간단한 컴포넌트에 관한 Factory 파일입니다.

    자식컴포넌트나 지시자, 파이프, Input , Output 등 많은 요소들이 포함되면 로직은 훨씬 복잡해집니다.

     

    주요 포인트는

    현재 컴파일러는 컴포넌트를 정의하는 메타데이터가 포함 된 Factory 파일을 생성하고, 해당 Factory 파일을 

    별도의 Angular Interpreter 가 해석하여 적용하고 생성하는 구조 입니다.

     

     

    # 그럼 Ivy는?? 

     

    간단하게 현재 컴파일러를 살펴 보았는데 그럼 ivy 로 빌드하면 어떻게 나올까요?

    ivy 컴파일러를 사용해 봅시다.

     

    tsconfig.app.json 에 다음 설정을 추가 합시다.

    // tsconfig.app.json
    
    {
      ...
      "angularCompilerOptions": {
        "enableIvy": true
      }
    }

     

     

    이제 Ivy 로 컴파일 해봅시다.

    node_modules/.bin/ngc -p tsconfig.app.json

     

    // out-tsc/app/src/app/app.component.js
    
    import { Component } from '@angular/core';
    import * as i0 from "@angular/core";
    export class AppComponent {
        constructor() {
            this.title = 'compile-test';
        }
    }
    AppComponent.ngComponentDef = i0.ɵɵdefineComponent({
        type: AppComponent,
        selectors: [["app-root"]],
        factory: function AppComponent_Factory(t) {
            return new (t || AppComponent)();
        }, consts: 2, vars: 1, template: function AppComponent_Template(rf, ctx) {
            if (rf & 1) {
                i0.ɵɵelementStart(0, "h1");
                i0.ɵɵtext(1);
                i0.ɵɵelementEnd();
            } if (rf & 2) {
                i0.ɵɵselect(1);
                i0.ɵɵtextInterpolate(ctx.title);
            }
        }, styles: [""]
    });
    /*@__PURE__*/ i0.ɵsetClassMetadata(AppComponent, [{
        type: Component,
        args: [{
            selector: 'app-root',
            template: `
        <h1>{{title}}</h1>
      `,
            styleUrls: ['./app.component.css']
        }]
    }], null, null);
    //# sourceMappingURL=app.component.js.map

    Ivy 컴파일로 변경 했을때 나오는 코드입니다.

    기존 과 차이점을 살펴보면 먼저 별도의 Factory 파일이 사라지고 내부에서 직접적으로 Factory 함수를 포함하고 있고,

    Template 에 관한 View 로직과 변경감지 로직을 포함하고 있습니다.

     

    Template 함수를 보면 rf 와 ctx 두개의 변수를 받습니다.

    rf 는 RenderFlag 라는 비트 연산자로서 두개의 조각으로 나뉘어져 있습니다. Create & Update.

    export const enum RenderFlags {
      /* Whether to run the creation block (e.g. create elements and directives) */
      Create = 0b01,
    
      /* Whether to run the update block (e.g. refresh bindings) */
      Update = 0b10
    }

    Create 는 0b01 , Update 는 0b10 으로 각각 정수로 1 & 2 를 나타내며

    해당 정수를 판단하여 if 문에서 생성과 변경을 담당합니다.

     

    변경된 값을 처리하는 textInterpolate 함수는  값을 가져와서 이전 값과 비교하고 textBinding 으로 전달됩니다.

    textBinding 은 node 의 인덱스와 값을 전달받아 변경하게 되는데 

    export function textBinding<T>(index: number, value: T | NO_CHANGE): void {
      if (value !== NO_CHANGE) {
        const lView = getLView();
        ngDevMode && assertDataInRange(lView, index + HEADER_OFFSET);
        const element = getNativeByIndex(index, lView) as any as RText;
        ngDevMode && assertDefined(element, 'native element should exist');
        ngDevMode && ngDevMode.rendererSetText++;
        const renderer = lView[RENDERER];
        isProceduralRenderer(renderer) ? renderer.setValue(element, renderStringify(value)) :
                                         element.textContent = renderStringify(value);
      }
    }

    NO_CHANGE 가 아닌 값이 들어오면 값을 업데이트 하게 됩니다. 

    복잡하니 참고만 합시다.  :( 

     

    값이 변경되면 VDom 처럼 생성된 별도의 모든 DOM 트리를 비교한 뒤 실제 DOM 을 업데이트 하는 것이 아닌,

    변경된 값이 포함 되어있는 Template 만 개별적으로 업데이트 됩니다.

     

    그러기 위해 컴파일된 파일을 살펴보면 내부에 템플릿의 생성과 변경감지, 그리고 만약 존재한다면

    directive, pipe 등 해당 컴포넌트를 구성하기 위한 지침이 모두 포함되어 있습니다.

    이는 그동안 고통받았던 Hot Reload 에도 굉장히 큰 이득을 가져다 줍니다.

     

     

     

    # 기존과 비교하여 변경된 작동 방식

     

     

    현재 버전에서는 컴포넌트를 인스턴스화 하고 DOM 노드를 생성하고 변경감지를 실행하는 모든 로직은 Angular Interpreter 에 

    위임합니다. 엘리먼트에 관한 정보와 값에 대한 메타데이터를 런타임 시 Angular Interpreter 에게 주게되면 Interpreter 는

    해당 데이터를 가지고 DOM 을 어떻게 만들지 결정하고 변경감지 관련 로직을 실행합니다.

     

     

     

    Ivy 는 메타데이터를 정의하고 이 데이터를 Interpreter 에게 전달하는 대신

    해당 Template 에 관련된 지침을 만들고 자체적으로 수행하여 DOM 작업을 실행합니다.

    메타데이터가 제거되고 각 컴포넌트가 독립적인 지침으로 생성되며, 해당 지침에서 사용하는 부분만

    렌더링 엔진에서 가져와 컴포넌트를 생성하게 됩니다.

     

    기존에는 NgFactory 의존하며 메타데이터를 분석하고 만들었다면

    이제 모든 컴포넌트는 독립적으로 생성과 변경감지를 처리하고 필요한 명령만 참조하도록 변경되었습니다.

    이는 컴파일시 해당 컴포넌트들이 독립적으로 생성할 때 어떤 명령을 참조하는지 알 수 있게 됩니다.

    Tree Shaking 이 렌더링 엔진에 닿는 순간입니다.

    아래에서 더 자세히 살펴봅시다.

     

     

    # VDOM 과의 비교

     

    일반적으로 데스크탑 이나 랩탑에 비해 메모리 용량이 적은 모바일에서 메모리 사용량을 줄이고 크기를 

    줄이기 위함이 Ivy 의 주요 목적입니다.

    그러기 위해선 번들사이즈를 줄이고 렌더링 엔진을 최소한으로 사용할 수 있어야 합니다.

     

    이를 위해 Angular 팀은 Google 에서 내부적으로 사용한 Increment DOM 을 Ivy 에 자체적으로 만들어 

    적용했습니다.

    어떤점이 VDom 과 다를까요?

     

    VDom 은 가상 DOM 트리를 만들고 비교한 뒤 변경된 부분만 실제 DOM 에 업데이트 합니다.

     

    

    이렇게 가상 DOM 트리를 비교하여 변경된 부분만 실제 DOM 에 적용하기 때문에

    실제 DOM 의 업데이트의 횟수는 작아집니다.

    다만 이전 VDom 트리와 현재 VDom 트리를 언제나 메모리에 가지고 있어야 하며,

    값을 변경하고 렌더링 될때 전체 VDom 트리를 다시 구축하고

    이 과정에서 어떤 명령이 쓰일지 안쓰일지 알 수 없으니 이를 모두 브라우저에 전달합니다. 

     

     

    반대로 Increment DOM 에서 컴포넌트는 일련의 명령어로 컴파일 됩니다.

     

     

    만약 바인딩 관련 작업이 있다면

     

     

    여기서 템플릿에 새로운 바인딩을 추가하면

     

     

    이런식으로 명령어가 늘어납니다.

    해당 명령어들이 점점 증분되며 독립적으로 Template 을 구성할 수 있도록 하고, 여기서 나아가서

    사용하지 않는 명령들은 컴파일시에 구분하여 번들에서 제거합니다.

     

    이런식으로 렌더링엔진 에 Tree Shaking 을 적용하고 렌더링엔진의 크기와 사이즈를 줄일 수 있습니다.

     

     

    # 메모리 사용

    가상 DOM 은 렌더링이 될때 모든 전체 DOM 트리를 다시 만들게 됩니다.

     

    Increment DOM 은 DOM 이 추가되거나 제거될 때 외에는 메모리가 필요하지 않습니다.

    각 컴포넌트가 생성과 변경감지에 독립적인 지침을 따르고 있기 때문입니다.

    이런 메모리 사용의 효율은 대규모 앱일수록 더욱 효과가 좋아집니다.

     

    Ivy 의 장점은 이에 그치지 않습니다.

    • 디버깅시 오류의 자세한 표시
    • 컴포넌트 기준 Lazy Loading
    • 빠른 컴파일 시간과 Hot reload
    • 단위테스트의 속도

    등 많은 장점이 존재합니다. 

     

     

    # 간단 번들사이즈 비교

     

    마지막으로 아래 간단한 컴포넌트를 빌드 할때 현재 Renderer2 와  Ivy 번들사이즈 비교

    import { Component } from '@angular/core';
    
    @Component({
      selector: 'app-root',
      template: `
        <h1>{{title}}</h1>
      `,
      styleUrls: ['./app.component.css']
    })
    export class AppComponent {
      title = 'compile-test';
    }
    

     

    Renderer2
    Ivy

    똑같은 내용인데 약 7Kb 가 감소했습니다.

    서비스가 커질수록 차이는 더 커집니다.

     

    Increment DOM 의 장점과 Ivy 의 작동방식이 대략적으로 이해 되었나 모르겠네요.

    사실 저도 아직 헷갈리는 부분이 많습니다.

     

    완전히 이해하긴 힘들지만 중요한건 이번 9 버전에서 Ivy 의 정식 도입으로 인해

    이전보다 Angular 사용자가 많이 늘어 날 수 있는 큰 변화 인 것 같습니다.

     

     

     

    자료 참고 :

     

    Angular Ivy  

    https://blog.angularindepth.com/ivy-engine-in-angular-first-in-depth-look-at-compilation-runtime-and-change-detection-876751edd9fd

    https://blog.angularindepth.com/inside-ivy-exploring-the-new-angular-compiler-ebf85141cee1

    https://itnext.io/discover-iterative-dom-the-magic-behind-angular-ivy-4ce84123e58e

     

    Increment DOM

    https://blog.nrwl.io/understanding-angular-ivy-incremental-dom-and-virtual-dom-243be844bf36

    댓글