Angular/Concept

[NX] Lazy loading with Router 또는 without Router - 2

윤영식 2021. 8. 5. 09:28

Angular router의 loadChildren 을 사용하지 않고 Lazy loading하는 방법을 알아본다. 

 

Angular Module 에 대한 Lazy Loading

Angular의 Lazy Loading 방식

  • NgModule 단위로 lazy loading한다.
  • Angular v8부터 import(<module path>).then(mod => mod.<moduleName>) 방식으로 로딩한다. import 구문을 사용하면 자동으로 code splitting이 된다.
  • Angular v7버전 이하는 ViewEngine 이라는 Template엔진을 사용했고, 8부터 선택적으로 9부터는 디폴트로 Ivy를 Template엔진으로 사용한다.
  • Module을 컴파일/로딩하고 Component를 인스턴스화하기위해 ComponentFactoryResolver를 사용하고, ViewContainer에 인스턴스화된 컴포넌트를 렌더링한다.

우선 manual lazy loading을 위한 모듈과 컴포넌트를 생성한다. 

// 라이브러리 생성, nx 또는 ng 둘 다 사용가능하다
$ nx g lib manuallazy

// 컴포넌트 생성
$ nx g c manual --project=manuallazy
CREATE libs/manuallazy/src/lib/manual/manual.component.scss (0 bytes)
CREATE libs/manuallazy/src/lib/manual/manual.component.html (21 bytes)
CREATE libs/manuallazy/src/lib/manual/manual.component.spec.ts (628 bytes)
CREATE libs/manuallazy/src/lib/manual/manual.component.ts (283 bytes)
UPDATE libs/manuallazy/src/lib/manuallazy.module.ts (266 bytes)

lazyload하는 service를 "app-container" 애플리케이션에 생성한다.

$ nx g s lazy-loader --project=app-container
CREATE apps/app-container/src/app/lazy-loader.service.spec.ts (378 bytes)
CREATE apps/app-container/src/app/lazy-loader.service.ts (139 bytes)

서비스에 loadModule 메소드를 추가한다. 8전부터 Ivy 사용만 대응한다. 7버전 이하는 NgModuleFactoryLoader를 사용한다. (참조)

import { Compiler, Injectable, Injector, NgModuleFactory, Type } from '@angular/core';

@Injectable({
    providedIn: 'root',
})
export class LazyLoaderService {
    constructor(private compiler: Compiler, private injector: Injector) {}

    loadModule(path: any) {
        (path() as Promise<Type<any>>)
            .then((elementModule) => {
                    try {
                        return this.compiler.compileModuleAsync(elementModule);
                    } catch (err) {
                        throw err;
                    }
            })
            .then((moduleFactory) => {
                try {
                    const elementModuleRef = moduleFactory.create(this.injector);
                    const moduleInstance = elementModuleRef.instance;

                    // instantiate component dynamically.
                } catch (err) {
                    throw err;
                }
            });
    }
}

 

 

컴포넌트 인스턴스화 및 ViewContainer에 렌더링하기

다음 순서로 컴포넌트를 인스턴스화 한다. 

  • ManuallazyComponent를 manuallazy 라이브러리에 생성한다.
  • MauallazyModule에 초기 인스턴스화할 Component를 리턴한다.
  • LazyLoaderService에서 ComponentFactoryResolver를 통해 인스턴스화하고 ViewContainer에 할당한다. 
  • "app-container" 애플리케이션의 tsconfig.app.json에 "include"에 MauallazyModule의 index.ts를 추가 설정한다. 
  • "app-container" 애플리케이션에서 LazyLoaderService를 loadModule을 호출한다. 

ManulComponent를 생성한다. 

$ ng g c manual --project=manuallazy

// libs/manuallazy/lib/src/manual/manual.component.html
<p>manual lazy component</p>

 

ManuallazyModule에 ComponentType을 리턴한다.

// libs/manuallazy/src/lib/manuallazy.module.ts

import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { ManualComponent } from './manual/manual.component';

@NgModule({
    imports: [CommonModule],
    declarations: [ManualComponent],
})
export class ManuallazyModule {
    bootstrapComponent(): any {
        return ManualComponent;
    }
}

 

LazyLoaderService에 컴포넌트를 로딩하는 "addComponent" 메소드을 추가한다. 

// apps/app-container/src/app/lazy-loader.service.ts
import {
    Compiler,
    Injectable,
    Injector,
    NgModuleFactory,
    Type,
    ViewContainerRef,
    ComponentRef,
    ComponentFactory,
    ComponentFactoryResolver,
} from '@angular/core';

@Injectable({
    providedIn: 'root',
})
export class LazyLoaderService {
    constructor(private compiler: Compiler, private injector: Injector, private resolver: ComponentFactoryResolver) {}

