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

Publication

Category

Recent Post

2021. 8. 5. 09:29 React/Architecture

NX + Angular 기반 개발시 엔터프라이즈 애플리케이션 개발방법에 대한 글을 정리한다. Micro Frontend 개발방법이라기 보다는 애플리케이션을 개발하며 지속적으로 확장할 필요가 있을 때 관심사들을 어떻게 분리하여(Bounded Context) 개발할 수 있을지 보여준다.

 

 

https://indepth.dev/the-shell-library-patterns-with-nx-and-monorepo-architectures/

 

Variations of the Shell Library pattern with Nx & Angular Monorepos

Compare shell library patterns to pick the right one for your Angular monorepo. Nrwl feature shell, Manfred Steyer shell and Composite shell libraries.

indepth.dev

https://connect.nrwl.io/app/books/enterprise-angular-monorepo-patterns

 

Nrwl Connect

Directly leverage Nrwl's Angular expertise. Ship more features in less time.

connect.nrwl.io

 

 

nx.dev를 이용한 라이브러리 관리

Nx는 하나의 레파지토리에서 멀티 애플리케이션과 라이브러리를 개발할 수 있는 환경을 제공한다. @angular/cli를 확장한 형태로 Angular, React, NodeJS를 함께 개발하고 별개로 번들링 배포할 수 있다. Nx 설치는 이전 글을 참조한다.

 

workspace/libs 하위로 폴더를 구분하여 업무를 개발할 수 있고, workspace/apps 의 애플리케이션에서 libs의 내용을 통합하여 사용하는 방법을 제시한다. 

  • application
    • application 안에서 sub-domain을 import하거나, routing 한다.
    • sub-domain을 통합하는 방식이다. 
    • 애플리케이션, web, desktop(using Eletron), mobile(Ionic)을 이용하여 sub-domain을 사용한다.
  • sub-domain
    • 업무 도메인이다.
    • sub-domain은 Bounded Context 이다.
    • Bounded Context는 업무 논리적으로 분리될 수 있는 단위이다.
    •  feature-shell
      • 업무 도메인의 페이지를 통합하고 routing을 설정한다. 
      • sub-domain 안에 1개 존재한다. 
      • shell은 sub-domain을 접근토록 하는 entry point 모듈이다.
      • shell orachestrate the routes of "Bounded Context"
    • feature-<bizName>
      • 일반적으로 웹 화면(Page) 하나라고 정의하자.
      • sub-domain 안에는 feature-<bizName>가 여러개 존재할 수 있다.

 

Nx applications과 libraries 폴더 구조

NX Workspace 의 apps, libs 폴더 구성 예

 

Feature-shell에서 sub-doamin(Bounded Context) 라우팅하기

shell에서 Bounded Context 즉, sub-domain을 routing한다.

 

애플리케이션에서 Feature-shell 라우팅하기. 애플리케이션에서 feature-shell을 조합하여 사용한다.

애플리케이션에서 feature-shell  라우팅

 

 

NX에서 Library 분류 및 개발 방법

nx workspace의 libs/ 밑으로 라이브러리를 개발할 때 다음과 같이 분류하여 개발한다. 

  • Feature-Shell library
    • 업무 sub-domain을 통합하여 애플리케이션에서 사용할 수 있도록 한다.
  • UI library
    • presentation component로써 버튼, 모달같은 업무 로직이 없는 컴포넌트들
  • Shared library
    • Data Access library
      • REST API 서비스
      • WebSocket 통신
      • Ngrx/Store 기반 State 관리
    • Utils library
      • 유틸리티 서비스

 

NX에서 tag 구분 keywords

  • scope
    • sub-domain 
    • grouping folder 
  • type
    • feature-shell
    • ui
    • data-cess
  • platform
    • desktop
    • mobile

예) scope:shared, type:feature,platform:desktop

 

각 라이브러리의 특성에 맞게 libs/ 밑으로 폴더/폴더 형태의 grouping folder 구조로 개발한다. 

예로 booking 폴더 밑에 feature-shell libary가 존재

 

feature-shell의 모듈 명칭은 "feature-shell" 으로 한다.

nx를 이용하여 library 생성시 옵션들

  • --directory=<grouping folder name>
  • --routing: forChild routing 자동 설정
  • --lazy: application에 lazy routing 자동 설정, 반드시 parent-module을 지정함.
  • --parent-module=<application module path and file name>: lazy 옵션 설정시 lazy routing 설정할 module 위치를 지정함.
  • --tags=<tag>,<tag>: nx.json에 포함됨, nx.json에서 직접 수정 및 추가 가능 예) --tags=scope:shared,type:feature
  • --publishable: 특별히 npm registry에 publish 할 수 있는 library를 만들고 싶을 때 사용한다. ng-packagr 기반이다.

//Command
nx g lib feature-<sub-domain name> --routing --directory=<grouping folder name> --lazy --parent-module=apps/<application>/src/app/app.module.ts

//Sample
$ nx g lib feature-shell --routing --directory=admin --lazy --parent-module=apps/app-container/src/app/app.module.ts
? Which stylesheet format would you like to use? SASS(.scss)  [ http://sass-lang.com   ]
CREATE libs/admin/feature-shell/README.md (162 bytes)
CREATE libs/admin/feature-shell/tsconfig.lib.json (459 bytes)
CREATE libs/admin/feature-shell/tslint.json (276 bytes)
CREATE libs/admin/feature-shell/src/index.ts (50 bytes)
CREATE libs/admin/feature-shell/src/lib/admin-feature-shell.module.ts (367 bytes)
CREATE libs/admin/feature-shell/src/lib/admin-feature-shell.module.spec.ts (432 bytes)
CREATE libs/admin/feature-shell/tsconfig.json (138 bytes)
CREATE libs/admin/feature-shell/jest.config.js (405 bytes)
CREATE libs/admin/feature-shell/tsconfig.spec.json (258 bytes)
CREATE libs/admin/feature-shell/src/test-setup.ts (30 bytes)
UPDATE angular.json (15807 bytes)
UPDATE nx.json (1098 bytes)
UPDATE tsconfig.json (863 bytes)

 

 

품질 및 일관성 유지 방법

  • Single Version을 apps와 libs에 적용한다. 
    • package.json에서 관리한다. 
  • Code Formatting은 prettier를 사용한다.
  • Library Dependencies 관리
    • library 순환 참조는 안된다. 
    • library가 application을 import하거나 의존하지 않는다. 
    • npm run dep-graph 또는 npm run affected:dep-graph 수행하면 라이브러리 의존관계도가 자동생성되어 웹브라우져에서 확인할 수 있다.
    • MS Code Editor의 Plugin으로 "Nx Console"을 설치하면 Editor에서 바로 수행해 볼 수도 있다.

의존성 그래프를 웹에서 확인

 

Nx Console이 Left Menu 하단에 N> 아이콘으로 있고, 명령어 목록을 클릭한다.

 

 

Nx Schematics 관리

Nx에 Built-in 된 schematics

  • ngrx: Ngrx/store 기반으로 reducer, action, selector, facade를 자동 생성함
  • node: Express node application의 경우 
  • jest: unit test
  • cypress: E2E test
  • etc..

Custom schematic 만들기

  • 개발자들이 샘플코드를 copy & paste하지 않고 schematic을 통해 업무 개발을 위한 파일을 생성토록할 때 사용한다.
  • nx g workspace-schematic <custom-schematic-name> 명령을 수행하면 workspace/tools/ 폴더 밑에 schematic파일이 생성됨.
    • 예제 파일 생성: nx g workspace-schematic data-access-lib 수행
    • 예제 파일 수행: npm run workspace-schematic data-access-lib <name>

index.ts에서 개발함

//index.ts에 ngrx를 추가로 적용한 예제

import { chain, externalSchematic, Rule } from '@angular-devkit/schematics';
import * as path from 'path';

export default function (schema: any): Rule {
    if (!schema.name.startsWith('data-access-')) {
        throw new Error(`Data-access lib names should start with 'data-access-'`);
    }
    const stateName = schema.name.replace('data-access-', '');
    return chain([
        externalSchematic('@nrwl/workspace', 'lib', {
            name: schema.name,
            tags: 'type:data-access',
        }),
        externalSchematic('@nrwl/schematics', 'ngrx', {
            name: stateName,
            module: path.join('libs', schema.name, 'src', 'lib', `${schema.name}.module.ts`),
        }),
    ]);
}

 

관련소스: github.com/ysyun/blog-5730-micro-demo.git

 

 

참조

https://indepth.dev/the-shell-library-patterns-with-nx-and-monorepo-architectures/

 

Variations of the Shell Library pattern with Nx & Angular Monorepos

Compare shell library patterns to pick the right one for your Angular monorepo. Nrwl feature shell, Manfred Steyer shell and Composite shell libraries.

indepth.dev

NX에서 Eletron, Ionic, NativeScript 통합 개발하기

https://nstudio.io/xplat/fundamentals/architecture

 

nstudio | xplat xplat/fundamentals/architecture - cross platform tools for Nx workspaces

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 윤영식
2021. 8. 5. 09:28 Angular/Concept

Nx.dev 기반개발에서 Angular 모듈을 Router기반으로 Lazy Loading하기와 Router를 사용하지 않고 Lazy Loading하는 방법에 대해 알아보자. 알단 Lazy Loading 설정을 하게되면 Angular 컴파일시에 자동으로 모듈단위 Code Splitting하여 번들링한다. 따라서 필요한 시점에 필요한 파일만 다운로드를 받기때문에  TTI(Time ot interactive)시간 즉, 애플리케이션 응답성능을 높일 수 있다. 참고로 Webpack 5 (핸재 beta) 에서 Federation Module 개념을 구현하였는데, Angular에서 이와 비슷한 기능을 Angular Module단위 개발로 제공한다.

 

NX 환경 설정

  • NodeJS v12.* 사용
  • @angular/cli와 @nwrl/ci 설치
  • create-nx-workspace 이용하여 nx 기반 환경 생성
  • yarn통한 node modules 설치
// NVM 설치 (https://github.com/nvm-sh/nvm)
$ nvm install 12.16.2
$ nvm alias default 12.16.2
$ nvm use 12.16.2

// 최신버전들 설치
$ npm i -g @angular/cli@latest
$ npm i -g @nrwl/cli@latest
$ npm i -g yarn@latest
$ npm install typescript@3.8.3 --save-dev --save-exact

// create-nx-workspace
$ npx create-nx-workspace@latest
  ? Worspace name   demo
  ...
$ cd demo
$ yarn install

 

 

Router 를 통한 Lazy Loading

  • nx 명령을 통해 Lazy loading할 library를 생성한다.
  • nx 명령을 통해 Lazy loading되어 보여질 component를 생성한다.
  • libs/lazyview/src/lib/lazyview.module.ts 에 RouterModule.forChild 설정
  • lazyview모듈을 로딩하는 "app-container" 애플리케이션의 tsconfig.app.json에 "include" path 설정
  • apps/app-container/src/app/app-routing.module.ts 안에 loadChildren 을 통해 lazyview 모듈을 import 한다.

libs/lazyview 밑에 라이브러리 생성

// 1) libs/lazyview 라이브러리 생성 - 라이브러리는 모듈단위
$ nx g lib --routing --lazy lazyview


// 2) 컴포넌트 생성
$ nx g c lazypage --project=lazyview


// 3) libs/lazyview/src/lib/lazyview.module.ts에 forChild 설정
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { RouterModule } from '@angular/router';
import { LazypageComponent } from './lazypage/lazypage.component';
@NgModule({
    imports: [
        CommonModule,

        RouterModule.forChild([
            { path: '', pathMatch: 'full', component: LazypageComponent }
        ]),
    ],
    declarations: [LazypageComponent],
})
export class LazyviewModule {}


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


// 5) app-routing.module.ts 에 loadChildren으로 '@micro-demo/lazyview' 설정
import { NgModule } from '@angular/core';
import { Routes, RouterModule } from '@angular/router';
const routes: Routes = [
    { path: '', pathMatch: 'full', redirectTo: '/welcome' },
    { path: 'lazyview', loadChildren: () => import('@micro-demo/lazyview').then((m) => m.LazyviewModule) },
    { path: 'welcome', loadChildren: () => import('./pages/welcome/welcome.module').then((m) => m.WelcomeModule) },
];
@NgModule({
    imports: [RouterModule.forRoot(routes)],
    exports: [RouterModule],
})
export class AppRoutingModule {}

"nx serve"  수행한 후 "http://localhost:4200/lazyview" 로 호출한다.

micro-demo-lazyview.js 파일을 code splitt되어 호출시 다운로드된다.

소스: 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

 

 

 

참조

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

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/2019/08/ngperf-route-level-code-splitting/

 

Angular Performance: Route Level Code Splitting

Learn about route-level code splitting and lazy loading with Angular

juristr.com

https://medium.com/angular-in-depth/lazy-load-components-in-angular-596357ab05d8

 

Lazy load components in Angular

Lazy load Angular components with Ivy and Angular 9

medium.com

https://indepth.dev/webpack-5-module-federation-a-game-changer-in-javascript-architecture/

 

Webpack 5 Federation. A Game-changer to Javascript architecture.

Module federation — The Javascript equivalent of what Apollo did with GraphQL. Multiple Webpack builds work together, sharing the dependency graph at runtime. Multiple bundles working as an SPA

indepth.dev

 

posted by 윤영식
2021. 8. 5. 09:28 Angular/Concept

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

 

posted by 윤영식
2020. 10. 27. 18:33 Cloud & AWS/Docker & Kubernetes

백앤드 10년 프론트앤드 10년 이젠 서비스 만들기 10년을 시작해 본다. 첫번째로 도커 개념을 이해하고 나만의 환경을 만들어 본다. 

 

 

개념과 기본 명령어

도커는 프로세스를 격리시켜 실행해주는 도구이다. 

도커 = 도커 엔진 + 도커 클라이언트로 클라이언트는 사용자가 명령을 전달하고, 서버는 명령을 수행한다. 서버는 보통 시스템 서비스로 등록된다. 

예) 도커 목록 보기

$ docker ps

exit 명령으로 container가 죽은 목록을 확인 할 때 -a 옵션
$ docker ps -a

 

도커 이미지 = 실행을 위한 애플리케이션 파일의 집합

IMAGE ID는 고유의 해쉬값이다.  

$ docker images

REPOSITORY          TAG                 IMAGE ID            CREATED             SIZE
run-java            1.0                 8e445847d25d        16 months ago       643MB
tomcat              8.5.15              b8dfe9ade316        3 years ago         334MB
java                latest              d23bdf5b1b1b        3 years ago         643MB

 

