블로그 이미지
윤영식
Full Stacker, Application Architecter, KnowHow Dispenser and Bike Rider

Publication

Category

Recent Post

2020. 5. 18. 20:23 React/Architecture

Angular v6부터 Web Components에 대한 지원으로 @angular/elements 기능이 추가되어 Custom HTML Tag을 만들 수 있도록 지원한다. 본 글은 해당 사이트의 글Nx.dev 환경과 통합하여 개발하는 과정을 설명한다. Nx 환경은 mono repository 기반으로 multi application을 개발 할 수 있는 환경을 제공한다. Angular/CLI기반이지만 Angular, React, Node.js 개발까지 하나의 Git Repository안에서 개발하고 번들링 할 수 있도록 지원한다. 따라서 micro frontend에서 multi application 개발 잇점을 갖는다. 

 

블로그 소스 [GitHub]

 

 

NX 환경 준비

Angular v9.* 

@angular/cli v9.1.6

RxJS v6.5.*

Typescript v3.8.*

NodeJS v12.16.*

Node Version Manager(nvm)를 통해 로컬환경에 여러 Node버전을 관리하자.

// NodeJS
$ nvm install 12.16.2
$ nvm alias default 12.16.2
$ nvm use 12.16.2

// Angular/CLI 최신버전 사용
$ npm i -g @angular/cli@latest
$ npm i -g @nrwl/cli@latest
$ npm i -g yarn@latest

// local 설치
$ yarn add

NX workspace를 생성한다. 

