export * from './lib/ajax/http.service';
export * from './lib/i18n/i18n';
다음으로 apps/gateway/web/src/main.tsx 에서 initI18N을 초기화 한다.
import * as ReactDOM from 'react-dom';
import { initI18N } from '@rnm/ui';
import App from './app/app';
import { config } from './environments/environment';
initI18N(config);
ReactDOM.render(<App />, document.getElementById('root'));
개발환경에서 Dashboard Web Dev Server로 연결하기
Gateway - Dashboard 로컬 개발시에는 총 4개의 프로세스가 구동되고 상호 연관성을 갖는다.
Gateway API (NodeJS & NestJS), Gateway Frontend (Web Dev Server & React) 로 Gateway하나에 두개의 프로세스가 구동된다.
Dashboard API, Dashboard Frontend 도 두개의 프로세스가 구동된다.
4개 프로세스간의 관계
개발시에 전체 루틴을 처리하고 싶다면 위와 같은 Proxy 설정이 되어야 한다. 환경 설정을 다음 순서로 진행한다.
Step-1) Gateway Web에서 Gateway API로 Proxy
apps/gateway/web/proxy.conf.json 환경은 Dashboard, Configuration, Back-Office 모두를 proxy 한다. 그리고 apps/gateway/web/project.json 안에 proxy.conf.json과 포트 9000 을 설정한다.
Step-2) Gateway API에서 Dashboard Web으로 Proxy
apps/gateway/api/src/environments/config.json 에서 REVERSE_ADDRESS가 기존 Dashboard API 의 8001 이 아니라, Dashboard Web의 9001 로 포트를 변경하면 된다.
Step-3) Dashboard Web 에서 Dashboard API로 proxy
Dashboard API로 proxy 하기위해 apps/dashboard/web/proxy.conf.json 파일을 추가한다. api 호출은 dashboard api의 8001로 proxy 한다.
baseHref: "/dashboard/"를 설정한다. "/dashboard"로 하면 안된다.
Step-4) Dashboard API 변경사항
apps/dashboard/api/src/public/dashboard 하위 내역을 모드 apps/dashboard/api/src/public으로 옮기고, dashboard 폴더를 삭제한다.
apps/dashboard/api/src/environments/config.json 의 HTTP 포트는 8001 이다.
테스트
먼저 콘솔에서 gateway, dashboard web을 구동한다.
$> nx serve gateway-web
NX Web Development Server is listening at http://localhost:9000/
$> nx serve dashboard-web
> NX Web Development Server is listening at http://localhost:9001/
VSCode에서 gateway, dashboard api를 구동한다.
브라우져에서 http://localhost:9000 을 호출하고, 로그인 해서 dashboard web 의 index.html 이 호출되는지 체크한다.
에러처리는 libs/shared/src/lib/filter/global-exception.filter.ts 의 에러 포멧을 따른다.
import { Request, Response } from 'express';
import { ArgumentsHost, Catch, ExceptionFilter, HttpException, HttpStatus, Logger } from '@nestjs/common';
@Catch()
export class GlobalExceptionFilter implements ExceptionFilter {
catch(exception: any, host: ArgumentsHost) {
const ctx = host.switchToHttp();
const response = ctx.getResponse<Response>();
const request = ctx.getRequest<Request>();
const message = (exception as any).message;
Logger.error(message, (exception as any).stack, `${request.method} ${request.url}`);
const name = exception?.constructor?.name || 'HttpException';
let status = HttpStatus.INTERNAL_SERVER_ERROR;
switch (name) {
case 'HttpException':
status = (exception as HttpException).getStatus();
break;
case 'UnauthorizedException':
status = HttpStatus.UNAUTHORIZED;
break;
case 'ForbiddenException':
status = HttpStatus.FORBIDDEN;
break;
case 'QueryFailedError': // this is a TypeOrm error
status = HttpStatus.UNPROCESSABLE_ENTITY;
break;
case 'EntityNotFoundError': // this is another TypeOrm error
status = HttpStatus.UNPROCESSABLE_ENTITY;
break;
case 'CannotCreateEntityIdMapError': // and another
status = HttpStatus.UNPROCESSABLE_ENTITY;
break;
default:
status = HttpStatus.INTERNAL_SERVER_ERROR;
}
// 에러 리턴 포멧
response.status(status).json(
{
statusCode: status,
error: name,
message,
method: request.method,
path: request.url,
timestamp: new Date().toISOString()
}
);
}
}
테스트 진행시 UI가 Nest쪽 패키지를 사용하면 번들링 오류가 발생할 수 있다. 따라서 libs 하위의 패키지들은 향후 API용, WEB용 구분하여 사용하고, model 패키지만 공용으로 사용한다. API용, WEB용을 구분한다면 하기와 같이 별도 폴더로 묶어 관리하는게 좋아 보인다.
api, web, model 분리
Nx 기반 library 생성 명령은 다음과 같다.
// api library
$> nx g @nrwl/nest:lib api/shared --publishable --importPath=@rnm/api-shared
$> nx g @nrwl/nest:lib api/domain --publishable --importPath=@rnm/api-domain
// web library
$> nx g @nrwl/react:lib web/shared --publishable --importPath=@gv/web-shared
$> nx g @nrwl/react:lib web/domain --publishable --importPath=@gv/web-domain
$> nx g @nrwl/react:lib web/ui --publishable --importPath=@gv/web-ui
// model library
$> nx g @nrwl/nest:lib model --publishable --importPath=@gv/model
Cookie의 REFRESH_TOKEN이 서버에 저장된 값과 맞으면 해당 user정보를 반환하는 코드를 libs/domain/src/lib/entities/user/user.service.ts 에 추가한다.
// user.service.ts 일부
async getUserIfRefreshTokenMatches(refreshToken: string, id: number): Promise<User | undefined> {
const user = await this.findOneById(id);
const isRefreshTokenMatching = await bcryptCompare(
refreshToken,
user.currentHashedRefreshToken as string
);
if (isRefreshTokenMatching) {
return user;
}
return;
}
JWT Refresh Strategy와 Guard 추가
Guard에서 사용할 Refresh Strategy를 libs/domain/src/lib/auth/strategies/jwt-refresh.strategy.ts 파일 생성후 추가한다.
import { Request } from 'express';
import { ExtractJwt, Strategy } from 'passport-jwt';
import { PassportStrategy } from '@nestjs/passport';
import { Injectable } from '@nestjs/common';
import { UserService } from '@rnm/domain';
import { loadConfigJson } from '@rnm/shared';
import { TokenPayload, User } from '@rnm/model';
const config: any = loadConfigJson();
@Injectable()
export class JwtRefreshTokenStrategy extends PassportStrategy(Strategy, 'jwt-refresh-token') {
constructor(
private readonly userService: UserService,
) {
super({
jwtFromRequest: ExtractJwt.fromExtractors([(request: Request) => {
return request?.cookies?.REFRESH_LOGIN_TOKEN;
}]),
secretOrKey: config?.AUTH?.REFRESH_SECRET,
passReqToCallback: true,
});
}
async validate(request: Request, payload: TokenPayload): Promise<User | undefined> {
const refreshToken = request.cookies?.REFRESH_LOGIN_TOKEN;
return this.userService.getUserIfRefreshTokenMatches(refreshToken, payload.id as number);
}
}
Refresh Guard도 libs/domain/src/lib/auth/guards/jwt-auth-refresh.guard.ts 파일 생성하고 추가한다.
import { Injectable } from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';
@Injectable()
export class JwtRefreshGuard extends AuthGuard('jwt-refresh-token') { }
파일 추가후에는 항시 libs/domain/src/index.ts 안에 export를 해야 한다.
export * from './lib/constants/core.contant';
export * from './lib/entities/user/user.entity';
export * from './lib/entities/user/user.service';
export * from './lib/entities/entity.module';
export * from './lib/models/request.model';
export * from './lib/auth/auth.service';
export * from './lib/auth/auth.middleware';
export * from './lib/auth/auth.module';
export * from './lib/auth/guards/local-auth.guard';
export * from './lib/auth/guards/jwt-auth.guard';
export * from './lib/auth/guards/jwt-auth-refresh.guard'; // <== 요기
export * from './lib/auth/strategies/local.strategy';
export * from './lib/auth/strategies/jwt.strategy';
export * from './lib/auth/strategies/jwt-refresh.strategy'; // <== 요기
export * from './lib/service/gateway/api/service/gateway-api-app.service';
export * from './lib/service/dashboard/api/service/dashboard-api-app.service';
export * from './lib/configuration/api/service/configuration-api-app.service';
export * from './lib/service/back-office/api/service/backoffice-api-app.service';
RefreshToken과 AuthToken을 Cookie에 실어 보내기
두가지 Token을 response cookie에 실어 보내기위해 먼저 cookie 생성하는 코드를 libs/domain/src/lib/auth/auth.service.ts 에 추가한다.
import { Injectable } from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';
@Injectable()
export class LocalAuthGuard extends AuthGuard('local') { }
User 생성하는 apps/gateway/api/src/app/user/user.controller.ts 에도 @UseGuards 를 JWT 토큰 체크하는 Guard로 등록한다. 이를 위하여 libs/domain/src/lib/auth/guards/jwt-auth.guard.ts 파일을 생성한다.
import { ExecutionContext, Injectable, UnauthorizedException } from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';
@Injectable()
export class JwtAuthGuard extends AuthGuard('jwt') {
canActivate(context: ExecutionContext) {
// Add your custom authentication logic here
// for example, call super.logIn(request) to establish a session.
return super.canActivate(context);
}
handleRequest(err: any, user: any, info: any, context: any, status?: any) {
// You can throw an exception based on either "info" or "err" arguments
if (err || !user) {
throw err || new UnauthorizedException();
}
return user;
}
}
그리고 apps/gateway/api/src/app/user/user.controller.ts 에 @UseGuards를 "JwtAuthGuard"로 등록한다.
$> npx prisma init
✔ Your Prisma schema was created at prisma/schema.prisma
You can now open it in your favorite editor.
Next steps:
1. Set the DATABASE_URL in the .env file to point to your existing database. If your database has no tables yet, read https://pris.ly/d/getting-started
2. Set the provider of the datasource block in schema.prisma to match your database: postgresql, mysql, sqlite, sqlserver (Preview) or mongodb (Preview).
3. Run prisma db pull to turn your database schema into a Prisma schema.
4. Run prisma generate to generate the Prisma Client. You can then start querying your database.
More information in our documentation:
https://pris.ly/d/getting-started
자동 생성된 .env 파일에 설정을 추가한다.
# Environment variables declared in this file are automatically made available to Prisma.
# See the documentation for more detail: https://pris.ly/d/prisma-schema#using-environment-variables
# Prisma supports the native connection string format for PostgreSQL, MySQL, SQLite, SQL Server (Preview) and MongoDB (Preview).
# See the documentation for all the connection string options: https://pris.ly/d/connection-strings
#DATABASE_URL="postgresql://johndoe:randompassword@localhost:5432/mydb?schema=public"
# POSTGRES
POSTGRES_USER=iot
POSTGRES_PASSWORD=1
POSTGRES_DB=rnm-stack
# Nest run locally
DB_HOST=localhost
# Nest run in docker, change host to database container name
# DB_HOST=postgres
DB_PORT=5432
DB_SCHEMA=public
# Prisma database connection
DATABASE_URL=postgresql://${POSTGRES_USER}:${POSTGRES_PASSWORD}@${DB_HOST}:${DB_PORT}/${POSTGRES_DB}?schema=${DB_SCHEMA}&sslmode=prefer
VS Code의 extension을 설치한다.
VSCode의 prisma extension
Step-2) schema.prisma 설정
VSCode extension이 설치되면 schema.prisma의 내용이 다음과 같이 highlighting된다.
schema.prisma 파일 안에 Prisma 방식의 스키마를 정의한다.
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
}
generator client {
provider = "prisma-client-js"
// previewFeatures = []
}
// generator dbml {
// provider = "prisma-dbml-generator"
// }
model User {
id Int @id @default(autoincrement())
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
email String @unique
password String
firstname String?
lastname String?
posts Post[]
role Role
}
model Post {
id Int @id @default(autoincrement())
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
published Boolean
title String
content String?
author User? @relation(fields: [authorId], references: [id])
authorId Int?
}
enum Role {
ADMIN
MANAGER
USER
}
schema.prisma를 통해 Migrate SQL과 Prisma Client 파일을 자동 생성한다. Prisma Client 파일은 구현 코드에서 사용된다. (참조)
Step-3) schema 설정을 통해 sql 생성하기
명령을 수행하면 prisma/migrations sql이 자동 실행된다. 생성된 sql을 통해 table schema를 업데이트한다. 변경점이 있으면 날짜별로 update 할 수 있는 table schema가 자동으로 생성된다.
$> npx prisma migrate dev --create-only --name=iot
$> npx prisma db push
Environment variables loaded from .env
Prisma schema loaded from prisma/schema.prisma
Datasource "db": PostgreSQL database "rnm-stack", schema "public" at "localhost:5432"
🚀 Your database is now in sync with your schema. Done in 77ms
✔ Generated Prisma Client (3.1.1) to ./node_modules/@prisma/client in 61ms
Post, User 테이블 자동 생성
Step-4) Prisma Studio 사용하기
prisma는 내장 웹기반 studio를 제공한다. 테이블을 선택하여 조작할 수 있다.
$> npx prisma studio
NestJS 에서 PrismaClient 사용하기
Step-1) PrismaClient 생성
schema에 생성되었으면 다음으로 코드에서 Prisma 접근을 위해 PrismaClient를 생성해야 한다. (참조)
"npx prisma migrate dev" 명령으로 수행할 경우는 "npx prisma generate"이 필요없다.
"npx prisma migrate dev --create-only" 일 경우만 수행한다.
$> npx prisma generate
✔ Generated Prisma Client (3.1.1) to ./node_modules/@prisma/client in 181ms
You can now start using Prisma Client in your code. Reference: https://pris.ly/d/client
```
import { PrismaClient } from '@prisma/client'
const prisma = new PrismaClient()
```
prisma client는 기본적으로 node_modules/.prisma/client 폴더 밑에 생성된다.
node_modules/.prisma/client 폴더
Step-2) schema.prisma가 변경이 발생할 경우
이제 "PrismaClient"를 import해서 사용할 수 있는 상태가 되었다. 만일 테이블 변경이 발생한다면 아래와 같이 수행한다.
// 코드: https://gist.github.com/ysyun/845ddd32467bb50af654e1e4605a4b50
// .zshrc
autoload -Uz add-zsh-hook
load-nvmrc() {
local _CUR_NODE_VER="$(nvm version)"
local _NVMRC_PATH="$(nvm_find_nvmrc)"
if [[ -n "${_NVMRC_PATH}" ]]; then
local _NVMRC_NODE_VER="$(nvm version "$(cat "${_NVMRC_PATH}")")"
if [[ "${_NVMRC_NODE_VER}" == 'N/A' ]]; then
local compcontext='yn:yes or no:(y n)'
vared -cp "Install the unmet version ($(cat "${_NVMRC_PATH}")) in nvm (y/n) ?" _ANSWER
if [[ "${_ANSWER}" =~ '^y(es)?$' ]] ; then
nvm install
fi
elif [[ "${_NVMRC_NODE_VER}" != "${_CUR_NODE_VER}" ]]; then
nvm use
fi
elif [[ "${_CUR_NODE_VER}" != "$(nvm version default)" ]]; then
echo -e "Reverting to the default version in nvm"
nvm use default
fi
}
add-zsh-hook chpwd load-nvmrc
load-nvmrc
// 작업 폴더에 .nvmrc 파일을 생성하고, 버전을 기입힌다. 아래 내용 참조
// .nvmrc
14.18.0
Step-2) NX 개발환경 생성
create-nx-workspace@latest를 이용하고, .nvmrc 파일을 생성한다. .zshrc가 적용되고, rnm-stack 폴더에 진입하면 자동으로 14.18.0 버전이 적용된다. rnm은 React Nest Micro-service의 약어이다.
$> npx create-nx-workspace@latest
✔ Workspace name (e.g., org name) · rnm-stack
✔ What to create in the new workspace · react
✔ Application name · gateway/web
✔ Default stylesheet format · scss
✔ Use Nx Cloud? (It's free and doesn't require registration.) · No
$> cd rnm-stack
$> echo "14.18.0" > .nvmrc
다음은 api 애플리케이션을 Nest기반으로 생성한다.
// nest 제너레이터 설치
$> yarn add -D @nrwl/nest@latest
// api 생성
$> nx g @nrwl/nest:app gateway/api
다음으로 루트폴더에 있는 workspace.json 파일을 각 api와 web폴더에 project.json 파일을 생성하여 설정을 옮긴다. 이후 생성되는 애플리케이션들은 자동으로 project.json 파일로 생성된다.
// workspace.json
{
"version": 2,
"projects": {
"gateway-api": "apps/gateway/api", <== project.json 파일의 위치 지정
"gateway-web": "apps/gateway/web",
"gateway/web-e2e": "apps/gateway/web-e2e"
},
... 생략 ...
"defaultProject": "gateway-web"
}
project.json 파일들
테스트 "nx serve gateway-web" 을 수행하면 정상적으로 dev server가 수행된다.
$> nx serve gateway-web
> nx run gateway-web:serve
> NX Web Development Server is listening at http://localhost:4200/
나머지 dashboard, configuration, back-office도 생성한다.
// react application
$> nx g @nrwl/react:app dashboard/web
$> nx g @nrwl/react:app configuration/web
$> nx g @nrwl/react:app back-office/web
// nest application
$> nx g @nrwl/nest:app dashboard/api
$> nx g @nrwl/nest:app configuration/api
$> nx g @nrwl/nest:app back-office/api
전체 생성 애플리케이션과 workspace.json 내역
Libraries 생성
다음으로 libs 폴더 하위로 라이브러리들을 생성한다. @rnm/page, @rnm/ui, @rnm/domain, @rnm/shared 로 import하여 사용한다.
page
ui
domain
shared
$> nx g @nrwl/react:lib page --importPath=@rnm/page --publishable
$> nx g @nrwl/react:lib ui --importPath=@rnm/ui --publishable
$> nx g @nrwl/nest:lib domain --importPath=@rnm/domain --publishable
$> nx g @nrwl/nest:lib shared --importPath=@rnm/shared --publishable
libs 폴더밑으로 4개 라이브러리 생성
Micro Service Communication Channel 설정
다음은 애플리케이션간의 연결을 위한 통신부분을 설정한다. NestJS 기반으로 microservice 생성을 위해 패키지를 설치한다. NestJS버전을 7.6.18 버전으로 업데이트 한다. (현재 8.*은 microservice 버그가 있다. 작동불능)
$> yarn add @nestjs/microservices
// package.json 에서 nestjs의 모든 패키지의 버전을 업데이트 한다.
"dependencies": {
"@nestjs/common": "^7.6.18", <== 요기
"@nestjs/core": "^7.6.18", <== 요기
"@nestjs/microservices": "^7.6.18", <== 요기
"@nestjs/platform-express": "^7.6.18", <== 요기
"@nestjs/serve-static": "^2.2.2",
"core-js": "^3.6.5",
"react": "17.0.2",
"react-dom": "17.0.2",
"react-router-dom": "5.2.0",
"reflect-metadata": "^0.1.13",
"regenerator-runtime": "0.13.7",
"rxjs": "~6.6.3",
"tslib": "^2.0.0"
},
"devDependencies": {
"@nestjs/schematics": "^7.3.1", <== 요기, 7최신이 7.3.1
"@nestjs/testing": "^7.6.18", <== 요기
...
}
// nestjs 모든 패키지의 업데이트 버전을 재설치한다.
$> yarn
Step-1) Micro Service 설정
dashboard, configuration, back-office 3가지 micro service
http server & static server
dashboard: 8001, configuration: 8002, back-office: 8002 HTTP port를 사용한다.
api server
dashboard: 8100, configuration: 8200, back-office: 8300 TCP port를 사용한다.
apps/dashboard/api/src 하위로 public 폴더를 하위로 dashboard 폴더를 추가로 생성한 후 index.html 파일을 생성한다.
apps/dashboard/api/src/environments폴더에 config.json 파일을 생성한다.
apps/dashboard/api/project.json 에 "assets" 파트에 설정을 추가한다. "assets": ["apps/dashboard/api/src/environments", "apps/dashboard/api/src/public"]
libs/shared/src/lib 밑에 configuration 폴더를 생성하고, config.service.ts 와 config.model.ts을 생성한다. config.json 파일을 읽기위한 용도이다. index.ts에 export도 설정한다.
apps/dashboard/api/src/main.ts 파일을 수정한다. http server를 위한 listen port를 설정하고, microservice client가 연결토록 TCP로 연결 준비를 한다. loadMicroServiceCofiguration 함수를 "@rnm/shared" 패키지로 부터 위치 투명하게 (마치 node_modules에 설치된 것처럼) import 할 수 있다.
import { Logger } from '@nestjs/common';
import { NestFactory } from '@nestjs/core';
import { Transport, MicroserviceOptions } from '@nestjs/microservices';
import { loadMicroServiceConfiguration } from '@rnm/shared';
import { AppModule } from './app/app.module';
async function bootstrap() {
const app = await NestFactory.create(AppModule);
// Load config.json file
const config = loadMicroServiceConfiguration();
// // Setup tcp server for api
const options: MicroserviceOptions = {
transport: Transport.TCP,
options: { host: config.TCP_HOST, port: config.TCP_PORT || 8100 }
};
app.connectMicroservice(options);
app.startAllMicroservices();
// // Setup http server for web
const httpPort = config.HTTP_PORT || process.env.HTTP_PORT || 8001;
const globalPrefix = config.GLOBAL_API_PREFIX || '/api';
app.setGlobalPrefix(globalPrefix);
await app.listen(httpPort, () => {
Logger.log(`Listening at http://localhost:${httpPort}${globalPrefix}`);
});
}
bootstrap();
확인 & 테스트
// LISTEN 포트 확인
$> netstat -an |grep 8100
tcp4 0 0 *.8100 *.* LISTEN
$> netstat -an |grep 8001
tcp46 0 0 *.8001 *.* LISTEN
// gateway 실행
$> nx serve dashboard-api
// other console
$> curl -X GET "http://localhost:8001/api"
{"message":"Welcome to dashboard/api in libs"}
configuration, back-office도 동일하게 적용한다.
Step-2) Micro Service의 HTTP port가 static file 서비스토록 설정
apps/dashboard/api/src/app/app.module.ts 에 static 환경을 설정한다. rootPath는 public 폴더를 만든다. exclude는 global prefix인 api를 제외한다. public 폴더에 index.html 을 만들어 본다. 향후 react기반 SPA 번들파일을 위치할 것이다.
import { Module } from '@nestjs/common';
import { ServeStaticModule } from '@nestjs/serve-static';
import { join } from 'path';
import { DashboardApiAppService } from '@rnm/domain';
import { AppController } from './app.controller';
@Module({
imports: [
ServeStaticModule.forRoot({
rootPath: join(__dirname, 'public'),
exclude: ['/api*', '/dashboard/api*'],
}),
],
controllers: [AppController],
providers: [DashboardApiAppService],
})
export class AppModule { }
// index.html
dashboard home
재실행 후, dashboard context path로 브라우져에서 확인한다. public/dashboard/assets/images 밑으로 샘플 이미지 복사
Step-3) Micro Service의 응답을 위한 TCP MessagePattern 데코레이터 생성
apps/dashboard/api/src/app/app.controller.ts 에서 TCP 요청에 대한 응답 코드를 작성한다. TCP는 MessagePattern 데코레이터를 통해 응답하고 반환은 Primitive type, Promise, Observable 다 가능하다. 만일 Observable로 하면 gateway 측에서 Observable로 대응할 수 있다.
{ cmd: 'dashboard-sum' } 은 gateway로부터 오는 Key이다.
import { Controller, Get } from '@nestjs/common';
import { MessagePattern } from '@nestjs/microservices';
import { DashboardApiAppService } from '@rnm/domain';
import { from, Observable } from 'rxjs';
@Controller()
export class AppController {
constructor(private readonly appService: DashboardApiAppService) { }
@Get()
getData() {
return this.appService.getData();
}
@MessagePattern({ cmd: 'dashboard-sum' })
accumulate(data: number[]): Observable<number> {
console.log('calling sum from dashboard....');
const sum = data[0] + data[1] + data[2];
return from([sum]);
}
}
테스트는 Gateway까지 설정후 진행한다.
Step-4) Gateway 설정
gateway
http server & static server
reverse proxy
api client
config.json 파일을 apps/gateway/api/src/environment 폴더밑으로 생성하고 하기와 같이 설정한다.
apps/gateway/api/src/app/app.module.ts에서는 static file 서비스 폴더를 지정한다. reverse proxy를 위해 아래 3개의 exclude를 추가한다. web context처럼 사용될 것이다.
dashboard
configuration
back-office
import { Module } from '@nestjs/common';
import { ServeStaticModule } from '@nestjs/serve-static';
import { join } from 'path';
import { GatewayApiAppService } from '@rnm/domain';
import { AppController } from './app.controller';
import { DashboardModule } from './dashboard/dashboard.module';
@Module({
imports: [
ServeStaticModule.forRoot({
rootPath: join(__dirname, 'public'),
exclude: [
'/api/gateway*', '/api/dashboard*',
'/dashboard*', '/configuration*', '/back-office*'
],
}),
DashboardModule
],
controllers: [AppController],
providers: [GatewayApiAppService]
})
export class AppModule { }
gateway, back-office, configuration, dashboard등에서 사용하는 api service 를 domain library로 옮기고, 명칭을 수정하여 import해서 사용한다. 예로 GatewayApiAppService, DashboardApiAppService와 같다. api service중에 라이브러리성으로 옮겨도 될 만한 것들을 domain 라이브러리 밑으로 작성한다. 예로 여러 곳에서 호출되는 공통 api의 경우 또는 업무적인 묶음단위로 api를 관리하고 싶을 경우이다. 해당 api를 라이브러리로 빼는 이유는 향후 다른 UI에서 사용하거나 또는 UI가 바뀌어도 업무적인 api에 변경이 없을 경우를 가정한 것이다.
micro service 인 dashboard, configuration, back-office는 자신의 http web server로써 static file을 서비스하고, tcp를 통해 api를 전송한다. 개발시에는 gateway를 구동하지 않고, micro service만 실행한다면 http를 통해 api 서비스 추가하면 될 것이다.
Gateway에서 reverse proxy되는 3개 context
http-proxy-middleware를 설치한다.
$> yarn add http-proxy-middleware
다음으로 apps/gateway/api/src/app 폴더 밑으로 dashboard 폴더를 생성하고, dashboard용 controller, service, module과 dashboard.proxy.ts 파일들을 생성한다.
NestMiddleware를 상속받아 use를 구현한다.
// dashboard.proxy.ts ==> dashboard-proxy.middleware.ts 로 추후 명칭 변경됨
/* eslint-disable @typescript-eslint/no-explicit-any */
import { NestMiddleware, Logger } from '@nestjs/common';
import { loadGatewayConfiguration } from '@rnm/shared';
import { createProxyMiddleware } from 'http-proxy-middleware';
export class DashboardReverseProxyMiddleware implements NestMiddleware {
private config = loadGatewayConfiguration();
private proxyOptions = {
target: this.config.DASHBOARD.REVERSE_ADDRESS,
secure: false,
onProxyReq: (proxyReq: any, req: any, res: any) => {
Logger.debug(`[DashboardReverseProxyMiddleware]: Proxying ${req.method} request originally made to '${req.url}'`);
},
};
private proxy: any = createProxyMiddleware(this.proxyOptions);
use(req: any, res: any, next: () => void) {
this.proxy(req, res, next);
}
}
dashboard.module.ts에 다음과 같이 dashboard에 TCP로 접근하는 Client설정을 한다. 그리고 ProxyMiddleware를 설정한다.
import { MiddlewareConsumer, Module, NestModule, RequestMethod } from '@nestjs/common';
import { ClientsModule, Transport } from '@nestjs/microservices';
import { loadGatewayConfiguration } from '@rnm/shared';
import { DashboardController } from './dashboard.controller';
import { DashboardReverseProxyMiddleware } from './dashboard.proxy';
import { DashboardService } from './dashboard.service';
const config = loadGatewayConfiguration();
@Module({
imports: [
// dashboard쪽 api 호출을 위한 TCP Client Proxy 등록
ClientsModule.register([
{
name: "DASHBOARD",
transport: Transport.TCP,
options: {
host: config.DASHBOARD.TCP_HOST,
port: config.DASHBOARD.TCP_PORT
}
}
])
],
controllers: [DashboardController],
providers: [DashboardService],
})
export class DashboardModule implements NestModule {
// middleware 적용
configure(consumer: MiddlewareConsumer) {
consumer
.apply(DashboardReverseProxyMiddleware)
.forRoutes({ path: config.DASHBOARD.REVERSE_CONTEXT, method: RequestMethod.ALL });
}
}
전체 코드 준비는 되었고, 다음 글에서는 MS Code에서 Debugging을 통해 위의 코드를 테스트해 본다.
domain: 업무 api 호출(react-query or graphql query) 및 결과 data model, custom Hook 기반
page에서 view와 domain을 조합한다.
SSR에서 Next.JS 기반으로 진행시 feature의 domain을 통해 데이터를 다룬다.
멀티 애플리케이션에서의 단위 업무화면 구성방식
NextJS 개념
최신것이 항상 좋은 것이 아니다. SPA 프레임워크의 CSR(Client Side Rendering)만으로 개발하다가 예전의 JSP, ASP같은 SSR(Server Side Rendering)의 이점이 있었다. NextJS는 CSR, SSR 뿐만아니라 SSG(Static Site Generation)도 지원을 한다.
CSR
SSR
SSG
SSG에 대한 개념
SSG를 위한 getStaticProps, getStaticPaths
getStaticProps와 getStaticPaths는 async이다.
getStaticProps, getStaticPaths에서 데이터를 가져오는 domain에 위치한 api를 호출한다.
AntD의 스타일을 apps/tube-csr/src/styles.scss에 import 하고, app.tsx에 AntD의 Button 컴포넌트를 테스트 해본다. styles.scss는 글로벌 스타일로 apps/tube-csr/project.json 환경파일에 설정되어 있다.
다음으로 apps/tube-csr/src/app/dog.tsx에 테스트 코드를 입력한다. httpService를 import할 때 from 구문이 Local file system에 있다하더라도 마치 node_modules에서 import하는 것처럼 위치투명성을 보장한다. 이는 루트에 위치한 tsconfig.base.json 파일에 lib 생성할 때 자동 등록된다.
소스 폴더로 이동을 하면 지정한 node version으로 nvm을 통해 switching 하고 싶다면 폴더에 .nvmrc 파일을 생성하고 "14.18.0" 버전을 명시하고, 로그인 사용자 .zshrc 또는 .bashrc 에 하기 사항을 설정한다. Gist 소스 참조
#! /usr/bin/env zsh
# Ref: https://github.com/creationix/nvm#calling-nvm-use-automatically-in-a-directory-with-a-nvmrc-file
# place this after nvm initialization!
autoload -Uz add-zsh-hook
# Function: load-nvmrc
load-nvmrc() {
local _CUR_NODE_VER="$(nvm version)"
local _NVMRC_PATH="$(nvm_find_nvmrc)"
if [[ -n "${_NVMRC_PATH}" ]]; then
local _NVMRC_NODE_VER="$(nvm version "$(cat "${_NVMRC_PATH}")")"
if [[ "${_NVMRC_NODE_VER}" == 'N/A' ]]; then
local compcontext='yn:yes or no:(y n)'
vared -cp "Install the unmet version ($(cat "${_NVMRC_PATH}")) in nvm (y/n) ?" _ANSWER
if [[ "${_ANSWER}" =~ '^y(es)?$' ]] ; then
nvm install
fi
elif [[ "${_NVMRC_NODE_VER}" != "${_CUR_NODE_VER}" ]]; then
nvm use
fi
elif [[ "${_CUR_NODE_VER}" != "$(nvm version default)" ]]; then
echo -e "Reverting to the default version in nvm"
nvm use default
fi
}
add-zsh-hook chpwd load-nvmrc
load-nvmrc
NX 개발환경 구성을 위한 글로벌 패키지를 설치한다. Typescript는 v4.3.5 이상을 사용한다.
$> npm i -g @angular/cli@latest
$> npm i -g @nrwl/cli@latest
$> npm i -g yarn@latest
NX 개발환경 생성하기
npx 명령으로 개발환경 생성. RNN Stack에서 RNN은 React NestJS NextJS 을 합친 것이다. React Application의 명칭은 "tube-csr" 이다.
반드시 latest 버전으로 설치한다.
$> npx create-nx-workspace@latest
선택하기
✔ Workspace name (e.g., org name) · rnn-stack
✔ What to create in the new workspace · react
✔ Application name · tube-csr
✔ Default stylesheet format · scss
✔ Use Nx Cloud? (It's free and doesn't require registration.) · No
$> cd rnn-stack
"tube-csr" 애플리케이션을 위한 Node서버로 NestJS 플로그인를 설치하고, "tube-api" 이름으로 서버를 생성한다.
Nx의 NestJS 플러그인 설치
$> yarn add -D @nrwl/nest@latest
생성
$> nx generate @nrwl/nest:app tube-api
다음으로 NX의 NextJS 플러그인을 설치한다. NextJS Application은 "tube-ssr" 이다.
설치
$> yarn add -D @nrwl/next@latest
생성
$> nx generate @nrwl/next:app tube-ssr
또는
$> nx g @nrwl/next:app tube-ssr
✔ Which stylesheet format would you like to use? · scss
NextJS 애플리케이션을 설치하면 sass-node버전을 v5.0.0이 설치된다. v4.14.1로 변경 사용한다. 5.0으로 사용시 컴파일 오류 발생하여 향후 패치되면 버전 업그레이드 함.
tube-ssr 애플리케이션의 개발 서버 포트는 4300 으로 변경한다. workspace.json의 "tube-ssr"의 serve options설정에 포트 정보를 수정한다.
Nx 환경파일 재구성
애플리케이션과 라이브러리를 많이 만들다 보면 rnn-stack/workspace.json 파일안에 설정이 계속 추가된다. 가독성을 위하여 애플리케이션(라이브러리 포함) Nx의 환경설정을 프로젝트별 별도 파일로 분리한다. 분리후에는 애플리케이션이나 라이브러리 생성시 자동으로 "project.json" 파일로 분리가 된다.
각 애플리케이션 root 폴더에 "project.json" 파일을 생성하고 workspace.json의 프로젝트별 설정 정보를 옮긴다. workspace.json에는 애플리케이션의 위치정보로 수정하면 컨벤션에 의해 project.json을 인지한다.
NX + Angular 기반 개발시 엔터프라이즈 애플리케이션 개발방법에 대한 글을 정리한다. Micro Frontend 개발방법이라기 보다는 애플리케이션을 개발하며 지속적으로 확장할 필요가 있을 때 관심사들을 어떻게 분리하여(Bounded Context) 개발할 수 있을지 보여준다.
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 개발 잇점을 갖는다.
// 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
설정 수정후 실행을 하면 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의 명령어를 자동 수정함.
테스트를 위해 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 파일을 합친다.
buildSingle.sh 내용을 수정한다. scripts.js 파일은 window.ng.core 또는 window.ng.common과 같은 global 객체가 담겨있는 파일이다. 따라서 scripts.js는 app-container 애플리케이션에서 최초 한번만 로딩하면 되고, 이후 monitor 애플리케이션과 같은 web components는 번들 파일은 자신의 내용만을 포함한다.
마이크로 프론트앤드는 마이크로 서비스처럼 전체 화면을 작동할 수 있는 단위로 나누어 개발한 후 서로 조립하는 방식이다. 여기서 작동 단위에 사용된 프론트앤드 프레임워크로 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)