이미지는 원격의 도커 저장소에서 pull받을 수도 있다. tag를 지정하지 않으면 기본 latest가 적용된다.

$ docker pull <image_name>:<tag>

예)
$ docker pull centos 

 

도커 실행예 -it 는 쉘을 실행하기 위한 옵션, bash를 실행한다. --name 옵션을 주면 실행 이미지의 별칭을 설정할 수 있다.

$ docker run -it <ImageName:tag> <command>

예)
$ docker run -it centos:latest bash

 

죽은 컨테이너 다시 시작하기는 container Id를 사용한다.

$ docker restart <container-hash-id>

예)
$ docker ps -a 로 container id 확인
$ docker restart afb7d8bdrexac
$ docker ps

프로세스와 터미널 상의 입출력 연결은 attach
$ docker attach afb7d8bdrexac

 

도커 컨테이너는 도커 이미지를 기반으로 실행된 격리된 프로세스이다. 따라서 컨테이너는 가상머신이라기 보다는 프로세스이다. 

이미지의 변경사항을 파악하기는 docker diff 명령을 사용한다. A (ADD), C (Change), D (Delete) 이다. 기존 컨테이너에 새로운 기능을 넣고 새로운 이미지를 만들려면 docker commit 명령을 사용한다. rm은 컨테이너 삭제, rmi는 이미지 삭제이다.

$ docker diff <container id>

$ docker commit <container id> <image name>

종료 상태 컨테이이너 ID를 가지고 rm으로 삭제한다.
$ docker ps -a 

$ docker rm <container id>

$ docker rmi <image name> (또는 image id)

 

 

 

Dockerfile로 이미지 만들기

Docker 이미지를 만들수 있는 DSL를 통해 Dockerfile 파일 내용을 구성한다. 

  • FROM: 어떤 이미지로부터 새로운 이미지를 생성할지 지정한다.
  • RUN: 실행할 컨테이너안에서 실행할 명령을 정의한다. 이미지 빌드시 한번만 수행한다. 다른 명령 이어 수행 && 다음줄 이어서 \ 사용
  • COPY: 호스트 머신의 파일이나 폴더를 도커 컨테이너 안으로 복사
  • ADD: COPY와 동일, url기반 다운로드, 압축파일은 압축 풀어줌
  • CMD: 컨테이너 안에서 실행할 명령어를 지정한다. 컨테이너 실행시 덮어 쓸 수 있다.
  • FOREGROUND: 실행 방식
  • ENTRYPOINT: 컨테이너 안에서 실행될 프로세스(명령)을 지정한다. CMD의 인자값이 ENTRYPOINT에 전달된다.
  • LABEL: key=value 설정
  • ENV: 컨테이너 실행 환경 변수값 설정
  • ARG: 이미지 빌드시 환경 변수값 설정, "--build-arg  key=value" 로 docker image build 명령 수행시 설정도 가능한다. 
  • WORKDIR: 실행하는 디렉토리 위치를 변경한다. cd 명령과 같다.
  • EXPOSE: 가상머신에 오픈할 포트를 지정한다.

Dockerfile 명으로 파일을 생성한다. ubuntu에 git 까지 설치하는 하기 내용을 넣는다.

FROM ubuntu:bionic
RUN apt-get update
RUN apt-get install -y git

 

빌드하고 이미지 확인한다.

$ docker build -t ubuntu:bionic-git .

Sending build context to Docker daemon  2.048kB
Step 1/3 : FROM ubuntu:bionic
bionic: Pulling from library/ubuntu
....
 ---> cdc67675bfc4
Successfully built cdc67675bfc4
Successfully tagged ubuntu:bionic-git

확인 하기
$ docker images
REPOSITORY          TAG                 IMAGE ID            CREATED             SIZE
ubuntu              bionic-git          cdc67675bfc4        14 seconds ago      197MB
ubuntu              bionic              56def654ec22        4 weeks ago         63.2MB

 

도커를 실행하여 컨테이너에서 git 설치 확인한다.  

$ docker run -it ubuntu:bionic-git bash
root@1aef904d7e26:/# git --version
git version 2.17.1

 

이미지의 빌드과정을 보고 싶을 경우 history 명령을 이용한다. 

$ docker history ubuntu:monawiki
IMAGE               CREATED             CREATED BY                                      SIZE                COMMENT
425046329513        2 hours ago         /bin/sh -c #(nop)  CMD ["/bin/sh" "-c" "bash…   0B

 

Dockerfile일 있는 이를 통해 이미지 빌드파일까지 있다면 향후 버전에 따른 영향에 대응할 수 있다. docker pull할 때 기본  레지스트리 정보는 info 명령으로 확인한다. 

$ docker info

Client:
 Debug Mode: false

Server:
 Containers: 0
....
 Registry: https://index.docker.io/v1/

 

pull 명령시에 Registory를 직접 지정할 수 있다.  레지스트리/네임스페이스/이미지명 => docker.io/library/ubuntu:bionic

$ docker pull docker.io/library/ubuntu:bionic

bionic: Pulling from library/ubuntu
171857c49d0f: Pull complete
419640447d26: Pull complete
61e52f862619: Pull complete
Digest: sha256:646942475da61b4ce9cc5b3fadb42642ea90e5d0de46111458e100ff2c7031e6
Status: Downloaded newer image for ubuntu:bionic
docker.io/library/ubuntu:bionic

$ docker images
REPOSITORY          TAG                 IMAGE ID            CREATED             SIZE
ubuntu              bionic              56def654ec22        4 weeks ago         63.2MB

 

콘솔창에서 도커 허브 로그인은 login 명령이다. tag로 명칭을 만들고 push할 수 있다. 

$ docker tag <로컬 이미지명> <docker-hub-id>/<이미지명>
"docker tag" requires exactly 2 arguments.
Usage:  docker tag SOURCE_IMAGE[:TAG] TARGET_IMAGE[:TAG]

$ docker push <docker-hub-id>/<이미지명>
"docker push" requires exactly 1 argument.
Usage:  docker push [OPTIONS] NAME[:TAG]

 

 

Docker Layer 개념

docker container layer의 변경사항을 알고 싶을 경우 

$ docker diff  <container id>

 

docker container layer의 변경사항을 이미지에 반영하고 싶을 경우

$ docker commit <container id> <new image name>

 

git을 포함시켜서 새로운 ubuntu:git 이미지 만들기

// 우분투 설치
$ docker pull ubuntu:focal

// git 있는지 확인
$ docker run -it ubuntu:focal /bin/sh -c 'git --version'
/bin/sh: 1: git: not found

// apt-get 업데이트
$ docker run -it ubuntu:focal /bin/sh -c 'apt-get update'

// 업데이트 이미지 커밋
$ docker commit $(docker ps -alq) ubuntu:git-layer-1
sha256:7c65ec0b9441f470a43e28a5c6dfc5a64a120090984e3c7bcd38540cb9ef0b1b

// 업데이트 layer에 git 설치
$ docker run ubuntu:git-layer-1 /bin/sh -c 'apt-get install -y git'

// git 설치 layer 커밋
$ docker commit $(docker ps -alq) ubuntu:git
sha256:e21b7d04736553bb0d995af6f6a751aca2e710f2e2f5874d026a89335a53ade9

// 설치 확인
$ docker run -it ubuntu:git bash -c 'git --version'
git version 2.25.1

// 이미지 확인
$ docker images
REPOSITORY          TAG                 IMAGE ID            CREATED             SIZE
ubuntu              git                 e21b7d047365        3 minutes ago       208MB
ubuntu              git-layer-1         7c65ec0b9441        5 minutes ago       98.8MB
ubuntu              focal               d70eaf7277ea        9 days ago          72.9MB

// history 확인
$ docker history ubuntu:git
IMAGE               CREATED             CREATED BY                                      SIZE                COMMENT
e21b7d047365        15 minutes ago      /bin/sh -c apt-get install -y git               109MB
7c65ec0b9441        17 minutes ago      /bin/sh -c apt-get update                       25.9MB
d70eaf7277ea        9 days ago          /bin/sh -c #(nop)  CMD ["/bin/bash"]

 

위의 commit으로 이미지를 새롭게 생성한 것과 동일하게 Dockerfile로 빌드하여 이미지 생성하기

FROM ubuntu:focal
RUN apt-get update
RUN apt-get install -y git

빌드하기

$ docker build -t ubuntu:git2 .

 

Dockerfile로 생성된 이미지와 commit을 이용해 만든 최종 이미지는 같은 형태이다. 

 

 

참조

www.44bits.io/ko/post/why-should-i-use-docker-container

 

왜 굳이 도커(컨테이너)를 써야 하나요? - 컨테이너를 사용해야 하는 이유

컨테이너는 서버 애플리케이션을 배포하고 서버를 운영하는 표준적인 기술이 되어가고 있습니다. 하지만 처음 사용해본다면 그 장점이 잘 와닿지 않을 수도 있습니다. 왜 굳이 도커 컨테이너를

www.44bits.io

  • 서버운용기록을 코드화 할 수 있다.
    • 도커 파일 만들기 = 서버 운영 기록 (Dockerfile)
    • 도커 이미지 만들기 = 도커 파일 + 실행 시점
    • 도커 컨테이너 만들기 = 도커 이미지 + 환경 변수

www.44bits.io/ko/post/easy-deploy-with-docker

 

도커(Docker) 입문편: 컨테이너 기초부터 서버 배포까지

도커(Docker)는 2013년 등장한 컨테이너 기반 가상화 도구입니다. 도커를 사용하면 컨테이너를 쉽게 관리할 수 있으며, 이미지를 만들어 외부 서버에 배포하는 것도 가능합니다. 이 글은 도커를 시

www.44bits.io

www.44bits.io/ko/post/how-docker-image-work

 

만들면서 이해하는 도커(Docker) 이미지: 도커 이미지 빌드 원리와 OverlayFS

도커 이미지는 유니온 마운트 기술을 활용해 계층화된 레이어들로 구성되며, 도커 레지스트리를 사용해 쉽고 효율적인 공유를 가능하게 해줍니다. 이 글에서는 도커 이미지가 저장되는 방식과

www.44bits.io

 

joont92.github.io/docker/Dockerfile/

 

[docker] Dockerfile

Dockerfile이란? 도커 이미지를 만들 때 꼭 필요한 설정파일이다 이 파일내에 작성된 인스트럭션들을 참조하여 이미지가 만들어진다 기본으로 Dockerfile 이라는 이름을 사용하고, 이름을 변경하고

joont92.github.io

nirsa.tistory.com/63

 

[Docker CE] 간단히 보는 Dockerfile 개념(명령어 종류, 빌드, 이미지 레이어)

Dockerfile 이란? 도커는 기본적으로 이미지가 있어야 컨테이너를 생성하고 동작시킬 수 있습니다. dockerfile은 필요한 최소한의 패키지를 설치하고 동작하기 위한 자신만의 설정을 담은 파일이고,

nirsa.tistory.com

 

posted by 윤영식
2020. 5. 22. 17:15 Angular/Concept

Angular에서 schematic은 복잡한 로직을 지원하는 템플릿 기반의 코드 생성기이다. @angular/cli에서 코드 생성은 schematics를 사용하는 것이다. 해당 패키지는 @schematics/angular 이다. 본글에서는 나만의 schematic을 만들어 본다. 하기 문서를 참조하여 NX 기반으로 개발한다.

 

https://medium.com/@tomastrajan/total-guide-to-custom-angular-schematics-5c50cf90cdb4

 

Total Guide To Custom Angular Schematics

Schematics are great! They enable us to achieve more in shorter amount of time! But most importantly, we can think less about mundane…

medium.com

 

Schematic 생성

 NX환경이 아니라면 @angular-devkit/schematics-cli 를 글로벌로 설치해서 schematics 명령을 사용해 초기 schematic파일을 자동 생성할 수 있다. 그러나 NX를 사용하면 @angular-devkit/schematics-cli설치 없이 nx명령으로 초기 파일을 생성할 수 있다. NX의 schematics을 사용하겠다는 정의는 루트의 angular.json에 정의되어 있다. 

    ...
    "schematics": {
        "@nrwl/angular:application": {
            "unitTestRunner": "jest",
            "e2eTestRunner": "cypress"
        },
        "@nrwl/angular:library": {
            "unitTestRunner": "jest"
        }
    },
    ...

 

명령어 예

//schematic-cli 설치
$ npm install -g @angular-devkit/schematics-cli

//schematics 명령 사용 경우
$ schematics blank hello


//NX 환경일 경우
$ nx g workspace-schematic hello
CREATE tools/schematics/hello/index.ts (250 bytes)
CREATE tools/schematics/hello/schema.json (348 bytes)

nx 명령을 생성된 schematic 파일은 tools/hello 폴더 밑에 존재한다. schema.json은 hello schematic의 정의 파일이고, index.ts가 동작하는 시작 파일이다. 

 

 

Schematic 키워드

  • Tree: virtual file system을 표현한다. base 라는 이미 존재하는 파일 묶음과 staging area 라는 base에서 변경될 것들 목록으로 구성된다. base 는 변경할 수 없고, staging area 만 변경가능 하다.
  • Rule: Tree를 받아서 변경점을 적용하고 새로운 Tree를 반환하는 함수이다. 메인 파일인 index.ts에 해당 함수를 정의한다.
  • Action: Action을 변환(Transformation)으로 표현되고, action 타입은 Create, Rename, Overwrite, Delete 4가지 유형이 있다.
  • 참조 원문

실제 사용자 정의 작업을 해야하는 Rule의 정의이다. 

export declare type Rule = (tree: Tree, context: SchematicContext) => Tree | Observable<Tree> | Rule | Promise<void> | Promise<Rule> | void;

 

모든 Schematic은 context안에서 구동되어야 하고, SchematicContext 객체로 표현된다. nx가 아닌 schematics 명령으로 생성된 index.ts 의 코드를 보자.

  • hello함수는 RuleFactory로써 Rule을 반환하는 고차함수(a higher-order function)를 반환한다.
  • hello함수안에서 나의 template이 어떻게 합쳐지고, 변경되어 지는 것인지 정의한다.
//schematic 명령으로 생성된 index.ts 내역

import { Rule, SchematicContext, Tree } from '@angular-devkit/schematics';

// You don't have to export the function as default. You can also have more than one rule factory
// per file.
export function hello(_options: any): Rule {
  return (tree: Tree, _context: SchematicContext) => {
    return tree;
  };
}

nx로 생성된 schematic 의 index.ts는 약간 틀린 구조이다. 

  • default함수로 RuleFactory를 정의한다.
  • Rule을 반환한다. 