    loadModule(path: any, viewContainer: ViewContainerRef): void {
        (path() as Promise<Type<any>>)
            .then((elementModule) => {
                try {
                    return this.compiler.compileModuleAsync(elementModule);
                } catch (err) {
                    throw err;
                }
            })
            .then((moduleFactory) => {
                try {
                    const elementModuleRef = moduleFactory.create(this.injector);
                    const moduleInstance = elementModuleRef.instance;

                    // instantiate component dynamically.
                    this.addComponent(moduleInstance.bootstrapComponent(), viewContainer);
                } catch (err) {
                    throw err;
                }
            });
    }

    addComponent(cmpType: any, viewContainer: ViewContainerRef): ComponentRef<any> {
        const factory: ComponentFactory<any> = this.resolver.resolveComponentFactory(cmpType);
        let cmp: ComponentRef<any>;
        if (viewContainer) {
            viewContainer.clear();
            cmp = viewContainer.createComponent(factory);
        }
        return cmp;
    }
}

 

"app-container" 애플리케이션의 tsconfig.app.json 에 MauallazyModule의 index.ts 를 추가 설정한다.

// apps/app-container/src/tsconfig.app.json 
{
    "extends": "./tsconfig.json",
    "compilerOptions": {
        "outDir": "../../dist/out-tsc",
        "types": []
    },
    "include": ["**/*.ts", "../../libs/lazyview/src/index.ts", "../../libs/manuallazy/src/index.ts"],
    "exclude": ["src/test-setup.ts", "**/*.spec.ts", "src/environments/*.ts"]
}

 

"app-container" 애플리케이션에서 ManullazyModule을 호출한다. 

// apps/app-container/src/app/app.component.html 일부분
<li nz-menu-item nzMatchRouter>
  <a (click)="manualLazyLoading($event)">Monitor</a>
</li>
... 중략 ...
<div class="inner-content">
   <router-outlet></router-outlet>
   <div style="padding-top: 100px;" #InnerContent></div>
</div>


// apps/app-container/src/app/app.component.ts
import { LazyLoaderService } from './lazy-loader.service';
import { Component, ViewChild, ViewContainerRef } from '@angular/core';

@Component({
    selector: 'app-root',
    templateUrl: './app.component.html',
    styleUrls: ['./app.component.scss'],
})
export class AppComponent {
    isCollapsed = false;
    @ViewChild('InnerContent', { read: ViewContainerRef }) innerContent: ViewContainerRef;

    constructor(private readonly lazyLoaderServie: LazyLoaderService) {}

    manualLazyLoading(event: MouseEvent): void {
        // import 할 때 위치투명하게 nx library를 호출한다.
        const manualLoad = () => import('@micro-demo/manuallazy').then((m) => m.ManuallazyModule);
        this.lazyLoaderServie.loadModule(manualLoad, this.innerContent);
    }
}

"Monitor" 메뉴를 클릭하면 "micro-demo-mauallazy.js"파일이 다운로드되고 ViewContainer위치에 컴퍼넌트를 렌더링한다

구현 소스: https://github.com/ysyun/blog-5730-micro-demo

 

ysyun/blog-5730-micro-demo

https://mobicon.tistory.com/573 블로그 소스. Contribute to ysyun/blog-5730-micro-demo development by creating an account on GitHub.

github.com

 

라우팅으로 통한 Angular 모듈 Lazy loading과 메뉴얼 Lazy loading 방식의 단점

  • 둘다 tsconfig.app.json과 같은 사전 설정작업이 필요하다.
  • "app-container"라는 Lazy loading된 모듈을 사용하는 애플리케이션과 함께 컴파일이 되어야 한다.

장점은 import구문을 통해 자동 Code Splitting 기능이다. 위이 단점을 해결하기 위해서 Web Components를 지원하는 @angular/elements를 사용한다. 이에 대한 글은 다음을 참조한다. 

https://mobicon.tistory.com/573

 

[Micro Frontend] Web Components 기반 마이크로 프론앤드

Angular v6부터 Web Components에 대한 지원으로 @angular/elements 기능이 추가되어 Custom HTML Tag을 만들 수 있도록 지원한다. 본 글은 해당 사이트의 글을 Nx.dev 환경과 통합하여 개발하는 과정을 설명한다...

mobicon.tistory.com

 

참조

https://juristr.com/blog/2019/10/lazyload-module-ivy-viewengine/

 

Manually Lazy Load an Angular Module with ViewEngine and Ivy

Find out how to lazy load an NgModule compatible with ViewEngine and Ivy

juristr.com

https://juristr.com/blog/2017/07/ng2-dynamic-tab-component/

 

Create a dynamic tab component with Angular

Learn about advanced topics such as dynamic components, ComponentFactoryResolver, ViewContainerRef, ngTemplateOutlet and much more...

juristr.com

https://juristr.com/blog/2019/04/state-lazy-loading-components-angular/#manual-lazy-loading-of-modules

 

Lazy load Angular Components

Or better..how to lazy load Angular Modules. Learn about the state of lazy loading and lazy loading on steroids with Angular Elements

juristr.com