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

Publication

Category

Recent Post

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 윤영식