import { chain, externalSchematic, Rule } from '@angular-devkit/schematics';

export default function (schema: any): Rule {
    return chain([
        externalSchematic('@nrwl/workspace', 'lib', {
            name: schema.name,
        }),
    ]);
}

 

 

Schematic 옵션 설정

  • nx로 생성했을 경우: schema.json 파일에 옵션을 설정한다.
  • schema.json의 properties 는 Schematic 수행시 prompt 로 작동하여 사용자 입력값을 처리한다. 
  • properties안에 사용자로 부터 입력받고자하는 옵션을 설정한다. 입력받을 수 있는 옵션은 schema.json에 정의되어 있다.
  • schema.json 전체 명세서

 

//nx 명령으로 생성된 schema.json 내역
{
    "$schema": "http://json-schema.org/schema",
    "id": "hello",
    "type": "object",
    "properties": {
        "name": {
            "type": "string",
            "description": "Library name",
            "$default": {
                "$source": "argv",
                "index": 0
            }
        }
    },
    "required": ["name"]
}

 

 

옵션의 Input "type"  

  • confirmation: boolean 옵션
  • input: text 또는 number 옵션
  • list: 미리 정의한 목록을 선택

schema.json에 정의하는 type

"style"을 정의한 예

//schema.json 예
"style": {
  "description": "The file extension or preprocessor to use for style files.",
  "type": "string",
  "default": "css",
  "enum": [
    "css",
    "scss",
    "sass",
    "less",
    "styl"
  ],
  "x-prompt": {
    "message": "Which stylesheet format would you like to use?",
    "type": "list",
    "items": [
      { "value": "css",  "label": "CSS" },
      { "value": "scss", "label": "SCSS   [ https://sass-lang.com/documentation/syntax#scss                ]" },
      { "value": "sass", "label": "Sass   [ https://sass-lang.com/documentation/syntax#the-indented-syntax ]" },
      { "value": "less", "label": "Less   [ http://lesscss.org                                             ]" },
      { "value": "styl", "label": "Stylus [ http://stylus-lang.com                                         ]" }
    ]
  },
},

x-prompt 필드를 통해 긴 입력 문구 조합이 가능하다. 

//x-prompt schema
{
    "oneOf": [
        { "type": "string" },
        {
            "type": "object",
            "properties": {
                "type": { "type": "string" },
                "message": { "type": "string" },
                "items": {
                    "type": "array",
                    "items": {
                        "oneOf": [
                            { "type": "string" },
                            {
                                "type": "object",
                                "properties": {
                                    "label": { "type": "string" },
                                    "value": { }
                                },
                                "required": [ "value" ]
                            }
                        ]
                    }
                }
            },
            "required": [ "message" ]
        }
    ]
}

 

 

Schematic 실행

Schematics CLI로 생성했을 경우

  • index.ts를 컴파일한다. 
  • 명령어: schematics <path-to-schematics-project>:<schematics-name> --<required-option>=<value>
//schematics 으로 생성한 폴더로 이동후 컴파일 수행
$ cd hello
$ npm run build

//schematics 수행
$ schematics .:hello
Nothing to be done.

nx 명령으로 생성했을 경우

  • 명령어: npm run workspace-schematic <customized schematic name> <option>
$ npm run workspace-schematic hello hi

> micro-demo@0.0.0 workspace-schematic /Users/dowonyun/mobicon/projects/bistel/src/2019/micro-demo
> nx workspace-schematic "hello" "hi"

>  NX  Executing your local schematic: hello

CREATE libs/hi/tslint.json (91 bytes)
CREATE libs/hi/README.md (158 bytes)
CREATE libs/hi/tsconfig.json (135 bytes)
CREATE libs/hi/tsconfig.lib.json (190 bytes)
CREATE libs/hi/src/index.ts (26 bytes)
CREATE libs/hi/src/lib/hi.ts (0 bytes)
CREATE libs/hi/jest.config.js (246 bytes)
CREATE libs/hi/tsconfig.spec.json (269 bytes)
UPDATE tsconfig.json (731 bytes)
UPDATE angular.json (16683 bytes)

 

 

참조

Angular Schematic 개념: https://angular.io/guide/schematics

 

Angular

 

angular.io

스키마 명세: https://github.com/angular/angular-cli/blob/7.0.x/packages/schematics/angular/application/schema.json

 

angular/angular-cli

CLI tool for Angular. Contribute to angular/angular-cli development by creating an account on GitHub.

github.com

 

posted by 윤영식
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 윤영식
2019. 1. 9. 18:26 Angular/Architecture

Nx Workspace와 NgRx/Platform을 연결한 boilerplate를 만들어 본다. 


  - Nx Workspace 생성하기 

  - NgRx 개념이해  및 사용예 만들기

  - 다음 3가지 문서와 동영상을 참조한다.

    + Nx Workspace 무료 동강 수강

    + Nx Workspace 메뉴얼

    + NgRx와 Nx Workshop 강좌

    + State Management Type 정리

    + Nx Workspace 기반 예제소스




Nx Workspace 생성


하나의 저장소에 여러 애플리케이션을 개발할 수 있는 workspace를 최초 한번 생성한다.  

  - 현재 최신 버전의 스케매티과 CLI를 설치한다. v7.* 이다.

  - workspace 생성시 npmScope를 설정하여 workspace의 공통 library를 import할 때 prefix로 사용된다. 예로 @jm/<library name> 식이다. 

     또한 컴폰너트 selector의 prefix로 사용된다.

  - workspace 생성시 옵션은 scss, npm (또는 yarn)을 선택한다.

$ npm i -g @nrwl/schematics @angular/cli


// create -react-app과 유사, 첫번째 인자는 workspace 명칭이다. 

$ create-nx-workspace  jamong  --npm-scope=jm


nx workspace에서 사용(2019.1.8)기준 Redux 패턴의 구현체인 ngrx/platform 버전이 v6.*으로 설정되어 있기에 v7.0.0으로 변경한다. 

  - <workspace>/package.json에서 @ngrx/xxxx 의 버전을 ^7.0.0 으로 설정한다. 

$ cd jamong

$ vi package.json


// 수정후 다시 install 한다.

$ npm i


다음으로 샘플 애플리케이션을 생성한다. 

  - g는 generate, app은 application 이다. 

  - dashboard 명칭의 애플리케이션을 생성한다.

  - routing, unit test - jest, e2e - cypress, linting등을 선택한다.

$ ng g app dashboard

또는

$ ng generate application dashboard


// 작동여부 확인

$ npm start --project=dashboard


다음으로 애플리케이션안에 component를 자동생성할 때 옵션을 미리 설정해 놓는다. angular workspace의 schematics 설정 목록을 참조한다.

  - <workspace>/angular.json 파일의 dashboard 애플리케이션에 하기 항목을 추가한다. 

  - OnPush, viewEncapsulation 적용

생성시 적용될 수 있도록 맨 하단에도 동일한 설정을 한다. 

  "schematics": {

    "@nrwl/schematics:application": {

      "style": "scss",

      "changeDetection": "OnPush",

      "viewEncapsulation": "None"

    },

    "@nrwl/schematics:library": {

      "style": "scss",

      "changeDetection": "OnPush",

      "viewEncapsulation": "None"

    }

  },

  "defaultProject": "mi-dashboard"



샘플 컴포넌트를 생성한다.

  - --project옵션을 통해 어느 프로젝트안에 생성할지 선택한다.

  - / 로 path를 지정할 수 있다.

  - component 생성 명령 설명

$ ng g component layout/page --project=dashboard

CREATE apps/dashboard/src/app/layout/page/page.component.scss (0 bytes)

CREATE apps/dashboard/src/app/layout/page/page.component.html (23 bytes)

CREATE apps/dashboard/src/app/layout/page/page.component.spec.ts (614 bytes)

CREATE apps/dashboard/src/app/layout/page/page.component.ts (261 bytes)

UPDATE apps/dashboard/src/app/app.module.ts (555 bytes)


공통 라이브러리도 생성해 본다.

  - tag를 통해 소스 분석시 논리적 그룹을 주고, tag들 끼리의 의존관계를 설정할 수도 있다. 

  - library를 생성하면 <workspace>/angular.json, nx.json, tsconfig.json 파일에 자동으로 정보가 입력된다.

  - library 생성 명령 설명

$ ng g lib shared --tags=shared-lib

? In which directory should the library be generated?

? Which module should import the library?

? Would you like to add a routing configuration to the library? No

? Will this library be lazy loaded? No

? Would you like to generate an NgModule within the library? Yes

? Which Unit Test Runner would you like to use for the library? Jest (https://

jestjs.io/)

CREATE libs/shared/tsconfig.lib.json (705 bytes)

CREATE libs/shared/tslint.json (185 bytes)

CREATE libs/shared/src/index.ts (37 bytes)

... 생략


Unit Test는 Jest를 사용하고, e2e는 cypress를 사용하므로 Karma, Jasmine와 Protractor관련 내용은 삭제한다. 

  - package.json안에 karma, jamine관련 패키지 삭제 (jasmine-marbles 패키지는 제외)

  - karma.conf.json 파일삭제

  - yarn.lock, package-lock.json파일 삭제

  - node_modules를 제거하고 다시 npm install을 수행한다. 




NgRx/Platform 적용하기


Redux 패턴의 Angular 버전인 NgRx/platform을 적용한다. 엔터프라이즈 아키텍트를 적용하여 Store/State 접근방식을 SandBox를 통한 접근토록 만든다. libs/appState 에 공통적인 부분을 구현한다. Angular Enterprise Architecture Pattern을 참조한다.

  - Application Core Facade는 Parent Class로 Presentation Module에서 사용하는 Sandbox가 상속을 받는다.

  - "State Management" library를 생성한다. 여기에는 기본 설정항목과 None Visual Domain 영역에 대한 State관리를 관장한다. 

     이것은 forRoot가 된다.

  - Common UI는 Library별로 존재하며 State Management는 각 UI별로 구현한다. 

     이들은 forFeature가 된다.


Application을 위한 ngrx state를 생성한다. 

  - appState 명칭과 --module 옵션을 반드시 설정해야 한다. 

  - 해당 명령을 수행하면 ngrx/entity가 자동 설치되는데 v6.* 가 설치되므로 package.json에서 v7.*으로 변경하고 다시 npm install 수행한다. 

  - application레벨의 root state를 선택한다. (yes) => app.module.ts에 forRoot가 자동 설정된다.

  - facade도 yes를 선택한다.

  - 명령 옵션 목록

$ ng g ngrx appState --module=./apps/dashboard/src/app/app.module.ts

? Is this the root state of the application? Yes

? Would you like to add a Facade to your ngrx state Yes


생성 파일들


Redux에서는 action, reducer가 기본이고, (side) effect라는 부수효과는 reducer를 타기전 ajax 호출같은 async를 처리할 때 이용한다. selector는 state의 내용을 Domain 요구에 따라 조합해서 반환하는 서비스이다. 그럼 Facade는 무엇일까?

  - Redux패턴에서 state값을 조합해서 얻기위해 성능향상을 줄 수 있는 것이 reselector개념이고, 이를 selector에서 구현한다. 참조글

  - Redux패턴에서의 Facade 글을 참조한다. facade 서비스를 통해서 데이터를 가져온다. 데모 소스

  - Facade 서비스가 Presentation 컴포넌트에서 사용하는 Biz 로직을 담고 있는 SandBox이다. 

  


AppStateFacade는 forFeature에서 사용하는 common한 내용을 담는다. 즉 "Application core facade" 이다.

  - schematics을 커스텀으로 만들어 사용할 수 있다.

import { Injectable } from '@angular/core';


import { select, Store } from '@ngrx/store';


import { AppStatePartialState } from './app-state.reducer';

import { appStateQuery } from './app-state.selectors';

import { LoadAppState } from './app-state.actions';


@Injectable()

export class AppStateFacade {

  loaded$ = this.store.pipe(select(appStateQuery.getLoaded));

  allAppState$ = this.store.pipe(select(appStateQuery.getAllAppState));

  selectedAppState$ = this.store.pipe(

    select(appStateQuery.getSelectedAppState)

  );


  constructor(private store: Store<AppStatePartialState>) {}


  // 업무 메소드, Action을 생성하고 dispatch하는 문구가 존재한다.

  loadAll() {

    this.store.dispatch(new LoadAppState());

  }

}


forFeature에 대한 것은 libs 폴더 밑에 생성한다.

  - library 생성을 한다. 이때 옵션으로 --directory=<groupName>을 주면 라이브러리들을 directory명칭 밑으로 그룹핑할 수 있다. 

  - 생성된 library에 menu에 대한 feature 레벨 state를 생성한다. 

$ ng g lib menu --directory=state

? Which module should import the library?

? Would you like to add a routing configuration to the library? No

? Will this library be lazy loaded? No

? Would you like to generate an NgModule within the library? Yes

? Which tags would you like to add to the library? (used for linting)

? Which Unit Test Runner would you like to use for the library? Jest

CREATE libs/state/menu/tsconfig.lib.json (712 bytes)

CREATE libs/state/menu/tslint.json (188 bytes)

CREATE libs/state/menu/src/index.ts (41 bytes)  <== state 그룹 밑으로 menu library가 생성되었다. 

CREATE libs/state/menu/src/lib/state-menu.module.ts (164 bytes)

... 중략


다음으로 menu state를 생성한다. 

  - 반드시 library의 모듈을 수행하는 명령의 상대경로로 지정한다.

$ ng g ngrx menu --module=libs/state/menu/src/lib/state-menu.module.ts

? Is this the root state of the application? No

? Would you like to add a Facade to your ngrx state Yes

CREATE libs/state/menu/src/lib/+state/menu.actions.ts (731 bytes)

CREATE libs/state/menu/src/lib/+state/menu.effects.spec.ts (1147 bytes)

CREATE libs/state/menu/src/lib/+state/menu.effects.ts (842 bytes)

... 중략


state-menu.module.ts에 forFeature가 다음과 같이 자동 등록된다. forFeature는 lazy loading 또한 가능하다.

  - state에 대한 forFeature 등록

  - effect에 대한 forFeature 등록

import { NgModule } from '@angular/core';

import { CommonModule } from '@angular/common';

import { StoreModule } from '@ngrx/store';

import { EffectsModule } from '@ngrx/effects';

import {

  MENU_FEATURE_KEY,

  initialState as menuInitialState,

  menuReducer

} from './+state/menu.reducer';

import { MenuEffects } from './+state/menu.effects';

import { MenuFacade } from './+state/menu.facade';


