Nx Workspace와 NgRx/Platform을 연결한 boilerplate를 만들어 본다.
- Nx Workspace 생성하기
- NgRx 개념이해 및 사용예 만들기
- 다음 3가지 문서와 동영상을 참조한다.
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를 지정할 수 있다.
$ 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 파일에 자동으로 정보가 입력된다.
$ 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 자동 호출
- 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>
) {}
}
<참조>
- Angular Workspace의 Schematics 설정 목록
- Nx workspace에서 component 생성하기 명령
- Angular Enterprise Architecture Pattern
'Angular > Architecture' 카테고리의 다른 글
[Platform & Plugin] 아키텍트 기술 검토 - 2 (0) | 2019.04.10 |
---|---|
[Platform & Plugin] 아키텍트 기술 검토 - 1 (0) | 2019.04.08 |
[Angular Architecture] 애플리케이션 구조 설계 - 2 (0) | 2018.12.27 |
[Angular Architecture] 애플리케이션 구조 설계 - 1 (0) | 2018.12.13 |