$ npx create-nx-workspace@latest
// 선택 및 입력
? Workspace name (e.g., org name)     micro-demo
? What to create in the new workspace angular [a workspace with a single Angular application]
? Application name                    app-container
? Default stylesheet format           SASS(.scss)  [ http://sass-lang.com   ]

 

 

Web Components 개발 환경 설정

@angular/elements 를 설치한다. 

$ yarn add @angular/elements

UI Component로 ng-antd v9.1.* 를 사용한다. yarn 이 아니라 angular/cli의 'ng' 명령을 사용한다. Yes와 sidemenu 형태 선택한다.

$ ng add ng-zorro-antd

선택하기 
? Enable icon dynamic loading [ Detail: https://ng.ant.design/components/icon/en ] Yes
? Set up custom theme file [ Detail: https://ng.ant.design/docs/customize-theme/en ] Yes
? Choose your locale code: en_US
? Choose template to create project: sidemenu

설치 후에 Nx workspace와 ng-zorro-antd의 불일치를 해결한다.

  • apps/app-container/theme.less의 첫줄의 import 문구 수정
    • @import "../../../node_modules/ng-zorro-antd/ng-zorro-antd.less";
  • apps/app-container/index.html의 root tag 수정
    • <app-root></app-root>

설정 수정후 실행을 하면 sidemenu가 있는 환경이 자동 셋업되어 아래와 같이 보인다. 자세한 설치방법은 사이트를 참조한다.

$ ng serve --open

ng-zorro-antd 자동 적용 화면

 

 

 

Monitor Web Components 개발 및 번들링

 

monitor 애플리케이션을 신규 생성한다. 모니터 애플리케이션을 Web Components로 만들어 app-container 애플리케이션에서 동적으로 로딩해 본다. 

$ ng g app monitor

선택
? Which stylesheet format would you like to use? SASS(.scss)  [ http://sass-lang.com   ]
? Would you like to configure routing for this application? No

apps/monitor/src/app/app.component.html과 app.component.ts 를 변경한다. 

// app.component.html
{{title}} Application

// app.component.ts
import { Component, Input } from '@angular/core';

@Component({
  selector: 'micro-demo-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.scss'],
})
export class AppComponent {
  @Input() title = 'monitor';
}

apps/monitor/src/app/app.module.ts에서 Web Comopennts를 등록한다. 

import { BrowserModule } from '@angular/platform-browser';
import { NgModule, Injector } from '@angular/core';
import { createCustomElement } from '@angular/elements';

import { AppComponent } from './app.component';

@NgModule({
  declarations: [AppComponent],
  imports: [BrowserModule],
  providers: [],
  // 동적으로 생성하므로 entryComponents에 등록
  entryComponents: [AppComponent],
  // 정적 bootstrap을 사용하지 않음
  // bootstrap: [AppComponent],
})
export class AppModule {
  constructor(private injector: Injector) {}

  ngDoBootstrap() {
    // createCustomElement를 통해 Web Components 스펙에 맞는 객체로 반환
    const monitorApp = createCustomElement(AppComponent, { injector: this.injector });
    // browser window객체에 잇는 customElements를 통해 Web Components 등록
    customElements.define('monitor-app', monitorApp);
    // 사용방법: @Input() title이 있으므로 attribute 설정가능
    // <monitor-app title="Monitor Application"></monitor-app>
  }
}

monitor 애플리케이션을 번들링하면 여러개의 파일로 나오는데 번들링 파일을 최소화한다. ngDoBootstrap() 은 정적 bootstrap이 아닌 실행타임에 외부 컴포넌트를 동적으로 로딩할 때 애플케이션 root를 결정할 수 있게 한다. ngDoBootstrap에 대한 설명을 참조하자.

$ ng build monitor --prod --output-hashing=none

수행할 경우 main, polyfill, runtime등의 파일이 생성된다. 파일 최소화를 위해 ngx-build-plus 패키지를 이용한다. 

monitor 애플리케이션의 번들링된 파일들

ng add 명령으로 ngx-build-plus를 설치하고 애플리케이션은 monitor를 지정한다. 

$ ng add ngx-build-plus --project=monitor

ng add 로 수행을 하면 angular.json 파일의 설정을 자동으로 적용해 준다. builder의 명령어를 자동 수정함.

    "monitor": {
      "projectType": "application",
      "schematics": {
        "@nrwl/angular:component": {
          "style": "scss"
        }
      },
      "root": "apps/monitor",
      "sourceRoot": "apps/monitor/src",
      "prefix": "micro-demo",
      "architect": {
        "build": {
          "builder": "ngx-build-plus:browser",  <== builder가 자동 변경됨
          "options": {
          ...
          

다시 명령으로 monitor 애플리케이션을 번들링한다. ngx-build-plus로 확장한 옵션인 --single-bundle true를 추가한다.

$ ng build monitor --prod --output-hashing=none --single-bundle true

main, polyfill로 압축된 번들링 파일

main과 polyfill을 합쳐주는 스크립트를 등록한다. Mac/Linux기준 명령이다. micro-demo 폴더 밑에 buildSingle.sh 파일을 생성한다. 번들링파일 합치는 명령을 넣는다.  

 

#!/bin/sh
ng build monitor --prod --output-hashing=none --single-bundle true && cat dist/apps/monitor/main-es5.js dist/apps/monitor/polyfills-es5.js > apps/app-container/src/assets/monitor-es5.js

 

buildSingle.sh를 수행한 결과 파일은 app-container의 apps/app-container/src/assets/monitor-es5.js 쪽으로 copy된다.

 

 

 

컨테이너 애플리케이션에서 동적로딩

Web Components 스펙을 기준으로 monitor 애플리케이션을 번들링 했기때문에 프레임워크의 종류에 상관없이 <monitor-app> 태그를 사용할 수 있다. 정적으로 사용하는 방법을 살펴보자.

 

apps/app-container/src/app/pages/welcome/welcome.component.ts에서 monitor-es5.js파일을 import한다. 

import { Component, OnInit } from '@angular/core';
// 소스 import
import '../../../assets/monitor-es5.js';  

@Component({
  selector: 'app-welcome',
  templateUrl: './welcome.component.html',
  styleUrls: ['./welcome.component.scss']
})
export class WelcomeComponent implements OnInit {

  constructor() { }

  ngOnInit() {
  }

}

 

테스트를 위해 welcome.component.html 에 <monitor-app>태그를 설정해 보자. 

<monitor-app title="Hi Monitor"></monitor-app>

 

여기까지하고 수행을 하면 <monitor-app> 태그를 해석할 수 없다고 Angular가 에러를 뱃는다. <monitor-app> 은 Angular가 해석하는 것이 아니라 Browser에서 해석되는 Web Components이므로 무시하도록 welcome.module.ts에 CUSTOM_ELEMENTS_SCHEMA를 설정한다. 

 

import { NgModule, CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';
import { WelcomeRoutingModule } from './welcome-routing.module';
import { WelcomeComponent } from './welcome.component';

@NgModule({
  imports: [WelcomeRoutingModule],
  declarations: [WelcomeComponent],
  schemas: [CUSTOM_ELEMENTS_SCHEMA],
  exports: [WelcomeComponent]
})
export class WelcomeModule { }

 

설정한 "Hi Monitor"와 함께 <monitor-app>이 출력
브라우져가 해석한 <monitor-app> 태그

 

<monitor-app>태그를 설정하지 않고 Javascript를 이용하여 로딩해 본다. 

apps/app-container/src/app/monitor.service.ts 파일을 생성한다. 

  • monitor-es5.js 파일 동적 로딩
  • <monitor-app> DOM 동적 추가
import { Injectable } from '@angular/core';

@Injectable({providedIn: 'root'})
export class MonitorLoaderService {
  loaded = false;

  constructor() { }

  // script 동적 로딩
  loadMonitorScript(): void {
    if (this.loaded) {
      return;
    }

    const script = document.createElement('script');
    script.src = 'assets/monitor-es5.js';
    document.body.appendChild(script);
    this.loaded = true;
  }

  // <monitor-app> 태그 추가
  addMonitorApp(): void {
    const tile = document.createElement('monitor-app');
    // @Input() 내용은 setAttribute로 추가 가능
    tile.setAttribute('title', 'Dynamic Load Monitor');

    const content = document.getElementById('content');
    content.appendChild(tile);
  }
}

 

 위의 경우 monitor-es5.js 파일을 별도로 다운로드받아 동적 로딩을 수행한다. 

monitor-es5.js 파일을 <script> 태그 추가후 동적으로 다운로드 받음

 

 

공통 파일 빼고 번들링하기

만일 app-container과 monitor 에서 사용하는 공통 패키지의 버전이 같다면, app-container 애플리케이션과 monitor 애플리케이션이 공통으로 사용하는 파일중, monitor 애플리케이션을 번들링할 때 공통파일을 제거하는 방법에 대해 알아보자. 제거를 통해 monitor 애플리케이션의 번들링 사이즈를 줄일 수 있다. 이는 Network payload time을 줄여주는 결과를 갖는다. 

 

ngx-build-plus를 이용해서 @angular/cli의 webpack externals 을 자동 생성한다. (참조)

$ ng g ngx-build-plus:externals --project monitor

수행을 하면 angular.json 파일에 별도 환경이 추가되고, apps/monitor/webpack.externals.js 파일이 생성된다. angular.json 내용중

"node_modules/@angular/elements/bundles/elements.umd.js", 내용은 제거한다. elements.umd.js를 공통파일로 빼서 사용하는데 오류가 있다. 

elements.umd.js 를 포함시킬 경우 오류가 발생함

angular.json의 monitor 애플리케이션으 "scripts" 설정 내역 => scripts.js 파일에 설정한 *.umd.js 파일을 합친다.

// angular.json 내의 monitor 애플리케이션 설정
// "scripts"에 externals로 참조하는 파일 설정이 자동으로 입력되어 진다.
    "monitor": {
      "projectType": "application",
      "schematics": {
        "@nrwl/angular:component": {
          "style": "scss"
        }
      },
      "root": "apps/monitor",
      "sourceRoot": "apps/monitor/src",
      "prefix": "micro-demo",
      "architect": {
        "build": {
          "builder": "ngx-build-plus:browser",
          "options": {
            "outputPath": "dist/apps/monitor",
            "index": "apps/monitor/src/index.html",
            "main": "apps/monitor/src/main.ts",
            "polyfills": "apps/monitor/src/polyfills.ts",
            "tsConfig": "apps/monitor/tsconfig.app.json",
            "aot": true,
            "assets": [
              "apps/monitor/src/favicon.ico",
              "apps/monitor/src/assets"
            ],
            "styles": [
              "apps/monitor/src/styles.scss"
            ],
            "scripts": [
              "node_modules/rxjs/bundles/rxjs.umd.js",
              "node_modules/@angular/core/bundles/core.umd.js",
              "node_modules/@angular/common/bundles/common.umd.js",
              "node_modules/@angular/common/bundles/common-http.umd.js",
              "node_modules/@angular/compiler/bundles/compiler.umd.js",
              "node_modules/@angular/elements/bundles/elements.umd.js", // <-- 제거한다
              "node_modules/@angular/platform-browser/bundles/platform-browser.umd.js",
              "node_modules/@angular/platform-browser-dynamic/bundles/platform-browser-dynamic.umd.js"
            ]
          },

apps/monitor/webpack.externals.js  파일내역에서 ng.elements도 주석처리한다.  => monitor-es5.js 파일에서 제거되는 파일목록이다.

const webpack = require('webpack');

module.exports = {
    "externals": {
        "rxjs": "rxjs",
        "@angular/core": "ng.core",
        "@angular/common": "ng.common",
        "@angular/common/http": "ng.common.http",
        "@angular/platform-browser": "ng.platformBrowser",
        "@angular/platform-browser-dynamic": "ng.platformBrowserDynamic",
        "@angular/compiler": "ng.compiler",
        // "@angular/elements": "ng.elements",  <-- 주석처리한다.

        // Uncomment and add to scripts in angular.json if needed
        // "@angular/router": "ng.router",
        // "@angular/forms": "ng.forms"
    }
}

buildSingle.sh 내용을 수정한다. scripts.js 파일은 window.ng.core 또는 window.ng.common과 같은 global 객체가 담겨있는 파일이다. 따라서 scripts.js는 app-container 애플리케이션에서 최초 한번만 로딩하면 되고, 이후 monitor 애플리케이션과 같은 web components는 번들 파일은 자신의 내용만을 포함한다.

 

monitor app size including common library  - 129KB

common library를 포함한 사이즈 - 129KB

monitor app size excluding common library - 19KB

monitor app과 @angular/elements만 포함한 사이즈 - 19KB

#!/bin/sh
ng build monitor --prod --extra-webpack-config=apps/monitor/webpack.externals.js --output-hashing=none --single-bundle true && cat dist/apps/monitor/main-es5.js dist/apps/monitor/polyfill-es5.js > apps/app-container/src/assets/monitor-es5.js
cat dist/apps/monitor/scripts.js > apps/app-container/src/assets/scripts.js

scripts.js 파일은 angular.json 의 app-container 애플리케이션 "scripts" 옵션에 추가한다. 

    "app-container": {
      "projectType": "application",
      "schematics": {
        "@nrwl/angular:component": {
          "style": "scss"
        }
      },
      "root": "apps/app-container",
      "sourceRoot": "apps/app-container/src",
      "prefix": "micro-demo",
      "architect": {
        "build": {
          "builder": "@angular-devkit/build-angular:browser",
          "options": {
            "outputPath": "dist/apps/app-container",
            "index": "apps/app-container/src/index.html",
            "main": "apps/app-container/src/main.ts",
            "polyfills": "apps/app-container/src/polyfills.ts",
            "tsConfig": "apps/app-container/tsconfig.app.json",
            "aot": true,
            "assets": [
              "apps/app-container/src/favicon.ico",
              "apps/app-container/src/assets",
              {
                "glob": "**/*",
                "input": "./node_modules/@ant-design/icons-angular/src/inline-svg/",
                "output": "/assets/"
              }
            ],
            "styles": [
              "apps/app-container/src/theme.less",
              "apps/app-container/src/styles.scss"
            ],
            "scripts": [
              "apps/app-container/src/assets/scripts.js"  <== 추가
            ]
          },

수행하면 네트워크에서 scripts.js 파일은 한번만 로딩되고, 이후 다양한 Web Components들은 자신의 애플리케이션 내역만 포함한 파일로 번들링하여 파일 사이즈를 줄일 수 있다. (참조)

Custom Element에서 common library를 window객체쪽으로 변경함.

 

scripts.js 로딩 후 monitor-es5.js 파일 로딩

 

 

참조

https://www.angulararchitects.io/aktuelles/angular-elements-part-i/

 

Angular Elements, Part I - ANGULARarchitects

A dynamic dashboard in four steps with Web Components

www.angulararchitects.io

https://ng.ant.design/docs/getting-started/en

 

NG-ZORRO - Ant Design Of Angular

An enterprise-class UI design language and Angular-based implementation with a set of high-quality Angular components, one of best Angular UI library for enterprises

ng.ant.design

https://medium.com/angular-in-depth/how-to-manually-bootstrap-an-angular-application-9a36ccf86429

 

How to manually bootstrap an Angular application

AngularInDepth is moving away from Medium. This article, its updates and more recent articles are hosted on the new platform inDepth.dev

medium.com

https://www.angulararchitects.io/aktuelles/your-options-for-building-angular-elements/

 

Your options for building Angular Elements - ANGULARarchitects

with the CLI

www.angulararchitects.io

https://indepth.dev/tiny-angular-application-projects-in-nx-workspaces/

 

Tiny Angular application projects in Nx workspaces

Use assets, styles, and environments workspace libraries to follow the Single Responsibility Principle. Step-by-step commands and instructions.

indepth.dev

https://nstudio.io/blog/custom-web-elements-with-angular-and-react

 

nstudio | Custom web elements for Angular and React with Nx + xplat

Passionate about implementing creative solutions for you. Technology, Consulting, Audio/Video Production. Web/Mobile apps, Angular/NativeScript plugins, Vue/NativeScript, product development, team training and help with project features/objectives.

nstudio.io

 

posted by 윤영식
2020. 5. 18. 14:56 React/Architecture

마이크로 프론트앤드는 마이크로 서비스처럼 전체 화면을 작동할 수 있는 단위로 나누어 개발한 후 서로 조립하는 방식이다. 여기서 작동 단위에 사용된 프론트앤드 프레임워크로 Angular 이든, React 또는 Vue 또는 Vanilla 자바스크립트에 상관하지 않고 조합 가능한 방법을 제공한다. 본글에서는 마이크로 프론트앤드 개발 방법중 Angular 프레임워크를 사용하면서 Web Components를 사용한 통합 방법에 대핸 알아보자.

 

마이크로 프론트앤드 기반 독립된 팀별 애플리케이션 개발

 

 

Micro Frontend 개념

마이크로 프론앤드 개념으로 개발을 하는 잇점은 대규모 엔터프라이즈 애플리케이션을 개발한다고 가정할 때, 각 팀별 또는 업무단위에 대해 Backend + Frontend 개발 후 통합하는 이슈를 줄일 수 있다. 

  • 작고, 응집력 있고 유지보수성을 가지는 코드베이스를 가질 수 있다. (Simple, decoupled codebase)
  • 분리배포가 용이하고, 자율적인 팀 조직운영이 수월해진다. (Independent deployment, Autonomous teams)
  • 프론트앤드 개발을 점진적 업그레이드 또는 재작성이 수월해진다. (Incremental upgrades)

하지만 단점도 존재한다. 

  • 배포 번들 사이즈가 커질 수 있다. (Payload size)
  • 서로간의 개발 환경의 차이로 복잡도가 올라간다. (Environment differences
  • 운영 및 거버넌스도 당연히 복잡해진다. (Operational governance complexity)

Thoughtworks의 Technology Radar에 의하면 Micro Frontend가 현재 적용 가능한(Adapt) 상황이다.

마틴 파울러의 글 (또는 번역글) 에서 잘 설명을 하고 있으니 참조하자.

 

 

 

Micro Frontend 통합 방법

독립적인 개발 및 배포

마이크로 프론앤드 방식으로 개발 후 각 단위 애플리케이션을 어떻게 통합할지 고려해야 한다. 통합할 때는 각 화면을 조합하는 컨테이너 애플리케이션이 있고, 그 하부에 들어가는 단위 애플리케이션이 존재한다. (참조)

  • 서버 템플릿 통합: 각 서버로 html 템플릿을 요청하고, 최종 응답서버에서 각 템플릿을 조합해서 응답을 보냄
    • 서버측에서 최종 화면을 조합한다.
  • 빌드타임 통합: 단위 애플리케이션을 패키지로 배포하고, package.json에 명시한 후 컨테이너 애플리케이션에서 import하여 사용하는 방법
    • 각 애플리케이션에 대한 런타임 대응이 안된다. 
    • 애플리케이션을 릴리즈하고 최종 애플리케이션에서 컴파일해야 한다. 
  • iframe 통합: 전통적인 방식이면서 가장 쉬운 방식이다.
    • 애플리케이션 통합의 유연성 높다.
    • 애플리케이션의 기술 종속성이 없다. 
    • routing, history, deep-link같은 것이 복잡해질 수 있다. 
    • 컨테이너 애플리케이션과 iframe에 들어가는 단위 애플리케이션간의 통신규약도 필요하다. 
    • UX가 iframe안에 갇히기 때문에 어색한 UI 표현을 가질 수 있다. 
  • Javascript를 통한 런타임 통합: iframe과 달리 유연한 통합이 가능하다. 현실적으로 가장 많이 사용하는 방식이다.
    • 컨테이너 애플리케이션을 단위 애플리케이션 번들을 <script> 태그를 통합 다운로드 받고
    • 약속된 초기화 메소드를 호출한다.
    • 클라이언트측에서 (브라우져) 통합한다.
  • Web Components를 통한 통합: HTML 커스텀 엘리먼트를 통한 통합방법, static, runtime 통합 둘 다 가능함.
    • Javascript를 통한 런타임 통합과 유사하지만 "The web component way"를 지향한다.
    • 클라이언트측에서 (브라우져) 통합한다.

Mirco Frontend 통합할 때 몇가지 고려사항

  • UI 스타일 일관성은 UI Component Library를 만들어 대응한다.
    • 한번에 만들지 말고, 중복코드가 발생하는 지점에서 만들고
    • 코드 일관성을 유지하는 팀이 수행한다. 
  • 어플리케이션 통신은 Custom events를 사용한다.
    • 커스텀 이벤트를 위해 PubSubJS를 고려해 보자.
    • 호출시 URL 라우팅에 넘기기
  • 백앤드 호출 API 구성
    • BFF(Backend for Frontend Pattern) 패턴으로 프론트앤드 전용 API를 갖는다. 
    • 별도의 데이터베이스를 가질 수도 있다.
    • 로그인은 인증 정보는 통합하는 Container가 소유한다. 

프론트앤드와 백앤드의 구조화

 

 

참조

https://micro-frontends.org/

 

Micro Frontends - extending the microservice idea to frontend development

Techniques, strategies and recipes for building a modern web app with multiple teams using different JavaScript frameworks.

micro-frontends.org

https://martinfowler.com/articles/micro-frontends.html

 

Micro Frontends

How to split up your large, complex, frontend codebases into simple, composable, independently deliverable apps.

martinfowler.com

번역글: https://medium.com/@juyeon.kate/micro-frontends-%EB%B2%88%EC%97%AD%EA%B8%80-1-5-29c80baf5df

 

Micro Frontends 번역글 1/5

이 글은, https://martinfowler.com/articles/micro-frontends.html 페이지를 번역한 글 입니다.

medium.com

https://www.thoughtworks.com/radar/techniques/micro-frontends

 

Micro frontends | Technology Radar | ThoughtWorks

This Technology Radar quadrant explores the techniques being used to develop and deliver software

www.thoughtworks.com

https://www.angulararchitects.io/aktuelles/angular-elements-part-i/

 

Angular Elements, Part I - ANGULARarchitects

A dynamic dashboard in four steps with Web Components

www.angulararchitects.io

 

posted by 윤영식
2020. 4. 23. 17:53 React/Architecture

React를 해야하지 접근했다 다시 놓고, 접근했다 다시 놓고 여러번의 시도를 하면서 이번에도? 생각할 수 있지만 이제는 정말 필요에 의해서 해야겠다는 생각이 든다. Angular를 할 수록 다른 것을 써봐야겠다는 욕구가 더 강해지고, 앞으로 개발할 소프트웨어에 어느 것이 더 적합할지 판단하기 위해 React를 다시 들여다 보고 있다. 그래서 Rethinkg React이지만 그속의 개념을 암기용으로 간단히 정리해 본다. 

 

 

ReactDom 

ReactDom은 Real DOM에 React Element 이 업데이트 하는 것을 관리한다.

SPA 구성할 때 ReactDOM.render(element, selector) 한번만 호출한다.

 

JSX

JSX는 React Element이다.

JSX를 React,createElement(type, properties, children)으로 쓸 수도 있다.

JSX의 attributes와 children은 "props" 객체를 통해 컴포넌트로 전달된다.

 

Component & props

컴포넌트는 SPA기반 개발의 경우 UI를 독립적으로 쪼개고, 격리시켜서 개발할 수 있고, 재사용 가능한  단위이다.

컴포넌트는 props를 가진다. props는 read only 이다.

컴포넌트는 순수함수로(Pure Function)로 컴포넌트를 만들 수 있다. 순수함수의 argument로 자동 전달된다.

컴포넌트는 ES6의 class 로 정의할 수 있다. 

컴포넌트는 데이터를 맵핑해서 JSX 조각을 리턴할 뿐이다. (순수함수는 JSX조합 리턴, 클래스 컴포넌트는 render메소드에서 JSX조합 리턴)

props 객체는 컴포넌트를 조합할(Composition) 때 하위 컴포넌트로 값을 내려 보낼수도 있는 객체이다. 

props 객체는 컴포넌트 -> 컴포넌트로의 값 전달 단위이다. Data flow Down => Top-Down unidirection flow

state가 있으면 stateful 컴포넌트, props만 사용하면 stateless 컴포넌트이다.

 

State & LifeCycle

Local state는 컴포넌트내부에서 DOM과 대화할 수 있는 유일한 수단이다.

Local state에 대해 컴포넌트안에서 값을 변경하고 JSX에서 반영한다. 

Local state는 setState의 Async 호출은 컴포넌트의 render()를 재수행토록 한다.

this.state 객체 값변경은 반드시 setState만을 통해 수행한다. 즉, this.state 값변경의 화면의 업데이트를 위한 것이다.

this.state와 this.props의 값은 Async하게 바뀐다. 

   - setState할 때 this.state값을 개별적으로 업데이트하면 변경된 값만 반영된다. 

   - setState안에서 this.state와 this.props를 사용할 경우 (state, props) => { ... } 함수를 등록하여 사용한다. 이때 첫번째 인자인 state는 이전 state 객체값이다. 

LifeCycle을 통해 업데이트 하는 시점을 잡는다. 컴포넌트 시작 componentDidMount, 종료되기전 componentWillUnmount

 

class Clock extends React.Component {
  constructor(props) {
    super(props);
    this.state = {date: new Date()};
  }

  componentDidMount() {
    this.timerID = setInterval(
      () => this.tick(),
      1000
    );
  }

  componentWillUnmount() {
    clearInterval(this.timerID);
  }

  tick() {
    this.setState({
      date: new Date()
    });
  }

  render() {
    return (
      <div>
        <h1>Hello, world!</h1>
        <h2>It is {this.state.date.toLocaleTimeString()}.</h2>
      </div>
    );
  }
}

ReactDOM.render(
  <Clock />,
  document.getElementById('root')
);
더보기
  1. When <Clock /> is passed to ReactDOM.render(), React calls the constructor of the Clock component. Since Clock needs to display the current time, it initializes this.state with an object including the current time. We will later update this state.
  2. React then calls the Clock component’s render() method. This is how React learns what should be displayed on the screen. React then updates the DOM to match the Clock’s render output.
  3. When the Clock output is inserted in the DOM, React calls the componentDidMount() lifecycle method. Inside it, the Clock component asks the browser to set up a timer to call the component’s tick() method once a second.
  4. Every second the browser calls the tick() method. Inside it, the Clock component schedules a UI update by calling setState() with an object containing the current time. Thanks to the setState() call, React knows the state has changed, and calls the render() method again to learn what should be on the screen. This time, this.state.date in the render() method will be different, and so the render output will include the updated time. React updates the DOM accordingly.
  5. If the Clock component is ever removed from the DOM, React calls the componentWillUnmount() lifecycle method so the timer is stopped.

 

이벤트 처리

이벤트 핸들러는 JSX안에서 함수 형태로 전달한다. 

함수 호출이 아닌, 함수를 서절하는 것이므로 preventDefault 하기위해 false를 리턴할 수 없는 구조이다. preventDefault()를 명시적으로 호출한다. 

React Element는 HTML의 이벤트가 아닌 SyntheticEvent를 사용해서 cross-browser 호환성을 고민할 필요가 없다.

function ActionLink() {
  function handleClick(e) {
    e.preventDefault();
    console.log('The link was clicked.');
  }

  return (
    <a href="#" onClick={handleClick}>
      Click me
    </a>
  );
}

TypeScript 방식의 경우 

  • { (params) => this.method(params) }
  • { this.method.bind(this, params) }
import React, { Component } from 'react';

interface State {
    isToggleOn: boolean;
}

export default class App2 extends Component<{}, State> {
    state: State = { isToggleOn: true };
    render() {
        return <button onClick={() => this.handleClick()}>{this.state.isToggleOn ? 'ON' : 'OFF'}</button>;
    }

    private handleClick() {
        this.setState(state => ({
            isToggleOn: !state.isToggleOn
        }));
    }
}

 

조건이 맞을 때 화면 렌더링 (Conditional Rendering)

{expression} 을 통해 JSX 를 리턴할 수 있다. 이를 위해 Short curcuit, 삼항식, 고차함수 사용가능. expression 이니깐...

expression의 리턴값이 null이면 화면 렌더링을 하지 않는다. 이때는 render가 호출안되고 componentDidUpdate만 호출된다.

 

 

Lists 와 Keys

JSX map의 리턴으로 받은 JSX List를 {[JSX, JSX, JSX]}으로도 화면 렌더링을 할 수 있다. 

map list할 때는 특별한 key attribute를 list element에 설정을 해야한다. 그래야 warning 안남, map에서 반드시 key 사용하기

Key는 아이템의 변경을 체크하는데 사용된다. key값은 unique해야 한다. map에서 index를 사용하지 말고 별도의 값을 사용하자

Key는 한번 순수함수 컴포넌트로 맵핑한 해당 컴포넌트에 할당한다. 

Key는 array안에서만 Unique하면 된다.

props.key는 map list에 예약 attribute이니 일반 컴포넌트에서 props로 key를 사용하지 말자

import React, { Component } from 'react';

// key설정하지 않고 반복하는 React Element
const ListItem = ({ value }) => <li>{value}</li>;
const numbers = [1, 2, 3, 4];
const List = ({ isToggleOn }) => {
    return isToggleOn ? (
        <ul>
            // map을 embedding했다. expression이니깐 당연히 가능
            {numbers.map((number, index) => (
                // 반복되는 React Element에 key를 설정한다. 
                <ListItem key={number.toString()} value={number} />
            ))}
        </ul>
    ) : (
        <div>There is no list.</div>
    );
};

interface State {
    isToggleOn: boolean;
}
export default class App2 extends Component<{}, State> {
    state: State = { isToggleOn: false };
    render() {
        return (
            <>
                <button onClick={() => this.handleClick()}>{this.state.isToggleOn ? 'ON' : 'OFF'}</button>
                <List isToggleOn={this.state.isToggleOn} />
            </>
        );
    }

    private handleClick() {
        this.setState(state => ({
            isToggleOn: !state.isToggleOn
        }));
    }
}

 

Forms 

form elements인 <input>, <textarea>, <select> 같은 것은 자신의 state를 가지고, 사용자 input을 기반으로 state를 업데이트한다.

React에 의해 값이 제어되는 input form element를 controlled component라 한다.

<select value="state value"> <option/> ... </select> 로 root <select>에서 value를 selected attribute로 대치한다. 

import React, { Component } from 'react';

interface State {
    value: string;
}
export default class App2 extends Component {
    state: State = { value: '0' };

    render() {
        return (
            <form onSubmit={this.handleSubmit.bind(this)}>
                <select value={this.state.value} onChange={this.handleChange.bind(this)}>
                    <option value="1">1</option>
                    <option value="2">2</option>
                    <option value="3">3</option>
                </select>
            </form>
        );
    }

    private handleSubmit(event) {
        event.preventDefault();
    }

    private handleChange(event) {
        this.setState({ value: event.target.value });
    }
}

<input type="file"> is uncotrolled component 이다. 

여러개의 input을 다룰때는 input 태그이 name을 통해 setState에 값을 할당한다.

setState는 부분적인 state를 현재 state에 합친다. 

Formik같은 패키지를 써보자.

 

 

Composition vs Inheritance

React는 컴폰넌트사용에서 Composition을 추천한다. 

props.children를 사용하거나, props를 통해 함수 컴포넌트를 전달할 수 있다. props통해 함수를 전달할 수 있음. (Lifting state up 참조)

// props.children을 사용하여 composition
function FancyBorder(props) {
  return (
    <div className={'FancyBorder FancyBorder-' + props.color}>
      {props.children}
    </div>
  );
}

function WelcomeDialog() {
  return (
    <FancyBorder color="blue">
      <h1 className="Dialog-title">
        Welcome
      </h1>
      <p className="Dialog-message">
        Thank you for visiting our spacecraft!
      </p>
    </FancyBorder>
  );
}

// props를 통해 컴포넌트를 전달하여 composition
function SplitPane(props) {
  return (
    <div className="SplitPane">
      <div className="SplitPane-left">
        {props.left}
      </div>
      <div className="SplitPane-right">
        {props.right}
      </div>
    </div>
  );
}

function App() {
  return (
    <SplitPane
      left={
        <Contacts />
      }
      right={
        <Chat />
      } />
  );
}

 

참조

- reactjs.org

posted by 윤영식
2019. 4. 10. 16:35 Angular/Architecture

두번째 Multi Application에 대한 Plugin 방식을 구성 테스트해 보자.

 

 

Library기반 Plugin 파일 만들기

플랫폼에 있는 플러그인이 아니라 플랫폼과 별개의 서비스에서 운영되는 플로그인을 플랫폼상에 렌더링하기 위해서는 UMD(Universal Module Definition) 방식으로 플러그인이 번들링 되어야 한다. UMD에 대해서는 본 블로그의 글을 참조한다.

$ ng g library plugin2 --publishable

 

plugin2 로 별도 번들링이 가능한 라이브러리 파일을 생성한다. 최종 npm scope를 jm으로 주었기 때문에 npm으로 설치했을 때 node_modules최종 명칭은 @jm/plugin2가 된다. libs/plugins/package.json의 설정 참조. build가 되는지 실행해 본다.

$ ng build plugin2

dist/libs/plugin2에 npm repository로 publish할 수 있는 형태의 다양한 포멧으로 번들링이 되었다. 해당 번들링은 ng-packagr를 기본으로 한 Angular Package Format에 따른다.

plugin2 라이브러리에 plugin2라는 이름의 컴포넌트를 생성하고 여기에 ngx-echart를 사용하는 샘플을 넣어보자.

// 컴포넌트 생성
$ ng g component plugin2 --project=plugin2

// echart 관련 라이브러리 및 echart angular wrapper 설치
$ npm i -S echarts ngx-echarts
$ npm i -D @types/echarts

plugin2/plugin2.component.html과 .ts에 ngx-echart의 예제 내용을 첨부한다. Plugin2Module에 bootstrap에 Plugin2Component를 설정한다.

// plugin2.component.html
<div echarts [options]="options" class="demo-chart"></div>

// plugin2.component.ts 
options 내용 설정: https://xieziyu.github.io/ngx-echarts/#/usage/basic 참조

// plugin2.module.ts
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { NgxEchartsModule } from 'ngx-echarts';

import { Plugin2Component } from './plugin2/plugin2.component';

@NgModule({
  declarations: [Plugin2Component],
  imports: [
    CommonModule,
    NgxEchartsModule
  ],
  exports: [
    Plugin2Component
  ],
  bootstrap: [
    Plugin2Component
  ]
})
export class Plugin2Module {}

plugin2 컴포넌트를 사용하는 별도 애플리케이션을 생성해 보자.

 

 

별도 애플리케이션 생성하여 Plugin2 테스트 하기

Multi Application을 가정하여 Plugin2를 사용하는 App2 애플리케이션을 생성하여 Plugin2가 잘 나오는지 테스트 한다.

$ ng g application app2

app2 가 실행할 때 4300 port 를 사용토록 angular.json에 포트설정을 추가한다.

"serve": {
          "builder": "@angular-devkit/build-angular:dev-server",
          "options": {
            "browserTarget": "app2:build",
            "port": 4300
          },
 ... 중략 ...

 $ ng s app2 

apps/app2/src/app/app.module.ts 와 app.component.html을 수정한다. import 할 때"@jm/plugin2"로 사용함을 주의한다.

// app.component.html
<jm-plugin2></jm-plugin2>

// app.module.ts
import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';
import { Plugin2Module } from '@jm/plugin2';

import { AppComponent } from './app.component';

@NgModule({
  declarations: [AppComponent],
  imports: [
    BrowserModule,
    Plugin2Module
  ],
  providers: [],
  bootstrap: [AppComponent]
})
export class AppModule {}

"ng s app2" 실행하고 http://localhost:4300 으로 호출하면 샘플 echart가 보인다.

 

 

Plugin2 번들링 하기

app2 애플케이션에 plugin2 컴포넌트가 잘 나오는 것을 확인 하였다. plugin2 번들링을 위해 한가지 설정 옵션을 변경하여 다시 번들링한다. build시에 에러가 발생하면 ng-packagr v5.0.1 이상을 설치한다.

//libs/plugin2/src/tsconfig.lib.json 에서 skipTemplateCodegen을 false로 변경한다
  "angularCompilerOptions": {
    "annotateForClosureCompiler": true,
    "skipTemplateCodegen": false,

// ng-packagr 관련 업데이트 
$ npm update ng-packagr
$ npm i -D tsickle

//다시 번들링한다. 
$ ng build plugin2
Building Angular Package
Building entry point '@jm/plugin2'
Compiling TypeScript sources through ngc
Bundling to FESM2015
Bundling to FESM5
Bundling to UMD
No name was provided for external module 'ngx-echarts' in output.globals – guessing 'ngxEcharts'
Minifying UMD bundle
Copying declaration files
Writing package metadata
Removing scripts section in package.json as it's considered a potential security vulnerability.
Built @jm/plugin2
Built Angular Package!
 - from: /Users/dowonyun/prototyping/jamong/libs/plugin2

skipTemplateCodegen를 false로 하면 ngfactory파일까지 생성되고 해당 파일은 Angular메타정보를 해석해 놓은 파일로 동적으로 파일을 로딩할 때 사용할 것이다. 다음으로 bundle 된파일을 다시 하나의 파일로 합치기 위해 package.json에 script를 등록한다.

// package.json
"scripts": {
   ... 중략 ...
   "build:plugin2": "rollup dist/libs/plugin2/esm2015/lib/plugin2.module.ngfactory.js --file dist/apps/api/plugin2.js --format umd --name plugin2"
}

// bundling 
$ npm run build:plugin2

dist/apps/api/plugin2.js 파일이 생성된다. 또는 ROOT에 rollup.config.js 파일을 생성하여 관리할 수도 있다.

//rollup.config.js
export default [
  {
    input: 'dist/libs/plugin2/esm2015/lib/plugin2.module.ngfactory.js',
    output: [
      {
        name: 'plugin2',
        file: 'dist/apps/api/plugin2.js',
        format: 'umd'
      }
    ]
  }
];

//build 명령
$ rollup -c

두가지의 명령을 수행한다.

  • ng build plugin2
  • npm run build:plugin2  또는 rollup.config.js를 설정하였다면 rollup -c

 

Plugin2 파일 API 서비스 추가

plugin2 파일을 동적으로 로딩하기 위해 다음과 같이 역할을 추가한다.

  • Dev Server는 Platform 서버 역할
  • API Server는 별개 애플리케이션 서버로 보고 Plugin-2 파일을 서비스하는 역할

이제 API Server에서 plugin2.js 파일을 읽어서 파일 내용을 전달하는 API를 추가한다. NetsJS는 NodeJS위에 구동되는 서버프레임워크로 Angular와 유사한 Syntax를 통해 MVC 패턴으로 개발한다.

  • GET으로 plugin2를 호출하면 파일을 읽는다.
  • 파일 내용을 return하면 끝!
// apps/api/src/app/app.controller.ts

import { Controller, Get } from "@nestjs/common";

import { Message } from "@jm/api-interface";
import { AppService } from "./app.service";

const path = require('path');
const fs = require('fs-extra');

@Controller()
export class AppController {
  constructor(private readonly appService: AppService) {}

  @Get("hello")
  getData(): Message {
    return this.appService.getData();
  }

  @Get("plugin2")
  getPlugin2() {
    const fileName = path.join(process.cwd(), 'dist/apps/api/plugin2.js');
    const plugin2 = fs.readFileSync(fileName, 'utf8');
    return plugin2;
  }
}

테스트를 해보자. API Server기본 context는 api로 http://localhost:3333/api/plugin2 로 호출한다. 번들링 파일 내용이 응답됨을 확인했다.

// API Server 기동
$ ng s api

 

 

원격 Plugin2을 Lazy Loading 하기

plugin2.js 파일을 다운로드 받아 로딩하기 위해 apps/jamong/src/app/app.component.html과 .ts에 로딩 구문을 추가한다.

// app.component.html
<button (click)="loadPlugin()">Load plugin</button>
<lazy-af *ngIf="plugin1Path" [moduleName]="plugin1Path"></lazy-af>

<p></p>

<button (click)="loadPlugin2()">Load plugin2</button>
<ng-template #Plugin></ng-template>

app.component.ts

  • plugin2.js 파일을 요청하기 위해 HttpClient를 사용
  • Plugin2ModuleNgFactory안의 Angular MetaData를 통해 관련 컴포넌트들 JIT 컴파일 수행
  • @herodevs/dynamic-af 패키지이 DynamicAFService 서비스를 injector받는다.
  • 이때 plugin2.js 소스내의 require하는 모듈을 위해 import * as from ''; 하여 아래 소스처럼 설정한다.
import { Component, ViewChild, ViewContainerRef, NgModuleFactory, Injector } from '@angular/core';
import { HttpClient, HttpErrorResponse } from '@angular/common/http';
import { throwError } from 'rxjs';
import { catchError } from 'rxjs/operators';
import { DynamicAFService } from '@herodevs/dynamic-af';

import * as common from '@angular/common';
import * as commonHttp from '@angular/common/http';
import * as core from '@angular/core';
import * as router from '@angular/router';
import * as rxjs from 'rxjs';
import * as rxjsOperators from 'rxjs/operators';
import * as ngxEcharts from 'ngx-echarts';

@Component({
  selector: 'jm-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.scss']
})
export class AppComponent {

  @ViewChild('Plugin', {read: ViewContainerRef}) pluginVcr: ViewContainerRef;
  plugin1Path: string;
  loadedPlugins: any = {};

  constructor(
    private http: HttpClient,
    private injector: Injector,
    private lazyService: DynamicAFService
  ) { }

  loadPlugin() {
    this.plugin1Path = 'apps/jamong/src/app/plugin1/plugin1.module#Plugin1Module';
  }

  loadPlugin2() {
    const moduleFactory = this.loadedPlugins['api/plugin2'];
    if (!moduleFactory) {
      this.loadRemoteComponent();
    } else {
      this.lazyService.createAndAttachModuleAsync(moduleFactory, this.injector, { vcr: this.pluginVcr });
    }
  }

  private loadRemoteComponent() {
    let moduleFactory: NgModuleFactory<any>;
    this.http.get('api/plugin2', { responseType: 'text' })
      .pipe(
        catchError(this.handleError)
      ).subscribe((compiledSource: any) => {
        const exports = {};
        const modules = {
          '@angular/core': core,
          '@angular/common': common,
          '@angular/common/http': commonHttp,
          '@angular/router': router,
          'rxjs': rxjs,
          'rxjs/operators': rxjsOperators,
          'ngx-echarts': ngxEcharts
        };
        const require: any = (module) => modules[module];
        // tslint:disable-next-line: no-eval
        eval(compiledSource);
        moduleFactory = exports['Plugin2ModuleNgFactory'];
        this.loadedPlugins['api/plugin2'] = moduleFactory;
        this.lazyService.createAndAttachModuleAsync(moduleFactory, this.injector, { vcr: this.pluginVcr});
      });
  }

  private handleError(error: HttpErrorResponse) {
    if (error.error instanceof ErrorEvent) {
      // A client-side or network error occurred. Handle it accordingly.
      console.error('An error occurred:', error.error.message);
    } else {
      // The backend returned an unsuccessful response code.
      // The response body may contain clues as to what went wrong,
      console.error(`Backend returned code ${error.status}, body was: ${error.error}`);
    }
    return throwError(error);
  }
}

Load plugin2를 호출하였을 때 원격 처리 결과

위의 모든 소스는 GitHub에...

 

 

<참조>

- ngx-echarts

 

ngx-echarts demo

 

xieziyu.github.io

- Another Angular Plugin Example

 

iwnow/angular-plugin-example

Angular plugin with AOT and separate build. Contribute to iwnow/angular-plugin-example development by creating an account on GitHub.

github.com

- Rollup Configuration

 

Bundling Your JavaScript Library with Rollup

A step-by-step tutorial on how to bundle your JavaScript library using Rollup. You’ll also learn how to publish those bundles to NPM.

bagja.net

 

posted by 윤영식
2019. 4. 8. 17:45 Angular/Architecture

플랫폼위에 필요에 따라 플러그인 파일을 동적으로 다운로드 받아 운영하는 방식에 대한 기술 검토를 한다. 하나는 Single Application이면서 필요시점에 파일을 다운로든 받는 방식과 두번째는 다른 애플리케이션의 파일을 다운로드 받아 처리하는 방식이다. 

 

첫번째는 Single Application Plugin 방식으로 하나의 애플리케이션이 서비스되면서 필요시점에 파일을 다운로드 받아서 사용하는 방법이다. 

  • platform, shared 파일을 최초에 다운도로드 받아 처리한다. 
  • 화면-1로 페이지가 전화될 때 plugin-1 파일을 다운로드 받아 처리한다.
  • 즉, 플랫폼과 플러그인 파일이 같은 서버에 존재하는 경우이다.

 

두번째는 Multi Application Plugin 방식으로 여러 애플케이션을 하나의 플랫폼에서 운영하는 방식이다. 

  • Remote Server-1/2/3은 각기 다른 애플리케이션이라 본다. 
  • Proxy Server는 플랫폼 서비스를 담당하고, Remote Server로의 요청을 중간에서 처리하는 Proxy 역할을 수행한다. 이때 Auth에 대한 권한을 처리할 수도 있다. 
  • 화면-1에 대한 요청을 Proxy Server에 하면 Url context 구분을 통해 Remote Server-1 서비스에 Plugin-1 파일을 요청 처리한다.
  • 즉, 플랫폼과 플러그인 파일이 존재하는 서버 위치가 서로 틀리다.

 

Single Application Plugin 방식 구성

@angular/cli와 @nrwl/schematics를 통해 애플케이션을 생성한다.  Node 버전은 LTS최신 버전을 사용하고 yarn도 설치한다.

$ node --version
v10.15.3
$ npm i -g yarn @angular/cli @nrwl/schematics @nestjs/cli
+ @angular/cli@7.3.8
+ @nrwl/schematics@7.8.0
+ @nestjs/cli@6.2.1

create-nx-workspace 명령으로 jamong이라는 플랫폼을 생성한다. SCSS 기반에 NestJS를 포함한 FullStack을 선택하자.  jamong 폴더 밑으로 apps/jamong Frontend 애플리케이션이 생성되었다. apps/api는 Backend 애플리케이션이다. ng s 명령을 수행하여 frontend, backend 서버를 각각 실행한다. frontend는 apps/jamong/proxy.conf.json 파일안에 proxy 경로로 /api가 설정되어 있다.

$ create-nx-workspace jamong --npm-scope=jm
$ cd jamong
$ ng s jamong (또는 ng serve jamong)
$ ng s api (또는 ng s api)

http://localhost:4200 을 호출한다. Dev Server와 API Server연결은 다음과 같다. 

 

plugin1 컴포넌트와 모듈을 생성한다. module을 생성하고 component를 생성하면 plugin1.module.ts의 declarations안에 자동으로 Plugin1Component가 설정된다. module 파일에 bootstrap 설정을 한다.

$ ng g module plugin1 --project=jamong
CREATE apps/jamong/src/app/plugin1/plugin1.module.ts (191 bytes)
$ ng g component plugin1 --project=jamong
CREATE apps/jamong/src/app/plugin1/plugin1.component.scss (0 bytes)
CREATE apps/jamong/src/app/plugin1/plugin1.component.html (26 bytes)
CREATE apps/jamong/src/app/plugin1/plugin1.component.spec.ts (635 bytes)
CREATE apps/jamong/src/app/plugin1/plugin1.component.ts (273 bytes)
UPDATE apps/jamong/src/app/app.module.ts (454 bytes)

// plugin1.module.ts
@NgModule({
  declarations: [Plugin1Component],
  imports: [
    CommonModule
  ],
  bootstrap: [Plugin1Component]
})
export class Plugin1Module { }

plugin module의 파일을 별도 파일로 번들링하기 위해 angular.json에서 "projects"/"jamong"/"architect"/"build"/"options" 안에 "lazyModules"설정을 한다.

  "projects": {
    "jamong": {
      "root": "apps/jamong/",
      "sourceRoot": "apps/jamong/src",
      "projectType": "application",
      "prefix": "jm",
      "schematics": {
        "@nrwl/schematics:component": {
          "style": "scss"
        }
      },
      "architect": {
        "build": {
          "builder": "@angular-devkit/build-angular:browser",
          "options": {
            "outputPath": "dist/apps/jamong",
            "index": "apps/jamong/src/index.html",
            "main": "apps/jamong/src/main.ts",
            "polyfills": "apps/jamong/src/polyfills.ts",
            "tsConfig": "apps/jamong/tsconfig.app.json",
            "assets": ["apps/jamong/src/favicon.ico", "apps/jamong/src/assets"],
            "styles": ["apps/jamong/src/styles.scss"],
            "scripts": [],
            "es5BrowserSupport": true,
            "lazyModules": [
              "apps/jamong/src/app/plugin1/plugin1.module"
            ]
          },
    ... 중략 ...

"ng s jamong" restart하면 plugin1 모듈 파일이 별도 생성됨을 알 수 있다. 물리적은 파일로 build 하고 싶다면 "ng build jamong"을 수행한다. 

$ ng build jamong

다음으로 app2-jamong-src-app-plugin1-plugin1-module.js 파일을 Dynamic Loading하는 Loader인 lazy-af를 설치한다. lazy-af의 자세한 소스는 github에서 확인한다. lazy-af는 lazy.module.ts에 NgModuleFactoryLoader로 SystemJSNgModuleLoader를 설정해 사용한다.

$ npm install @herodevs/lazy-af

// https://github.com/herodevs/herodevs-packages/blob/master/projects/lazy/src/lib/lazy.module.ts
@NgModule({
  imports: [],
  declarations: [LazyAFComponent],
  exports: [LazyAFComponent],
  providers: [{ provide: NgModuleFactoryLoader, useClass: SystemJsNgModuleLoader }],
})
export class LazyModule {}

lazy-af관련 모듈을 apps/jamong/src/app/app.module.ts에 import 한다.

import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';

import { LazyModule } from '@herodevs/lazy-af';

import { AppComponent } from './app.component';
import { HttpClientModule } from '@angular/common/http';

@NgModule({
  declarations: [AppComponent],
  imports: [
    BrowserModule, 
    HttpClientModule,
    LazyModule
  ],
  providers: [],
  bootstrap: [AppComponent]
})
export class AppModule {}

apps/jamong/src/app/app.component.html 과 .ts 와 plugin1/plugin1.component.html 과 .ts을 수정한다. 

// app.component.html
<button (click)="loadPlugin()">Load plugin</button>
<lazy-af *ngIf="plugin1Path" [moduleName]="plugin1Path"></lazy-af>

// app.component.ts
@Component({
  selector: 'jm-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.scss']
})
export class AppComponent {

  plugin1Path: string;

  loadPlugin() {
    this.plugin1Path = 'apps/jamong/src/app/plugin1/plugin1.module#Plugin1Module';
  }
}

// plugin1/plugin1.component.html
<div style="padding-top: 20px">
  Plugin 1
</div>
<div>Message: {{ hello$ | async | json }}</div>

// plugin1/plugin1.component.ts
import { Component } from '@angular/core';
import { Message } from '@jm/api-interface';
import { HttpClient } from '@angular/common/http';

@Component({
  selector: 'jm-plugin1',
  templateUrl: './plugin1.component.html',
  styleUrls: ['./plugin1.component.scss']
})
export class Plugin1Component {
  hello$ = this.http.get<Message>('/api/hello');
  constructor(private http: HttpClient) { }
}

수정을 반영하고 실행을 한다. 

  1. Load plugin 버튼을 클릭한다.
  2. apps-jamong***.js 파일을 다운로드 한다. 
  3. Angular F/W 모듈을 해석한후 Plugin1Component를 렌더링한다.

지금까지의 소스 - github

다음 글에서 Multi Application의 Plugin 방식을 살펴보자.

 

<참조>

- nx development

 

Nx: Angular CLI power-ups for modern development

With Nx, you can develop multiple full-stack applications holistically and share code between them all in the same workspace. Add Cypress, Jest, Prettier, and Nest into your dev workflow.

nx.dev

- Angular Code Syntax와 유사한 Node 서비스 개발 프레임워크 NestJS

 

NestJS - A progressive Node.js web framework

NestJS is a framework for building efficient, scalable Node.js web applications. It uses modern JavaScript, is built with TypeScript and combines elements of OOP (Object Oriented Progamming), FP (Functional Programming), and FRP (Functional Reactive Progra

nestjs.com

- Angular Module Loader - lazy-af

 

@herodevs/lazy-af

This component allows you to lazily load your Angular module whenever you want, instead of being restricted to lazy loading on route changes.

www.npmjs.com

 

posted by 윤영식