@NgModule({

  imports: [

    CommonModule,

    StoreModule.forFeature(MENU_FEATURE_KEY, menuReducer, {

      initialState: menuInitialState

    }),

    EffectsModule.forFeature([MenuEffects])

  ],

  providers: [MenuFacade]

})

export class StateMenuModule {}


StateMenuModule을 app.module.ts에 등록한다. 

  - @jm/state/menu 경로로 import한다. 이는 <workspace>/tsconfig.json에 path가 자동 설정되기에 가능하다. 

import { StateMenuModule } from '@jm/state/menu';


@NgModule({

  declarations: [AppComponent, PageComponent],

  imports: [

    BrowserModule,

    NxModule.forRoot(),

    RouterModule.forRoot([], { initialNavigation: 'enabled' }),

    StoreModule.forRoot(

      { appState: appStateReducer },

      {

        initialState: { appState: appStateInitialState },

        metaReducers: !environment.production ? [storeFreeze] : []

      }

    ),

    EffectsModule.forRoot([AppStateEffects]),

    !environment.production ? StoreDevtoolsModule.instrument() : [],

    StoreRouterConnectingModule,


    StateMenuModule

  ],

  providers: [AppStateFacade],

  bootstrap: [AppComponent]

})

export class AppModule {}


effect 클래스를 보면 DataPersistence 헬퍼 클래스를 Nx에서 제공한다. 

  - async 처리에 대한 추상화이다. 

  - 4가지 주요 헬퍼 메소드 제공

    + optimisticUpdate: 요청값을 client에 먼저 반영하고 만일 backend 오류 발생시 이전 상태로 복원

    + pessimisticUpdate: 요청값을 backend에 반영하고 이후 client에 반영

    + fetch: 같은 Action에 대한 다중 요청은 가장 최신 것만 반영함. pessimisticUpdate이면서 같은 Action에 따른 요청은 최신만 반영하는 것.

    + navigation: routing될 때 effect 자동 호출

  - DataPersistence에 대한 상세 설명

  - Angular Application State관리 방법

import { Injectable } from '@angular/core';

import { Effect, Actions } from '@ngrx/effects';

import { DataPersistence } from '@nrwl/nx';


import { MenuPartialState } from './menu.reducer';

import {

  LoadMenu,

  MenuLoaded,

  MenuLoadError,

  MenuActionTypes

} from './menu.actions';



@Injectable()

export class MenuEffects {

  @Effect() loadMenu$ = this.dataPersistence.fetch(

    MenuActionTypes.LoadMenu, 

    {

      run: (action: LoadMenu, state: MenuPartialState) => {

        // Your custom REST 'load' logic goes here. For now just return an empty list...

        return new MenuLoaded([]);

      },


      onError: (action: LoadMenu, error) => {

        console.error('Error', error);

        return new MenuLoadError(error);

      }

  });


  constructor(

    private actions$: Actions,

    private dataPersistence: DataPersistence<MenuPartialState>

  ) {}

}



<참조>

- Nx Workspace 홈페이지

- Nx Github

- Angular Workspace의 Schematics 설정 목록

Nx workspace에서 component 생성하기 명령

- Angular Enterprise Architecture Pattern

- NgRx Schematics 설정

- Redux의 reselector를 통한 성능향상

- NgRx Facade 이해

- Angular Application State관리 방법

- Ionic + Angular + ngrx 사례

posted by 윤영식
2018. 12. 27. 15:27 Angular/Architecture

Angular CLI v6.* 이후 Multi Application을 위한 Workspace개념이 도입되었다. 공통 Module을 통해 하나의 폴더안에서 여러 애플리케이션을 만들 수 있는 방법에 대해 알아본다. 




Schematics


스케메틱스는 workflow 툴이다. 

  - 프로젝트에 파일을 생성하고

  - 기존 코드를 변경하고

  - 환경 옵션을 추가하거나 프레임워크를 추가할 수 있다. 


스케메틱스는 Angular CLI의 코드 스케폴딩에 사용하며 다음 목표를 지향하여 개발되었다. 

  - 사용 및 개발이 쉬워야 한다

  - 확장 및 재상용성이 좋아야 한다

  - CLI동작시 부작용을 제거할 수 있어야 한다.

  - 비동기성을 지원한다.


Tree

  - 이미 존재하는 파일 관계를 작는 데이터 구조체이다. 

  - 파일을 수정하거나 생성하고 찾아 갈때 사용한다.

  - 스케메틱의 Start point가 된다. 

  - 하나의 스케메틱 Tree를 다른 스케메틱 Tree로 전달할 수 있다. 

  - create, delete, rename, overwrite 내가지 함수를 제공한다.


나만의 스케메틱스 만들기 

  - Node v6.9 이상 설치

  - schematics 실행환경을 만들기 위해 shcematics-cli를 설치한다. 

$ npm install -g @angular-devkit/schematics-cli

// 빈 스케메틱스 프로젝트 생성

$ schematics blank --name=my-project

$ cd my-project


Collections

  - 이름을 갖는 schematics의 집합이다. 이것은 사용자에 의해 발행되고 설치될 수 있다. 

  - 예로 @schematics/angular collection이 있다. 이것은 component, module, application 등에 대한 schematics를 가지고 있다. 

  - 생성한 my-project는 하나의 schematics만을 가지고 있고, my-project/src/collection.json 에 정의되어 있다. 

  - schematics key안에 하나의 my-project schematic 이 설정되어 있다.

  - factory key는 자바스크립트 펑션 위치를 설정한다. 이것은 RuleFactory라 한다. 

// src/collection.json

{

  "$schema": "../node_modules/@angular-devkit/schematics/collection-schema.json",

  "schematics": {

    "my-project": {

      "description": "A blank schematic.",

      "factory": "./my-project/index#myProject"

    }

  }

}


// src/my-project/index.ts

import { Rule, SchematicContext, Tree } from '@angular-devkit/schematics';


export function myProject(_options: any): Rule {

  return (tree: Tree, _context: SchematicContext) => {

    return tree;

  };

}


Rule

  - Tree를 받아 Tree를 반환하는 함수이다. 

  - Rule이 스케메틱스의 핵심이다. 

  - Factory 함수는 입력 options 아규먼트를 받아 원하는 것을 처리 후 다시 Rule을 반환한다. 

  - 옵션은 CLI의 경우 command line 아규먼트로 전달되는 값이다. (보통 --name=<value> 같이 전달함)

 

사용자가 만든 스케메틱스를 수행하기 

  - dry-run은 스케메틱스 자신안에서 수행할 수 있게 한다. default는 false 이다.

  - 우리가 만든 스케메틱스는 애플리케이션안에서 사용하는 것이다. 따라서 자기 자신안에서 스케메틱스 검증을 해보려면 dry-run 옵션을 준다.

  - 만들어진 스케메틱스를 디버깅 할 수 있다. 

$ npm run build


// dry-run 수행시, 가급적 사용하지 말자, 별도의 temporary 폴더에서 dry-run없이 테스트 한다. 

$ schematics .:my-project --name=test --dry-run=true


// 디버깅

$ node --inspect-brk $(which schematics) .:my-project --name=test


다른 스케메틱스를 함께 사용하기 

  - chain을 통해 기존 @schamatics/angular의 component 생성하는 것을 이용한다. 

  - Angular component를 만든 다음 상단에 license 문구를 추가한다. 

import { Rule, SchematicContext, Tree, chain, externalSchematic } from '@angular-devkit/schematics';


const licenseText = `

/**

 * @license

 * Copyright Google Inc. All Rights Reserved.

 *

 * Use of this source code is governed by an MIT-style license that can be

 * found in the LICENSE file at https://angular.io/license

 */

`;


export function myProject(options: any): Rule {

  return chain([

    externalSchematic('@schematics/angular', 'component', options),

    (tree: Tree, _context: SchematicContext) => {

      tree.getDir(options.sourceDir)

          .visit(filePath => {

            if (!filePath.endsWith('.ts')) {

              return;

            }

            const content = tree.read(filePath);

            if (!content) {

              return;

            }


            // Prevent from writing license to files that already have one.

            if (content.indexOf(licenseText) == -1) {

              tree.overwrite(filePath, licenseText + content);

            }

          });

      return tree;

    },

  ]);

}


수행해 보자.

// my-project schematics 폴더안에서 의존관계있는 @schematics/angular 설치

$ npm install --save @schematics/angular

$ npm run build


// 신규 프로젝트 생성하고, my-project schematics 를 npm link 건다.

$ cd ../

$ ng new <otherProjectName>

$ cd <otherProjectName>

$ npm link <my-project schematics path>


// schematics:schematic 을 호출한다. angular module의 declarations에도 자동 추가된다. 

$ ng g my-project:my-project

? What name would you like to use for the component? hi

CREATE src/app/hi/hi.component.css (0 bytes)

CREATE src/app/hi/hi.component.html (21 bytes)

CREATE src/app/hi/hi.component.spec.ts (600 bytes)

CREATE src/app/hi/hi.component.ts (253 bytes)

UPDATE src/app/app.module.ts (537 bytes)




Multi Application을 위한 Nx Workspace


Nx Workspace는 nrwl에서 만든 multi application 개발을 위한 Angular CLI에 대한 확장팩이다. 다음 3가지의 향상시키기 위해 만들어졌다. 

  - 생산성: 초시 셋업시간을 절약한다.

  - 일관성: 기업용 애플리케이션을 위한 좀 더 향상된 코드 제너레이터를 제공하고, 일관된 물리적 공간과 컨벤션을 제공한다.

  - 안정성: 분석기능 강화




Workspace에서 작업하기

  - 단일 레파지토리에서 Multi Application과 Library에 대한 개발을 수행한다.

    + Unified versioning: 하나의 레파지토리 사용에 따른 

    + Promotes code sharing and reuse: lib모듈을 통한 공유

    + Easier dependency management: 하나의 node_modules만 사용, 하나의 build setup

    + Refactoring benefits: code editors 지원, 변경이 전체 애플리케이션에 반영

    + Consistent developers experience: 공유 코드 변경에 대한 전체 애플리케이션 유효성 체크 가능



Nx Workspace 만들기

create-nx-workspace 명령어를 통해 nx 기반 angular workspace를 생성한다. 마치 create-react-app과 같은 역할이다. 

  - npmScope: 그림처럼 NPM Scope로 library를 접근하는 @my-company와 같은 npm scope를 설정


예로 @angular가 npm scope이다. 


  - directory workspace의 폴더명 변경

$ npm i -g @nrwl/schematics  @angular/cli


// command: create-nx-workspace <workspace name> --directory=<workspace별칭폴더명> --npm-scope=<library접근 scope>

$ create-nx-workspace my-project-suite --npm-scope=apple




Multi Application & Library 생성하기


Nx Workspace에 application과 library를 생성한다. Angular CLI Workspace에서 만들어 지는 폴더 구조와 다음과 같은 차이가 있다. 

  - apps: apps폴더 밑으로 Application별 폴더가 존재한다. 

  - libs: application에서 공유하는 Angular Module별 폴더가 존재한다.


애플리케이션 프로젝트에 대한 정보는 angular.json에서 확인할 수 있다. 

{

  "$schema": "./node_modules/@angular/cli/lib/config/schema.json",

  "version": 1,

  "newProjectRoot": "",

  "projects": {}, <-- 프로젝트 즉, 애플리케이션은 아직 등록되지 않았다. 

  "cli": {

    "warnings": {

      "typescriptMismatch": false,

      "versionMismatch": false

    },

    "defaultCollection": "@nrwl/schematics", <-- nrwl/schematics를 사용한다. 

    "packageManager": "npm"

  }

}


nx.json은 Nx Workspace특화된 환경값을 갖는다. 

{

  "npmScope": "apple",

  "implicitDependencies": {

    "angular.json": "*",

    "package.json": "*",

    "tsconfig.json": "*",

    "tslint.json": "*",

    "nx.json": "*"

  },

  "projects": {}

}


그외 tsconfig.json에 paths와 tslin.json의 nx-enforce-module-boundaries 설정, 그리고 package.json에 Nx를 위한 별도 script들이 존재한다. 


apps폴더 밑으로 프로젝트 추가하기

  - 명령: ng g app <application name>

  - ng g app --help 로 생성 옵션을 볼 수 있다. 예로 --routing 옵션을 주면 routing module이 자동 설정된다. 또는 생성시 yes 를 선택해되 된다. 

$ ng g app app-one


// 다음을 선택했다. 

? In which directory should the application be generated?

? Would you like to add Angular routing? Yes

? Which stylesheet format would you like to use? SCSS   [ http://sass-lang.com]

? Which Unit Test Runner would you like to use for the application? Jest   [ https://jestjs.io ]

? Which E2E Test Runner would you like to use for the application? Cypress[ https://www.cypress.io ]

? Which tags would you like to add to the application? (used for linting)


애플리케이션 app-one을 위해 다음과 같은 두개 폴더가 생성된다. 


만들어진 애플리케이션을 실행해 보자. 

  - ng serve <application name>

  - default port는 4200이다. 여러개의 애플리케이션을 동시 실행하고 싶다면 --port 옵션으로 포트를 서로 틀리게 설정한다. 또는 angular.json에 설정할 수 있다. 

$ ng serve app-one


libs폴더 밑으로 공유 라이브러리 추가하기

  - 라이브러리는 Angular Module이다.

  - ng g lib <library name> 

$ ng g lib adk

? In which directory should the library be generated?

? Which module should import the library?

? Would you like to add a routing configuration to the library? No

? Will this library be lazy loaded? No

? Would you like to generate an NgModule within the library? Yes

? Which tags would you like to add to the library? (used for linting)

? Which Unit Test Runner would you like to use for the library? Jest (https://jestjs.io/)


생성을 하게되면 libs/adk 폴더가 생성되고, angular.json에 projectType으로 library가 자동 등록된다. (nx.json에도 자동 등록됨)


lazy loading되는 library module 생성

  - --routing이 설정된 모듈

  - lazy loading되는 상위 모듈: 어차피 애플리케이션도 라이브러리도 모듈이다.

  - --lazy 옵션을 직접주거나 아래처럼 선택할 수 있다. 

$ ng g lib login --routing --parent-module=./apps/app-one/src/app/app.module.ts


? In which directory should the library be generated?

? Will this library be lazy loaded? Yes

? Would you like to generate an NgModule within the library? Yes

? Which tags would you like to add to the library? (used for linting)

? Which Unit Test Runner would you like to use for the library? Jest (https://jestjs.io/)


routing 설정을 하게되면 login.module.ts와 app-one의 app.module.ts 안의 설정은 다음과 같다. 

  - @apple 이라는 npm scope가 자동으로 붙는다. 

// libs/login/src/lib/login.module.ts

@NgModule({

  imports: [

    CommonModule,


    RouterModule.forChild([

      /* {path: '', pathMatch: 'full', component: InsertYourComponentHere} */

    ])

  ]

})

export class LoginModule {}


// apps/app-one/src/app/app.module.ts

@NgModule({

  declarations: [AppComponent],

  imports: [

    BrowserModule,

    NxModule.forRoot(),

    RouterModule.forRoot(

      [{ path: 'login', loadChildren: '@apple/login#LoginModule' }],

      { initialNavigation: 'enabled' }

    )

  ],

  providers: [],

  bootstrap: [AppComponent]

})

export class AppModule {}


루트의 tsconfig.json에는 library module에 대한 paths 정보가 자동 설정된다. 

    "lib": [

      "es2017",

      "dom"

    ],

    "baseUrl": ".",

    "paths": {

      "@apple/adk": [

        "libs/adk/src/index.ts"

      ],

      "@apple/login": [

        "libs/login/src/index.ts"

      ]

    }




Multi Application 빌드하기 


angular.json의 architect에는 build와 serve에 대한 환경설정이 있다.

      "architect": {

        "build": {

          "builder": "@angular-devkit/build-angular:browser",

          "options": {

            "outputPath": "dist/apps/app-one",

            "index": "apps/app-one/src/index.html",

            "main": "apps/app-one/src/main.ts",

            "polyfills": "apps/app-one/src/polyfills.ts",

            "tsConfig": "apps/app-one/tsconfig.app.json",

            "assets": [

              "apps/app-one/src/favicon.ico",

              "apps/app-one/src/assets"

            ],

            "styles": [

              "apps/app-one/src/styles.scss"

            ],

            "scripts": []

          },

          "configurations": {

            "production": {

              ....

             }

           }


빌드 명령

애플리케이션을 빌드한다

  - ng build <application name> --prod

  - nx workspace command 목록

$ ng build app-one --prod


라이브러리를 빌드하려면 라이브러리 생성할 때 --publishable 옵션을 주어서 생성한다. 그러면 angular.json파일의 "architect"에 "build"옵션이 추가된다. library build할 때는 ng-packagr를 이용하여 어디에서든 사용할 수 있는 APF v6가 적용된다. APF는 Angular Package Format의 약어이다. 

 $ ng g lib ui --publishable --routing --parent-module=./apps/app-one/src/app/app.module.ts


// angular.json 

    "ui": {

      "root": "libs/ui",

      "sourceRoot": "libs/ui/src",

      "projectType": "library",

      "prefix": "apple",

      "architect": {

        "build": {

          "builder": "@angular-devkit/build-ng-packagr:build",

          "options": {

            "tsConfig": "libs/ui/tsconfig.lib.json",

            "project": "libs/ui/ng-package.json"

          }

        },


Angular Pakage Format v6 지원 목록


ui라는 라이브러리 빌드하기 

$ ng build ui

Building Angular Package

Building entry point '@apple/ui'

Compiling TypeScript sources through ngc

Bundling to FESM2015

Bundling to FESM5

Bundling to UMD

Minifying UMD bundle

Copying declaration files

Writing package metadata

Removing scripts section in package.json as it's considered a potential security vulnerability.

Built @apple/ui

Built Angular Package!

 - from: /Users/dowonyun/mobicon/projects/bistel/prototyping/my-project-suite/libs/ui

 - to:   /Users/dowonyun/mobicon/projects/bistel/prototyping/my-project-suite/dist/libs/ui


// 다양한 번들 파일이 생성됨 



$schema와 schema.json 개념

Angular Workspace에 대한 schema를 강제하기 위하여 schema.json 파일을 angular.json의 $schema에 설정한다. 

  - Angular Workspace Schame에 대한 Json 스펙

  - Angular CLI의 Schematics 에 목록: 각 폴더 밑에 schema.json 파일이 존재한다. 

  - schematics 키의 값은 workflow를 수행할 때 적용할 옵션을 설정할 수 있다. 

     지정가능한 옵션 목록

"schematics": {

        "@schematics/angular:component": {

          "styleext": "scss",

          "changeDetection": "OnPush",

          "viewEncapsulation": "None",

          "export": true

        }

},




<참조>

- Schematics 소개

- 2018 FEConf, 고재도님의 Schematics 소개영상

- Angular CLI의 Workspace 과 Schematics 개념

- Angular CLI command 목록

- Nrwl Nx Workspace  홈페이지

- Nx Github 

- Nx Workspace 온라인 강좌 (Free)

- Angular Package Format(APF) 개념 강좌

- APF를 만들어주는 ng-packagr 소스 및 설명

- APF v6 스펙

- ng-packagr 소개

posted by 윤영식
2018. 12. 13. 16:39 Angular/Architecture

Angular 프로젝트를 위한 구조 설계 요건에 대해 알아본다. 


  - 프레임워크 선택 고려 사항

  - 개발 생산성과 유지 보수성을 높일 수 있는 방안

  - 참조 문서와 소스 

    + Angular Enterprise Architecture pattern 문서

    + 해당 문서의 예제 소스




프레임워크 선택 고려 사항


프레임워크는 잘 정돈된 놀이터와 같아 기구를 타고 놀면 된다. 하지만 라이브러리의 경우는 툴박스와 같아서 박스안에는 망치, 드라이버, 못같은 것만 있다. 그럼 놀 것을 어떻게 만들지는 도구를 사용하는 사용자의 몫이다. 현재 UI 프레임워크/라이브러리는 Angular, React, Vue 등이고 풀스택으로는 Meteor를 선택해서 사용하고 있다. Angular 프레임워크와 React UI 라이브러리 또는 Meteor 선택시 고려사항을 개인의견으로 간략히 정리한다. 


Angular Framework

  - 대규모 협업 팀에 유리: 대략 10명이상으로 HTML Publisher와 Javascript 개발자가 분리되어 있을 경우

  - JQuery가 low level의 javascript로 개발해 오던 개발자들이 접근하기에 수월하다고 판단함

  - Typescript, RxJS 반드시 알아야 한다.


React Library

  - HTML Publisher와 Javascript 개발을 같이하는 스타트업 또는 프론앤드 개발자에게 유리

  - 원하는 것들을 선택해 적용할 수 있다. 


Meteor

  - FullStack 으로서 javascript가지고 전부 개발하고 싶을 경우

  - 아이디어를 빠르게 구현하고 검증 받고 싶을 경우

  - UI는 Angular, React, Vue 등으로 선택해 사용할 수 있으나, React가 적합하다 판단



그러나 어떤 것을 선택하든 공통적으로 고려해야 하는 사항으로 애플리케이션 아키텍쳐이다. 이에 대한 좋은 영상이 있어 보기를 권한다. 




애플리케이션 아키텍쳐



위의 영상을 정리해 보면 다음과 같다. 

  - Layered Architecture 구성

  - 역할 분담

    + Module: 업무 최소 단위, 하나를 제거해도 다른 것에 영향을 주지 않는 단위, Module은 Sandbox만 알뿐이다. 

    + Sandbox: Module은 Sandbox를 통해 Application Core나 Base Library를 접근한다.

         - 일관성을 유지시킴

         - 시큐리티 가드 역할

         - 공통 Interface 역할

    + Application Core: Module을 관리(Control) 한다. 

         - Module의 Life Cycle을 담당한다.

         - Module 간 통신을 담당한다 (inter-module communication)

         - Module을 직접 가져다 쓰지 않고, Sandbox의 공통 API를 통해 명령을 내리고, 듣는다. 

         - General error handling

         - Extension을 통해 Application core기능을 확장한다.  

            

   + Extension 확장

       - Error handling

       - Ajax communication: 공통 Format (request/response) 통신, 서버 오류 관리

       - New module capabilities

       - General Utils

       - What you want

    + Base Library

       - 개발자들이 직접 base library를 건드리게 하지 말자. 나중에 바뀔 수 있다. 

       - Browser normalization

       - General purpose utilities: Paser/serializer, Object / DOM manipulation, Ajax Communication

       - low-level extension 제공: application extension처럼 base library를 확장한 것들을 제공한다. 

         


정리

  - Base Library만이 사용중인 브라우저를 알고 있다. 아키텍쳐의 다른 layer에서 알 필요가 없다.

  - Application Core만이 사용중인 Base Library를 알고 있다. 아키텍쳐의 다른 layer에서 알 필요가 없다. 

  - Module은 Sandbox의 존재외에는 아무것도 모른다. 아키텍쳐의 나머지에 대해선 아무것도 모른다.


장점

  - 하나의 프레임워크상에서 서로 다른 멀티 애플리케이션을 만들 수 있다. 즉, 기존의 컴포넌트를 재사용함으로서 시간을 줄일 수 있다. 

  - 느슨한 연결(loose coupling)으로 인해 모듈 단위 테스트가 가능해짐.

  - 확장 가능한 자바스크립트 아키텍쳐는 하나의 블록을 교체한다고 해도 두려워 할 필요가 없는 상태가 되어야 한다. 즉, 이를 달성할 수 있다.




유지 보수성 높이기


유지보수가 좋은 코드는 다음의 특성을 갖는다. 

  - 직관적이다. Intuitive

  - 코드 이해가 쉽다 Understable

  - 적용하기 쉽다 Adaptable

  - 확장하기 쉽다 Extendable

  - 디버깅하기 쉽다 Debuggable


이를 위해 해야할 것들. 
  - Code Style, Code Convention 지키기
  - Naming 잘 하기: 이름에 대한 길이에 대해 걱정하지 말자.
  - Javascript에서 css 핸들링 하지 말자 
  - event object를 바로 다른 function의 argument로 넘기지 말고, 필요한 정보만을 준다. 
  - 사용전 object undefined 체크
  - string 값들은 별도의 Config Data 객체에 담아서 사용한다.

    

  - 빌드 자동화 하자

    


posted by 윤영식
2018. 12. 4. 15:13 Meteor/React + Meteor

컴포넌트에 style을 직접 적용하는 대신, class를 적용해 본다. 


  - meteor 기반에서 css 적용의 어려운 부분들

  - styled-component를 사용한 적용

  - 적용소스




styled-components 적용하기


meteor는 자체 builder를 통해 패키지를 빌드하는 관계로 webpack과 같은 custom 설정에 제약이 있어 보인다. 물론 잘 알지 못해서 아직 찾지 못했을 수도 있지만 다음과 같은 class 적용이 안된다. 

  - import * as style from './link.scss': from 문구를 사용하지 못 한다. 나머지 다양한 class적용 방법도 잘 안되는 듯 하다. 

  - import './link.scss' 만 사용가능하다. local 적용이 아닌 global 적용을 한다. <head> 태그에 prefix없이 들어간다. 


Meteor의 제약사항을 고려하여 별도의 scss 파일 작성을 최대한 줄이고, 컴포넌트의 style을 확장하기 위해 styled-components를 사용토록 한다. 

$ meteor npm install --save styled-components


imports/ui/sdk/eui/flexgroup.style.tsx 파일 을 생성한다. 

  - 테스트로 확장 컴포넌트로 부터 받은 색을 적용해 본다. 

  - props는 camelCase로 설정한다.

import styled from 'styled-components';

import { EuiFlexItem, EuiFlexGroup } from '@elastic/eui';


export const StyledWidthEuiFlexGroup = styled(EuiFlexGroup)`

  width: ${props => props.width || '100%'};

`;


export const StyledLongEuiFlexItem = styled(EuiFlexItem)`

  max-width: 400px;

  input {

    background: ${props => props.background || 'yellow'};  // 동적으로 bgcolor를 적용한다.

    color: white;

  }

`;


export const StyledShortEuiFlexItem = styled(EuiFlexItem)`

  max-width: 100px;

`;


AddLink.tsx에서 사용한다. 

import { StyledLongEuiFlexItem, StyledShortEuiFlexItem } from '../../sdk/eui/flexgroup.style';


class AddLink extends React.Component<AddLinkProps, any> {

...

  makeForm = ({handleSubmit, submitting, pristine}) => {

    return (

      <form onSubmit={handleSubmit}>

        <EuiFlexGroup direction="row" gutterSize="s">

          <EuiFlexItem><EInput name="title" component="input" type="text" placeholder="Title" /></EuiFlexItem>

          <StyledLongEuiFlexItem background="red">

             <EInput name="url" component="input" type="text" placeholder="Url" />

          </StyledLongEuiFlexItem>

          <StyledShortEuiFlexItem>

            <EuiButton type="submit" fill disabled={submitting || pristine}>Add Link</EuiButton>

          </StyledShortEuiFlexItem>

        </EuiFlexGroup>

      </form>

    );

  }

...

}



Login.tsx에도 적용한다. 

  - FlexGroup  의 width=600px으로 설정한다. 

  - JPage, JRow를 설정한다. (아래에서 계속 개발)

import { JPage, JRow } from '../layouts/common.style';


export default class Login extends React.Component<LoginProps, LoginState> {

...

  makeForm = ({ handleSubmit, submitting, pristine }) => {

    return (

      <form onSubmit={handleSubmit}>

        <StyledWidthEuiFlexGroup width="600px" direction="row" gutterSize="s">

          <EuiFlexItem><EInput name="email" type="email" placeholder="Email" /></EuiFlexItem>

          <EuiFlexItem><EInput name="password" type="password" placeholder="Passowrd" /></EuiFlexItem>

          <StyledShortEuiFlexItem>

                <EuiButton type="submit" disabled={submitting || pristine}>Login</EuiButton>

          </StyledShortEuiFlexItem>

        </StyledWidthEuiFlexGroup>

      </form>

    );

  };


  public render() {

    return (

      <JPage>

        <JRow padding="10px" fontSize="20px">Login to short Link</JRow>

        <JRow>{this.state.error ? <p>{this.state.error} </p> : undefined}</JRow>

        <JRow><Form onSubmit={this.onLogin} render={this.makeForm} /></JRow>

        <JRow><Link to="/signup">Have a account?</Link></JRow >

      </JPage>

    );

  }

}


JPage와 JRow 컴포넌트를 imports/ui/layouts/common.style.ts 생성한다. 

  - JPage는  padding

  - JRow는 flexbox의 row direction으로 설정한 컴포넌트이다.

  - 컴포넌트의 properties를 통해 값을 받아 동적으로 설정한다. 

import styled from 'styled-components';


export const JPage = styled.div`

  display: block;

  position: relative;

  padding: 10px;

  height: 100%;

  width: 100%;

`;


export const JRow = styled.div`

  display: flex;

  justify-content: ${ props => props.align || 'flex-start' };

  align-items: ${ props => props.align || 'center' };

  width: ${ props => props.width || '100%' };

  padding: ${ props => props.padding || '5px' };

  font-size: ${ props => props.fontSize || '14px' };

`;



styled-components를 통해 얻을 수 있는 장점


  - 기존 컴포넌트의 style 확장을 컴포넌트 개념을 할 수 있다. 기존은 .css 작성

  - 일반 태그를 style을 동적으로 적용할 수 있는 컴포넌트로 만들어 React컴포넌트에서 Common 컴포넌트처럼 사용할 수 있다. 즉 Style만을 담당하는 컴포넌트임.




<참조>

- class 적용 방법들

- styled-components 홈페이지

posted by 윤영식
2018. 12. 3. 16:09 Meteor/React + Meteor

Ant Design 컴포넌트 또는 Elatstic UI 같은 컴포넌트 적용방법을 알아보자. 


  - Ant Design 컴포넌트 적용

  - Elastic UI  컴포넌트 적용

  - 적용 소스




Final Form 에 Ant Design 컴포넌트 적용하기


적용하기-1 블로그에서 Antd을 설치했다. Ant Design의 Form을 사용하여 Login, SignOut, AddLink의 Input을 수정한다. 

  - imports/ui/sdk/antd/antd-final-input.tsx 파일 생성

  -antd 의 input react 컴포넌트를  react-final-form의 input의 custom component로 설정한다. (third component support )

import * as React from 'react';

import { Input } from 'antd';

import { Field } from 'react-final-form';


// antd 의 react input component를 react-final-form에서 사용할 수 있는 custom component로 만듦

const AntdInput = ({input, ...rest}) => {

  return (

    <Input {...input} {...rest} />

  )

}


// 최종 애플리케이션에서 사용할 react-final-form 컴포넌트

const AInput = (props: any) => {

    return (

      <Field {...props} component={AntdInput} />

    );

}


export default AInput;


antd의 layout 컴포넌트은 Col, Row를 사용해서 레이아웃을 잡는다. 

import { Row, Col, Button } from 'antd';

import AInput from '../sdk/antd/antd-final-input';


export default class Login extends React.Component<LoginProps, LoginState> {

...

  makeForm = ({ handleSubmit, submitting, pristine }) => {

    return (

        <form onSubmit={handleSubmit}>

          <Col span={4}><AInput name="email" type="email" placeholder="Email" /></Col>

          <Col span={4}><AInput name="password" type="password" placeholder="Passowrd" /></Col>

          <Col span={2}><Button type="primary" htmlType="submit" disabled={submitting || pristine}>Login</Button></Col>

        </form>

    );

  };


  public render() {

    return (

      <div>

        <h1>Login to short Link</h1>

        {this.state.error ? <p>{this.state.error} </p> : undefined}

        <Row gutter={5}>

          <Form onSubmit={this.onLogin} render={this.makeForm} />

        </Row>

        <Row gutter={5}>

          <Link to="/signup">Have a account?</Link>

        </Row>

      </div>

    );

  }

}



AddLink.tsx, Signup.tsx도 동일하게 바꾸어 본다. 




Final Form에 Elastic UI 적용하기 


eui(Elastic UI)는 Elastic Search의 React기반 오픈소스이다. Elastic Search의 Kibana에서 사용중이다. AntD대신 Eui를 적용해 본다. 

eui는 yarn install 만 지원하는 관계로 Meteor에서 yarn을 사용하기 위해서 다음 과정을 최초 한번 설정한다. 

$ meteor npm i -g yarn

$ meteor yarn info 

yarn info v1.12.3


$ rm -rf node_modules (MS는 윈도우 명령으로)

$ rm package-lock.json


// package.json 내용 설치

$ meteor yarn 


  - @elastic/eui 패키지 설치

  - css 설정

// eui는 npm install을 지원하지 않는다. 

$ meteor yarn add  @elastic/eui


// client/main.tsx에서 import한다. 

import '@elastic/eui/dist/eui_theme_light.css';


// client/theme.less 안은 import는 주석처리한다.

// @import '{}/node_modules/antd/dist/antd.less';


Lisk화면을 다음과 같이 전환한다. 


react-final-form에 EuiFieldText (input tag) 컴포넌트를 적용한다. 

  - imports/ui/sdk/eui/eui-final-input.tsx 파일 생성

import * as React from 'react';

import { EuiFieldText } from '@elastic/eui';

import { Field } from 'react-final-form';


const EuiInput = ({ input, ...rest }) => {

  return (

    <EuiFieldText {...input} {...rest} />

  )

}


const EInput = (props: any) => {

  return (

    <Field {...props} component={EuiInput} />

  );

}


export default EInput;


imports/ui/Info.tsx 에서 EuiPage 관련 컴포넌트로 레이아웃을 꾸민다. 

  - EuiPageHeader, EuiPageContent로 나눔

  - 태그안의 정렬은 FlexBox가 적용된 EuiFlexGroup과 EuiFlexItem을 사용한다.

  - width, padding은 style일 직접 설정한다.

import {

  EuiFlexGroup, EuiFlexItem, EuiButton, EuiPage, EuiSpacer,

  EuiPageBody, EuiPageHeader,  EuiPageContent, EuiPageContentBody, EuiTitle

} from '@elastic/eui';


class Info extends React.Component<InfoProps, any> {

...

  render() {

    return (

      <EuiPage>

        <EuiPageBody>

          <EuiPageHeader>

              <EuiFlexGroup justifyContent="spaceBetween">

                <EuiFlexItem grow={1} style={{ paddingLeft: 20 }}>

                  <EuiTitle size="m"><h1>Add Link & List</h1></EuiTitle>

                </EuiFlexItem>

                <EuiFlexItem style={{ maxWidth: 130, paddingRight: 30 }}>

                  <EuiButton style={{ maxWidth: 100 }} onClick={this.onLogout}>Log out</EuiButton>

                </EuiFlexItem>

              </EuiFlexGroup>

          </EuiPageHeader>

          <EuiPageContent>

            <EuiPageContentBody>

              <EuiFlexGroup direction="column" justifyContent="spaceBetween">

                <EuiFlexItem style={{ maxWidth: 800 }}>

                  <AddLink /> 

                </EuiFlexItem>

                <EuiSpacer />

                <EuiFlexItem style={{ maxWidth: 400 }}>

                  {this.linkList()}

                </EuiFlexItem>

              </EuiFlexGroup>

            </EuiPageContentBody>

          </EuiPageContent>

        </EuiPageBody>

      </EuiPage>

    );

  }

}


imports/ui/pages/link/AddLink.tsx도 수정한다.

  - EuiFlexGroup으로 레이아웃 적용: flexbox direction은 row이다.

  - EInput 컴포넌트 적용

  - EuiButton 적용하기: type="submit" 설정

  - AntD적용 내용은 모두 주석처리한다. 

import EInput from '../../sdk/eui/eui-final-input';

import { EuiButton, EuiFlexGroup, EuiFlexItem } from '@elastic/eui';



class AddLink extends React.Component<AddLinkProps, any> {

...

  makeForm = ({handleSubmit, submitting, pristine}) => {

    return (

      <form onSubmit={handleSubmit}>

        {/* <Col span={4}><AInput name="title" component="input" type="text" placeholder="Title" /></Col>

        <Col span={4}><AInput name="url" component="input" type="text" placeholder="Url" /></Col>

        <Col span={2}><Button type="primary" htmlType="submit" disabled={submitting || pristine}>Add Link</Button></Col> */}

        <EuiFlexGroup direction="row" gutterSize="s">

          <EuiFlexItem><EInput name="title" component="input" type="text" placeholder="Title" /></EuiFlexItem>

          <EuiFlexItem><EInput name="url" component="input" type="text" placeholder="Url" /></EuiFlexItem>

          <EuiFlexItem style={{ maxWidth: 100 }}>

            <EuiButton type="submit" fill disabled={submitting || pristine}>Add Link</EuiButton>

          </EuiFlexItem>

        </EuiFlexGroup>

      </form>

    );

  }


  public render() {

    return (

      // <Row gutter={5}>

        <Form onSubmit={this.handleSubmit} render={this.makeForm} />

      // </Row>

      );

  }

}


Link.tsx도 수정한다. 

  - EuiButton 사이즈 small 설정

  - EuiButton minWidth 적용: default가 120px 이므로 overrriding을 minWidth: 40 을 설정한다

class Link extends React.Component<LinkProps, any> {

...

  public render() {

    const { link } = this.props;

    return (

      <li key={link._id}>

        <a href={link.url} target="_blank">{link.title}</a>

        <EuiButton size="s" style={{minWidth: 40, marginLeft: 10}} onClick={this.deleteLink}>x</EuiButton>

      </li>

    );

  }

}


Login.tsx, Signup.tsx도 eui 컴포넌트로 변경해 본다. 개인적으로 FlexBox를 많이 사용하는데 이에대한 컴포넌트 레벨 지원을 Eui가 제공하므로, 앞으로 Eui 컴포넌트를 사용한다. 다음은 style을 직접 적용하는 방식이 아니라 class 적용방법을 알아본다. 




<참조> 

- Ant Design React Component

- Elastic UI React Component 

- Meteor에서 yarn 사용하기 - 마지막 답변 참조

posted by 윤영식
2018. 11. 30. 15:48 Meteor/React + Meteor

React의 route 설정을 리팩토링해보고, createRef() 를 사용하지 않고 form을 변경해 본다. 


  - react-router v4에 맞는 redux 환경 재구성

  - react-final-form  적용하기

  - 사용자별 Link 목록 보여주기

  - 적용소스




React Router v4에 맞는 Redux 설정


react-router-redux는 react router v2와 v3에 맞는 패키지이고 react router v4에 맞는 connected-react-router로 교체해야 한다. recct-router-redux 설정 상태로는 redux action이 작동하지 않는다.

// 설치

$ meteor npm install --save connected-react-router

// 삭제

$ meteor npm uninstall --save react-router-redux

$ meteor npm uninstall --save @types/react-router-redux


imports/ui/store.ts 리팩토링

  - Routes.tsx의 browserHistory 를 store.ts로 옮김

  - connectRouter로 routerReducer를 생성

  - RouterAction, LocationChangeAction을 추가

import { createBrowserHistory } from 'history';

import { connectRouter, RouterAction, LocationChangeAction } from 'connected-react-router';


export const browserHistory = createBrowserHistory();


const rootReducer = combineReducers({

  router: connectRouter(browserHistory),

  links: linkReducer

});


export type RootState = StateType<typeof rootReducer>;


type ReactRouterAction = RouterAction | LocationChangeAction;

export type RootAction = ReactRouterAction | LinkAction;


imports/ui/Routes.tsx 리팩토링

  - react-router v4의 Router를 사용하지 않고, connected-react-router의 ConnectedRouter를 사용

  - 인자로 store.ts 에서 생성한 browserHistory를 사용

import { ConnectedRouter } from 'connected-react-router';

import store, { browserHistory } from './store';


export const Root = (

  <Provider store={store}>

    <ConnectedRouter history={browserHistory}>

      <Switch>

        <Route exact path="/" component={Login} />

        <Route path="/main" component={App} />

        <Route path="/signup" component={Signup} />

        <Route path="/links" component={InfoContainer} />

        <Route path="*" component={NotFound} />

      </Switch>

    </ConnectedRouter>

  </Provider>

);


이제 Redux Chrome Extension에서 LOCATION_CHANGE 액션을 볼 수 있다. 




final-form 통해 입력


form 관련부분을 final-form으로 변경한다. React버전의 react-final-form을 사용한다. 

$ meteor npm install --save final-form 

$ meteor npm install --save react-final-form


로그인 화면부터 react-final-form으로 변경해 본다. 

  - react-final-form의 Form, Field import

  - Form 의 onSubmit={this.onLogin}을 통해 입력한 values 객체를 받기, 보통 폼 필드의 name을 key로하여 json 객체를 받는다. 

  - <input> 태그를 <Field> 태그로 변경

  - <button> 태그에 disabled 속성추가

  - 필요없는 부분 추석처리: React.createRef()

import * as React from 'react';

import { Link } from 'react-router-dom';

import { Meteor } from 'meteor/meteor';

import { Form, Field } from 'react-final-form';


export interface LoginProps {

  history: any;

}


export interface LoginState {

  error: string;

}


export default class Login extends React.Component<LoginProps, LoginState> {

  // email: any = React.createRef();

  // password: any = React.createRef();


  constructor(props) {

    super(props);

    this.state = {

      error: ''

    };

  }


  onLogin = ({email, password}) => {

    // e.preventDefault();

    // let email = this.email.current.value.trim();

    // let password = this.password.current.value.trim();

    if (!email || !password) {

      this.setState({error: 'Please input email and password both'});

      return;

    }

    Meteor.loginWithPassword({ email }, password, (err) => {

      if (err) {

        this.setState({ error: err.reason });

      } else {

        this.setState({ error: '' });

      }

    });

  }


  makeForm = ({ handleSubmit, submitting, pristine, values }) => {

    return (

      <form onSubmit={handleSubmit}>

        <Field name="email" component="input" type="email" placeholder="Email" required/>

        <Field name="password" component="input" type="password" placeholder="Passowrd"/>

        {/* <input type="email" ref={this.email} name="email" placeholder="Email" />

        <input type="password" ref={this.password} name="password" placeholder="Password" /> */}

        <button type="submit" disabled={submitting || pristine}>Login</button>

      </form>

    );

  };


  public render() {

    return (

      <div>

        <h1>Login to short Link</h1>

        {this.state.error ? <p>{this.state.error} </p> : undefined}

        <Form onSubmit={this.onLogin} render={this.makeForm} />

        <Link to="/signup">Have a account?</Link>

      </div>

    );

  }

}


Signup.tsx, AddLink.tsx 도 변경한다. 




사용자별 Link 목록 보여주기


사용자를 추가하여 각 사용자가 등록한 목록만 보기 위해서 pub/sub 설정에서 userId  파라미터를 넘겨주어 본인 목록만 조회하여 publish 한다. 

// imports/ui/Info.tsx 맨 하단의 subscribe시에 자신의 아이디를 파라미터로 보낸다. 

export default compose(

  withTracker(() => {

    const connection = Meteor.subscribe('links', {userId: Meteor.userId()});

    return {

      links: Links.find().fetch(),

      loading: !connection.ready()

    };

  }),

  connect(mapProps)

)(Info);


imports/api/links.ts에서 userId를 파라미터로 find한다. 

if (Meteor.isServer) {

  Meteor.publish('links', ({userId}) => {

    console.log('userId:', userId);

    if (!userId) {

      return this.ready();

    }

    return Links.find({owner: userId});

  });

  ...

}




<참조>

- connected-react-router 저장소

- react-final-form 소개 영상

posted by 윤영식
2018. 11. 28. 16:56 Meteor/React + Meteor

Meteor와 React 환경을 좀 더 Production에 가깝게 만들어 본다. 


  - Insecure 제거 및 allow/deny 설정

  - React의 Refs에 대해 변경

  - Meteor methods/call 사용한 목록 조회

  - SimpleSchema 적용

  - 적용소스




Insecure 패키지 제거 및 Allow/Deny 설정


모든 컬렉션에 대한 자동 subscribe에 대한 autopublish 패키지는 이미 제거를 했고, 다음으로 insecure 패키지를 제거하여 컬렉션의 접근 권한을 제어하는 allow/deny를 설정한다. 

$ meteor remove insecure


insecure를 제거하게 되면 read만 가능하고 update, delete, insert가 불가능하다. 따라서 권한부분은 서버에서 실행되기 때문에 allow/deny설정을 Meteor.isServer블럭안에서 해주어야 한다.

  - allow: true로 설정된 것만 가능하고 그외는 모두 불가능하다. (가능한 것이 적으면 allow 설정)

  - deny: true로 서정된 것만 불가능하고 그외는 모두 가능하다.  (불가능한 것이 적으면 deny 설정)

// api/links.ts

import { Meteor } from 'meteor/meteor';

import { Mongo } from 'meteor/mongo';


const Links = new Mongo.Collection('links');


if (Meteor.isServer) {

  Meteor.publish('links', () => Links.find());

  Links.allow({

    insert (userId: string, doc: any) {

      console.log('insert doc:', doc);

      return (userId && doc.owner === userId);

    },

    remove(userId: string, doc: any) {

     console.log('delete doc:', doc);

      return (userId && doc.owner === userId);

    }

  })

}

export default Links;


 allow에서 owner 아이디를 비교하므로 epic에서 owner 값을 설정한다. 

// imports/ui/pages/link/link.epic.ts

const addLink: Epic = (

  action$,

  store

) =>

  action$.pipe(

    filter(isOfType(actions.ADD_REQUEST)),

    switchMap(action => {

      const { title, url } = action.payload;

      const owner = Meteor.userId();

      return insertCollection(Links, { title, url, owner, createdAt: new Date() })

    }),

  ...

);


userId는 로그인을 하게되면 이미 서버와 동기화 되어 서버가 가지고 있는 userId를 첫번째 파라미터로 넘겨준다. 두번째 파라미터 doc은 insert할 때 입력한 파라미터값 이고, remove의 doc은 저장된 몽고디비의 document이다. 

// meteor run을 수행한 콘솔창에 찍힌다. 

=> Meteor server restarted

I20181123-15:00:36.619(9)? insert doc: { title: 'google2',

I20181123-15:00:36.676(9)?   url: 'http://www.google.com',

I20181123-15:00:36.676(9)?   owner: 'AmMBJzZ33Nc8Bsqhe',

I20181123-15:00:36.676(9)?   createdAt: 2018-11-23T06:00:36.611Z }

I20181123-15:00:39.955(9)? remove doc: { _id: 'Zrb8jGSpt2AyRn4Q3',

I20181123-15:00:39.955(9)?   title: 'google2',

I20181123-15:00:39.955(9)?   url: 'http://www.google.com',

I20181123-15:00:39.955(9)?   owner: 'AmMBJzZ33Nc8Bsqhe',

I20181123-15:00:39.955(9)?   createdAt: 2018-11-23T06:00:36.611Z }




Form 입력의 React의 Refs 변경


React에서 DOM객체 접근 방법은 3가지 이고, string과  콜백펑션말고 16.3 버전이후 나온 createRefs()를 사용한다. 

  - ref="string"

  - ref={callback-function}

  - createRefs()


DOM 객체를 받을 변수를 선언하고, ref={this.변수}로 할당한다. 값 접근은 this.변수.current 객체를 통한다.

// imports/ui/pages/Login.tsx

.. 중략 ..

export default class Login extends React.Component<LoginProps, LoginState> {

  email: any = React.createRef();

  password: any = React.createRef();


  constructor(props) {

    super(props);

    this.state = {

      error: ''

    };

  }


  onLogin = (e: any) => {

    e.preventDefault();


    let email = this.email.current.value.trim();

    let password = this.password.current.value.trim();

    Meteor.loginWithPassword({ email }, password, (err) => {

      if (err) {

        this.setState({ error: err.reason });

      } else {

        this.setState({ error: '' });

      }

    });

  }


  public render() {

    return (

      <div>

        <h1>Login to short Link</h1>

        {this.state.error ? <p>{this.state.error} </p> : undefined}

        <form onSubmit={this.onLogin}>

          <input type="email" ref={this.email} name="email" placeholder="Email" />

          <input type="password" ref={this.password} name="password" placeholder="Password" />

          <button>Login</button>

        </form>

        <Link to="/signup">Have a account?</Link>

      </div>

    );

  }

}


Signup.tsx과 AddLink.tsx도 동일하게 변경한다. 




Meteor methods/call을 사용한 저장/삭제


imports/sdk/utils/ddp.util.ts 에 method call에 대한 observable 반환 메소드 추가

export function insertCall(methodName: string, params: any): Observable<RequestModel> {

  return from(new Promise((resolve, reject) => {

    Meteor.call(methodName, params, (error, result) => {

      if (error) {

        reject({ error: true, result: { ...error }, params: { ...params } });

      }

      if (typeof result === 'string' || typeof result === 'number') {

        resolve({ success: true, result, params: { ...params } });

      } else {

        resolve({ success: true, result: { ...result }, params: { ...params } });

      }

    });

  }));

}


export function removeCall(methodName: string, _id: string): Observable<RequestModel> {

  return from(new Promise((resolve, reject) => {

    Meteor.call(methodName, _id, (error, result) => {

      if (error) {

        reject({ error: true, result: { ...error }, params: { _id } });

      }

      if (typeof result === 'string' || typeof result === 'number') {

        resolve({ success: true, result, params: { _id } });

      } else {

        resolve({ success: true, result: { ...result }, params: { _id } });

      }

    });

  }));

}


imports/api/links.ts 에서 Meteor.methods를 추가한다. 

if (Meteor.isServer) {

 ... 

  Meteor.methods({

    insertLink(params: any) {

      if (!this.userId) {

        throw new Meteor.Error('Please login');

      }

      return Links.insert(params);

    },

    removeLink(_id: string) {

      if (!this.userId) {

        throw new Meteor.Error('Please login');

      }

      return Links.remove(_id);

    }

  });

}


imports/ui/pages/link/link.epic.ts 에서 collection 을 호출하지 않고, Meteor.call을 호출 한다. takeUntil은 브라우져 이동등을 할때 호출을 끊는 역할을 한다. 현재는 주석처리로 미구현상태임.

const addLink: Epic = (

  action$,

  store

) =>

  action$.pipe(

    filter(isOfType(actions.ADD_REQUEST)),

    switchMap(action => {

      const { title, url } = action.payload;

      const owner = Meteor.userId();

      return insertCall('insertLink', { title, url, owner, createdAt: new Date() })

      // return insertCollection(Links, { title, url, owner, createdAt: new Date() })

    }),

    map((response: RequestModel) => {

      if (response.error) {

        return actions.addLinkFailed({ ...response.result })

      }

      return actions.addLinkSuccess(response.result)

    }),

    // takeUntil(action$.pipe(

    //   filter(isOfType(actions.ADD_REQUEST))

    // ))

  );


const removeLink: Epic = (

  action$,

  store

) =>

  action$.pipe(

    filter(isOfType(actions.DELETE_REQUEST)),

    switchMap(action => {

      return removeCall('removeLink', action.payload);

      // return removeCollection(Links, action.payload);

    }),

    map((response: RequestModel) => {

      if (response.error) {

        return actions.removeLinkFailed({ ...response.result, ...response.params })

      }

      return actions.removeLinkSuccess(response.params._id);

    }),

    // takeUntil(action$.pipe(

    //   filter(isOfType(actions.ADD_REQUEST))

    // ))

  );




SimpleSchema 적용하기 


Validation을 위해 simple schema 패키지를 설치한다. 

$ meteor npm install --save simpl-schema


/imports/api/links.ts 에 schema를 정의한다. 

import { Meteor } from 'meteor/meteor';

import { Mongo } from 'meteor/mongo';

import SimpleSchema from 'simpl-schema';


const Links = new Mongo.Collection('links');


if (Meteor.isServer) {

 ...

  const linkSchema = new SimpleSchema({

    title: {

      type: String,

      min: 3

    },

    url: {

      type: String

    },

    owner: {

      type: String

    },

    createdAt: {

      type: Date 

    }

  });


  Meteor.methods({

    insertLink(params: any) {

      if (!this.userId) {

        throw new Meteor.Error('Please login');

      }

      try {

        linkSchema.validate(params);

        return Links.insert(params);

      } catch(e) {

        throw new Meteor.Error('no valid schema');

      }

    },

   ...

  });

}

export default Links;


account에 대한 validate도 정의해 본다. imports/api/ 폴더아래에 account-validate.ts 파일을 생성하고 server/main.ts에서 import한다. 

// account-validate.ts

import { Meteor } from 'meteor/meteor';

import { Accounts } from 'meteor/accounts-base';

import SimpleSchema from 'simpl-schema';


if (Meteor.isServer) {

  Accounts.validateNewUser((user) => {

    const email = user.emails[0].address;


    try {

      new SimpleSchema({

        email: {

          type: String,

          regEx: SimpleSchema.RegEx.Email

        }

      }).validate({ email });

    } catch(e) {

      throw new Meteor.Error(400, e.message);

    }


    return true;

  });

}


// server/main.ts

import { Meteor } from 'meteor/meteor';

import '../imports/api/links';

import '../imports/api/account-validate';


Meteor.startup(() => {

});


// Singup.tsx 파일에 테스트를 위해 noValidate를 추가한다. 

<form onSubmit={this.onCreateAccount} noValidate>




<참조>

React createRefs() 사용하기

- Meteor methods/call 사용하기

posted by 윤영식
2018. 11. 26. 16:05 BlockChain

2018 W3C 컨퍼런스를 요약한다. 관심분야는 Web3.0, WebRTC, Game, BlockChain 이다. 물론 경품도...




Web 3.0


web2.0 시대에 사용자의 데이터를 가지고 정보로 가공하여 장사를 하고 있다. 플랫폼 독점 기업의 특징. 예) 구글, 페이스북


탈중앙화된 웹의 필요성

  - 데이터 정보 제공자에게 더 많은 인센티브를 줘야 한다. 

  - 사용자가 정보의 주인

  - 거대 플랫폼으로부터 자유로운

  - 장애와 공격으로 부터 자유로운


데이터의 탈중앙화가 목표

  - 데이터가 서비스 업체의 저장소에 있는 것이 아니라 프로토콜 내에 있는 것이다. 

  - 여러 블록체인 서비스는 새로운 프로토콜의 서비스이다. 예) 스팀잇

  - 구조의 변경을 의미하고 팀버너스리가 Web3.0을 주창하고 있다.


변경되는 점

  - 프로토콜 개발은?

  - 애플리케이션 개발은?j


libp2p

  - CASTO: 리모트몬스터에서 하고 있는 서비스: live stream broadcast를 위한 decentralized web

    MiniStreamer 소스코드

  - IPFS 정의




CASTO


분산화 방송 플랫폼 코어 기술을 만들고 있음.

  - 블록체인: 서버 + DB + 분산시스템 + 보상시스템

  

이더리움선택

  - geth

  - remix: 웹 IDE 

  - solidity: 개발 언어

  - web3.js: geth 접근, JSONRPC 사용

  - metamask: web3.js 포함, 이더리움 월렛 역할, 사용자 Auth(컨트랙트 실행권한)  

    => 로컬 개발 내역을 웹으로통해 geth node로 접근할 때 반드시 필요하다. 

    => Ganache 사이트 참조


메타마스크

  - 소스분석

  - 소스의 web3는 메타마스크가 주입해 준것이다. 

  - 신규는 비동기 async await 사용 가능 (구버전은 callback방식)

// https://github.com/MobiconSoft/simple-blog-example-on-ethereum/blob/master/src/index.js

let web3js = new Web3(web3.currentProvider);




Serverless P2P 구축



- IPFS의 근간이 되는 libp2p 라이브러리를 근간으로 사용하는 애플리케이션일뿐...

- libp2p 브라우져, Node.js, Javascript 지원

- 연결과정

  + discover: peer 끼리 dial 

  + 연결받은 peer가 handle을 상대 peer에게 전달한다. 

- mutliAddr를 제공하여 접속

  + 서로 다른 네트워크 환경이어도 접근이 가능하다. 

- lib2p2는 TCP, WebRTC, WebSockets, UDP를 제공한다.

- Node 장애에서도 무정지 버시스 대응이 가능하다.

// 로컬 노드에서 수행하기 (소스)

$ npm install -g libp2p-webrtc-star

$ start-signal --port 12345


- 만일 electron등의 애플리케이션으로 수행을 하면 해당 애플리케이션 WebRTC server 즉, Node역할을 할 수 있다. 




HTML5 기반 블록체인 서비스


WebRTC + Blockchain 기반의 웹 게임 서비스의 개발: PlayDapp

  - 블록체인에서 웹, WebRTC가 굉장히 중요해진다. 

  - 통화로: 비트코인, 캐시, 이더리움, EOS, 리플만 사용


사용이유

  - 고객의 데이터로 이익창출이지만 고객에게 돌아가는 보상이 없다.

  - 데이터 중앙집중 관리로 해킹 공격 대상

  - 서비스의 영속성 문제: 경영 문제시 서비스 Shutdown


web + p2p + blockchain 을 통한 platform의 확산이 예상됨.




<참조>

- 블록체인이 불러온 Web3.0

- IPFS란 무엇인가?

- Awesome IPFS 목록

- 이재호님 Serverless libp2p 발표 슬라이드


posted by 윤영식
2018. 11. 25. 18:05 AI Deep Learning

자연어 처리 강좌를 정리한다. 





개념


- 문장을 분해하여 관계를 만들어 주어야 한다. 

- 데이터를 서로 연결하다보면 2차원 이상으로 발전한다. 

- 자연어는 모델링은 쉽지만, 전처리와 결과의 해석이 중요한다. 결과에 따른 해석이 따라 의미가 틀려진다. 이는 이미지/음성과 틀린 부분이다.

- 음운론, 형태론, 통사론, 의미론, 추리론

- Document -> Tokenizing -> Streaming/Tagging (컴퓨터가 이해하는 방식으로 배치하기) -> 최종 Word2Vec 에서 사용함




Tokenizing


- 구글 Colab을 사용한다.

- Token: document에서 쓸만한 것들로 의미를 같는 문자열 -> Tokenizing

- Colab에서 필요한 모듈 설치하기 in ubuntu

! apt-get update

! apt-get install g++ openjdk-8-jdk 

! pip3  install  nltk konlpy wordcloud matplotlib gensim 


! apt-get install fonts-nanum*

! apt-get install fontconfig

! fc-cache -fv

! cp /usr/share/fonts/truetype/nanum/Nanum* /usr/local/lib/python3.6/dist-packages/matplotlib/mpl-data/fonts/ttf/

! rm -rf /content/.cache/matplotlib/*


- import nltk # Natural Language ToolKit 사용

  + sent_tokenize, word_tokenize

- import re  #정규표현식 사용

// 패키지 사용

text = """갤럭시(GalaxyNote9)노트9. 2018년 08월 폭발적인 인기를 이끌고 있습니다. 

담당자 010-222-9999. 홍보팀 010-8888-9999"""

from nltk import sent_tokenize, word_tokenize, FreqDist

sent_tokenize(text)

tokens = word_tokenize(text)


// 정규표현식

import re

tokenizer = re.compile(r'[가-힣]+')

tokenizer.findall(text)




Stemming & Tagging


순서 Tokenizing -> Stemming -> Tagging을 한다. 일반/평문이 결과값이 잘나오고, 강조문/도치문/압축문은 일반/평문으로 만들어하는게 좋다. 


Stemming: 정규화

- 토큰의 어근/어간 추출

- 가능한 비슷한 의미 형태로 묶어주기 => 원형화한다

   예) 가다, 가니, 가고 => 가- 

         산뜻하다, 산뜻하니, 산뜻하고 => 산뜻하-


Tagging: 문법/Filter

- Token별 높은 확률의 태그(속성값/문법)을 추가 

- 단어의 품사별로 묶음: 동사, 명사, 부사


// 토큰

text = "Don't hesitate to ask question"

from nltk.tokenize import TreebankWordTokenizer

tokenizer = TreebankWordTokenizer()

token = tokenizer.tokenize(text)


// 태그

from nltk import pos_tag

pos_tag(token)


// 한글처리

// 테크

from konlpy.tag import Okt

twitter = Okt()

twitter.pos('서울R&D캠퍼스 수업자료')


//토큰, 스테밍을 같이 처리 stem === stemming

text = "워런 버핏은 삼성전자가 아닌 애플주식을 왜 샀을까"

print(twitter.pos(text, stem="true"))




Word Cloud로 시각화하기


- Visualization (시각화)

- 연설문을 워드클라우드로 표현하기 

// 필요 모듈 설치

! apt-get update

! apt-get install g++ openjdk-8-jdk 

! pip3  install  nltk konlpy wordcloud matplotlib gensim 


! apt-get install fonts-nanum*

! apt-get install fontconfig

! fc-cache -fv

! cp /usr/share/fonts/truetype/nanum/Nanum* /usr/local/lib/python3.6/dist-packages/matplotlib/mpl-data/fonts/ttf/

! rm -rf /content/.cache/matplotlib/*

speech_text = "https://raw.githubusercontent.com/YongBeomKim/nltk_tutorial/master/data/pyongyang_fin.txt"

script_text = "https://raw.githubusercontent.com/YongBeomKim/nltk_tutorial/master/data/movie_memories_of_murder_2003.txt"

font_file = "/usr/local/lib/python3.6/dist-packages/matplotlib/mpl-data/fonts/ttf/NanumGothicCoding.ttf"


// 워드클라우드 사용

import requests

import pandas as pd

import matplotlib.pyplot as plt

from wordcloud import WordCloud

from nltk import FreqDist

from nltk.tokenize import word_tokenize

import nltk

nltk.download('punkt')


texts = requests.get(speech_text).text

texts[:100]


// 그리기 

%matplotlib inline

wcloud = WordCloud(font_file).generate(texts)

plt.figure(figsize=(12,12))

plt.imshow(wcloud)

plt.axis('off')


명사만을 사용하여 word cloud 표현

from konlpy.tag import Okt

twitter = Okt()

tokens = twitter.pos(texts, stem=True)

tokens_noun = [token[0] for token in tokens if token[1] == 'Noun']

texts_noun = " ".join(tokens_noun)

texts_noun[:300]




Deep Learning 수행


Natural Language Processing + Deep Learning을 결합하여 챗봇을 만들어가기

  - RNN, LSTM, GRU가 챗봇만드는 기본 알고리즘

  - Data 입력/전처리 (Train Data) -> Cell 선택 및 모델 구성 -> Training / Modeling / Validation -> Model 평가 

  - Input -> Hidden Layer -> Output

  - Input Data 를 구성하는 Word2Vec


Word2Vec는 문장에 따라 데이터를 구조화한다. 

  - 하나의 중심데이터와 주변데이터로 구분함. 

  - 단어간 유사도 검사

  - 구조자체가 작아서, 딥러닝이라 할 수 없다. 

  - 로지스틱 회귀식(2 Cell)을 통해 분류한다. 

  - Corpus (말뭉치) -> Vocabulary Builder -> Context Builder -> Input -> Output 

  


새로운 방식

  - Input 하나에 Output 여러개를 통해 학습 => Skip-gram (sg)

  - try -> error를 통해 맞는 길로 찾아는 방식

과거 방식

  - Input 여러개에 대한 Output 으로 중심 단어 하나 => CBOW (Content Back of Word)


     



실습


  - 20차원의 구조를 만든다. 

  - Dimension(차원)안에 토큰을 넣는다. 비슷한 토큰은 같은 차원에 넣는다. 

  - 빈도수가 적은 것은 하나의 차원에 묶는다. 이안에 빈도수가 높은 것은 중심축에 놓는다. 

    



- size: 차원 30개 (중요)  <== 실습 단어가 151개여서 유의미화를 위해 30차원으로 줄여서 설정했음.

- window: 주변 데이터

- min_count: 10번이상 등장한 단어만 (중요)

- hs: ?

- workers: cpu parallel 동작

- sg: skip-gram

// 전처리후 

...

// 명사 저장

sentences = txtnoun(sentences, skip=skips, tags=["Noun"])

script_file = 'scripts.txt'

with open(script_file, 'w', encoding="utf-8") as file:

   file.write(sentences)


// gensim 이용 모델 만들기

%%time

from gensim.models import word2vec

data = word2vec.LineSentence(script_file)

model = word2vec.Word2Vec(data, size=30, window=2, min_count=10, hs=1, workers=4, iter=100, sg=1)

model_file = "script.model"

model.save(model_file)


결과

CPU times: user 1.59 s, sys: 113 ms, total: 1.71 s, Wall time: 1.7 s


// 모델의 단어수 

model = word2vec.Word2Vec.load(model_file)

len(model.wv.vocab.keys())

list(model.wv.index2word)


결과

151 


// 근접 백터값 확인하기, 참깨밭을 제거하고 추출해 봄

model.wv.most_similar(['현장','백광호'], negative=['참깨밭'], topn=20)


30차원을 시각화하기 위해 skitlearn의 TSNE를 이용해서 2차원으로 변경해서 표현한다. 




<참조>

- 강좌 슬라이드, 전체 실습 파일

- 정규 표현식 정리, 실습하는 서비스

KoNLP

- Word2Vec 관련 이론

posted by 윤영식
2018. 11. 22. 14:37 Meteor/React + Meteor

Meteor의  accounts-password를 패키지를 통한 로그인 환경 만들기를 해본다. 


  - accounts-password 패키지를 통한 미티어 사용자 가입/로그인 사용 방법 알기

  - React Router 설정

  - 미티어의 Tracker.autorun을 통해 reactivity computation과 resource 사용 방법 알기

  - 소스코드




미티어 사용자 관리


accounts-password 패키지를 설치하고, 가입 및 로그인 화면을 만든다. 

$ meteor add accounts-password

$ meteor npm install --save bcrypt

// Link 사용을 위해 

$ meteor npm install --save react-router-dom


Signup.tsx와 Login.tsx 생성한다. 

  - email, password의 값을 읽기 위하여 typescript방식의 public refs를 정의한다. 

  - Accounts.createUser를 통해 사용자를 등록한다.

// Singup.tsx

import * as React from 'react';

import { Link } from 'react-router-dom';

import { Accounts } from 'meteor/accounts-base'


export interface SignupProps {}

export default class Signup extends React.Component<SignupProps, any> {

  // @see https://goenning.net/2016/11/02/strongly-typed-react-refs-with-typescript/

  public refs: {

    email: HTMLInputElement,

    password: HTMLInputElement

  }


  constructor(props) {

    super(props);

    this.state = {

      error: ''

    };

  }


  onCreateAccount = (e: any) => {

    e.preventDefault();

    let email = this.refs.email.value.trim();

    let password = this.refs.password.value.trim();

    Accounts.createUser({email, password}, (err) => {

      if (err) {

        this.setState({ error: err.reason });

      } else {

        this.setState({ error: '' });

      }

    });

  }


  public render() {

    return (

      <div>

        <h1>Signup to short Link</h1>

        { this.state.error ? <p>{this.state.error} </p> : undefined}

        <form onSubmit={this.onCreateAccount}> 

          <input type="email" ref="email" name="email" placeholder="Email"/>

          <input type="password" ref="password" name="password" placeholder="Password"/>

          <button>Create Acount</button>

        </form>

        <Link to="/login">Already have a account?</Link>

      </div>

    );

  }

}


Login.tsx

  - refs 정의

  - Meteor.loginWithPassword를 통해 로그인한다.

import * as React from 'react';

import { Link } from 'react-router-dom';

import { Meteor } from 'meteor/meteor';


export interface LoginProps {

  history: any;

}

export default class Login extends React.Component<LoginProps, any> {

  public refs: {

    email: HTMLInputElement,

    password: HTMLInputElement

  }


  constructor(props) {

    super(props);

    this.state = {

      error: ''

    };

  }


  onLogin = (e: any) => {

    e.preventDefault();

    let email = this.refs.email.value.trim();

    let password = this.refs.password.value.trim();

    Meteor.loginWithPassword({ email }, password, (err) => {

      if (err) {

        this.setState({ error: err.reason });

      } else {

        this.setState({ error: '' });

      }

    });

  }


  public render() {

    return (

      <div>

        <h1>Login to short Link</h1>

        {this.state.error ? <p>{this.state.error} </p> : undefined}

        <form onSubmit={this.onLogin}>

          <input type="email" ref="email" name="email" placeholder="Email" />

          <input type="password" ref="password" name="password" placeholder="Password" />

          <button>Login</button>

        </form>

        <Link to="/signup">Have a account?</Link>

      </div>

    );

  }

}


NotFound.tsx 파일도 생성한다. 

import * as React from 'react';

export default () => <div>Not Found Page</div>




React Router 설정


라우팅 설정을 통해 가입, 로그인후 Link화면으로 이동하는 설정한다. 


history 모듈을 설치한다.

$ meteor npm install --save history


client/main.tsx에 Route설정을 한다.

import * as React from 'react';

import { Meteor } from 'meteor/meteor';

import { render } from 'react-dom';

import { Provider } from 'react-redux';

import { Route, Router, Switch } from 'react-router-dom';

import { createBrowserHistory } from 'history';

 

import App from '../imports/ui/App'

import Login from '../imports/ui/pages/Login';

import Signup from '../imports/ui/pages/Signup';

import InfoContainer from '../imports/ui/Info';

import NotFound from '../imports/ui/pages/NotFound';

import store from '../imports/ui/store';


const browserHistory = createBrowserHistory();

const Root = (

  <Provider store={store}>

    <Router history={browserHistory}>

      <Switch>

        <Route exact path="/" component={Login} />

        <Route path="/main" component={App} />

        <Route path="/signup" component={Signup} />

        <Route path="/links" component={InfoContainer} />

        <Route path="*" component={NotFound} />

      </Switch>

    </Router>

  </Provider>

);


Meteor.startup(() => {

  render(Root, document.getElementById('react-target'));

});


links는 인증된 사용자만 볼 수 있으므로 Tracker.autorun이라는 Reactivity Computation 영역에서 Meteor.user()라는 Reactivity Source가 로그인 상태인지 검사한다. 

  - 로그인 상태가 아니면 무조건 login화면으로 이동

  - 로그인 상태이면서 인증이 필요없는 화면인 경우 link화면으로 이동

// client/main.tsx 

import { Tracker } from 'meteor/tracker';


Tracker.autorun(() => {

  const isAuthenticated = !!Meteor.user();

  if (isAuthenticated) {

    const pathname = browserHistory.location.pathname;

    const unauthenticatedPages: any = ['/', '/signup'];

    const isUnauthenticatedPage = unauthenticatedPages.includes(pathname);

    if (isUnauthenticatedPage) {

      browserHistory.push('/links'); // main

    } else {

      browserHistory.push(pathname);

    }

  } else {

    browserHistory.push('/'); // login

  }

});


Meteor.startup(() => {

  render(Root, document.getElementById('react-target'));

});


imports/ui/Info.tsx 에 logout 기능 추가

...

import { Accounts } from 'meteor/accounts-base';


interface InfoProps {

  links: any;

  loading: boolean

}


class Info extends React.Component<InfoProps, any> {


  linkList() {

    const { links, loading } = this.props;

    if (loading) {

      return <div>loading...</div>

    } else {

      return <LinkList links={links} />

    }

  }


  onLogout = () => {

    Accounts.logout();

  }


  render() {

    return (

      <div>

        <button onClick={this.onLogout}>Log out</button>

        <AddLink />

        {this.linkList()}

      </div>

    );

  }

}




가입 및 로그인 테스트 확인하기


가입을 하고, 로그인을 한다. 

Chrome Extension 으로 MiniMongoExplorer를 설치하고 user Collection의 내용을 확인한다. 


또는 Chrome DevTool의 Application에서 로그인 사용자의 userId와 console에서 확인해 볼 수 있다. 또한 Link페이지에서 로그아웃을 해보고 콘솔을 확인해 본다. 




<참조>

- 크롬 확장툴 MiniMongoExplorer

- 세번째 글 소스

posted by 윤영식