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

Publication

Category

Recent Post

2023. 5. 12. 09:59 React/Architecture

Webpack v5.* 기반의 Module Federation 개념을 간단히 정리하고, 환경을 설정해 본다. 

 

Module Federation Concept

Micro Frontend를 위한 컨셉에서 출발

- 참조: https://mobicon.tistory.com/572

개별 빌드 배포

 

Host 모듈 & Remote 모듈

- 모듈: webpack 번들링으로 생성된 js, css, html 파일 묶음

- Host 모듈: 단일 webpack 모듈 -> 개별 번들링된다. 

- Remote 모듈: 단일 webpack 모듈 -> 개별 번들링된다. 

   + 빌드시 호스트/원격 모듈 따로 따로 빌드 관리된다. 원격 모듈은 다른 도메인에서 제공할 수도 있다.

- 컨테이너: 각각 따로 빌드되며 독립적인 애플리케이션이다. 

    + A, B 컨테이너가 존재하면 각자 상호 로딩가능한다. 

- Expose: 컨테이너가 로딩할 원격 모듈 설정

    + 자세한 예: 참조

    + expose 되는 것은 별도의 chunk file 이 생성된다. (즉 해당 chunk file만 로딩해서 사용함)

// Remote App의 webpack.config.js 에서 exposes하기 (App2)
  plugins: [
    // To learn more about the usage of this plugin, please visit https://webpack.js.org/plugins/module-federation-plugin/
    new ModuleFederationPlugin({
      name: 'app2',
      filename: 'remoteEntry.js',
      exposes: {
        './App': './src/App',
      },
      shared: { react: { singleton: true }, 'react-dom': { singleton: true } },
    }),
    new HtmlWebpackPlugin({
      template: './public/index.html',
    }),
  ],
  
// Host App의 webpack.config.js에서 remote app 로딩하기 (App1)
  plugins: [
    new ModuleFederationPlugin({
      name: "app1",
      remotes: {
        app2: "app2@[app2Url]/remoteEntry.js",
      },
      shared: {react: {singleton: true}, "react-dom": {singleton: true}},
    }),
    new ExternalTemplateRemotesPlugin(),
    new HtmlWebpackPlugin({
      template: "./public/index.html",
    }),
  ],
  
// App1에서 index.js 에서 app2Url 설정으로 remote app url 설정 
// You can write your own logic here to determine the actual url
window.app2Url = "http://localhost:3002"
  
// Host App에서 app2의 App 비동기 로딩 (App1)
import React, {Suspense} from "react";
const RemoteApp = React.lazy(() => import("app2/App"));

const App = () => {
  return (
    <div>
      <div style={{
        margin:"10px",
        padding:"10px",
        textAlign:"center",
        backgroundColor:"greenyellow"
      }}>
        <h1>App1</h1>
      </div>
      <Suspense fallback={"loading..."}>
        <RemoteApp/>
      </Suspense>
    </div>)
}


export default App;

App1이 호스트 앱, App2가 리모트 앱

 

- 공유 모듈: 여러 컨테이너에서 같이 사용하는 모듈

    + 예: react, react-dom     

- 호스트 앱: 원격 모듈을 사용하는 컨테이너

    + 리모트 앱이 expose한 원격 모듈을 호스트 앱에서 비동기 로딩해서 사용한다. 

- 리모트 앱: 모듈을 expose하는 컨테이너

 

 

NX 기반 환경설정

NX는 module federation 설정의 Host & Remote App을 생성하고 환경설정에 대해 이해한다. (참조)

 

Portal App & Micro Apps 역할

- Portal App

   + Remote 앱이 되어서 다양한 packages의 모듈을 expose 한다.

- Micro App

   + Host 앱이 되어서 portal의 exposed module을 async loading하여 사용한다.

 

Dashboard App에서 widget을 사용, Portal App 에서 Dashboard App을 사용한다

 

Portal App 모듈과 Micro App 모듈의 분리

- Micro App은 필요한 모듈을 Portal App (remote app) 으로 부터 로딩하여 사용한다. 따라서 Micro App에서 필요한 모듈을 package.json에 설정하여 npm install 하여 로컬에 설치 후 사용하는 것이 아니라, runtime에 로딩하여 사용할 수 있다. 

- Micro App 개발시 참조하는 모듈을 로컬에 설치할 필요없이 개발을 진행할 수 있다. 

- 즉, Micro Frontend의 개념을 적용하여 개발을 진행한다.

 

명령어 예

- host라는 host app이 자동 생성된다

- store 이라는 remote app이 자동 생성된다.

- @nx/react:host 의 명령어에 따라서 module federation 관련한 설정 내역이 자동으로 생성된다. 

nx g @nx/react:host mf/host --remotes=mf/store

mf폴더 밑으로 host, store app 생성

- host app 생성파일들

  + main.ts 에서 bootstrap.tsx를 import 형식: project.json에서 main도 main.ts 로 설정됨 (기존은 main.tsx 하나만 존재)

  + module-federation.config.js 파일 생성: remote 설정

  + webpack.config.<prod>.js 파일들 생성 

  + project.json: serve 의 executor가 @nx/react:module-federation-dev-server 로 변경됨

 

또는 remote app만들 별도로 생성할 수 있다. 

npx nx g @nx/react:remote portal/store

- remote app 생성파일들

  + remote-entry.ts 

  +  main.ts 에서 bootstrap.tsx를 import 형식: project.json에서 main도 main.ts 로 설정됨 (기존은 main.tsx 하나만 존재)

  + module-federation.config.js 파일 생성: exposes 설정

  + webpack.config.<prod>.js 파일들 생성 

  + project.json: serve 의 executor가 @nx/react:module-federation-dev-server 로 변경됨

 

host를 실행하면

  + 관련된 remote도 자동으로 실행된다. (remote는 project.json의 static-server의 port로 자동 실행된다.)

  + 즉, host app과 remote app이 동시에 구동된다. 

nx serve mf-host --open

 

NX 기반 설정파일 이해하기

remote app 설정 파일들

- webpack.config.js

  + withModuleFederation은 node_modules/@nx/react/src/module-federation/with-module-federation.js 위치하고 있고, remote와 shared할 libraries를 자동으로 설정해 준다. 즉, remote빌드시 shared libraries는 external libs로 취급되어 번들파일에 포함되지 않는다. 

  + nx 명령을 통해 생성한 remote app에는 webpack.config.js와 webpack.config.prod.js 파일이 자동 생성 및 설정되어 있다.

const { composePlugins, withNx } = require('@nx/webpack');
const { withReact } = require('@nx/react');
const { withModuleFederation } = require('@nx/react/module-federation');

const baseConfig = require('./module-federation.config');

const config = {
  ...baseConfig,
};

// Nx plugins for webpack to build config object from Nx options and context.
module.exports = composePlugins(withNx(), withReact(), withModuleFederation(config));

- module-federation.config.js 파일명은 변경하지 말고 그대로 사용해한다. 

  + shared쪽에 libraryName을 체크하여 singleton, strictVersion, requiredVersion을 설정할 수 있다. (host도 동일)

       > return undefined 이면 nx default 값 사용

       > return false 이면 shared library로 사용하지 않겠다는 의미이다. 

       > version을 명시하는 경우는 host와 remote간에 버전이 서로 틀릴 경우 사용한다. 

module.exports = {
  name: 'mf-store',
  exposes: {
    './Module': './src/remote-entry.ts',
  },
  shared: (libraryName, config) => {
    if (libraryName && libraryName.indexOf('@gv') >= 0) {
      config = { singleton: true, strictVersion: true, requiredVersion: '1.0.0' };
    }
    console.log('--- remote libraryName:', libraryName, config);
    return config;
  },
};

- project.json 

  + serve의 executor가 webpack-dev-server가 이니라, module-federation-dev-server 이다. 이는 host 기동시 remote도 자동 기동해 준다.

 "serve": {
      "executor": "@nx/react:module-federation-dev-server",
      "defaultConfiguration": "development",
      "options": {
        "buildTarget": "mf-store:build",
        "hmr": true,
        "proxyConfig": "apps/mf/store/proxy.conf.json",
        "port": 3001
      },
      "configurations": {
        "development": {
          "buildTarget": "mf-store:build:development"
        },
        "production": {
          "buildTarget": "mf-store:build:production",
          "hmr": false
        }
      }
    },

- remote-entry.ts 파일

  + host에 접근할 micro-frontend 애플리케이션

export { default } from './app/dashboard-app';

 

host app 설정 파일들

- webpack.config.js 개발시 내역은 remote app설정과 동일하다.

- webpack.config.prod.js 에는 remote app의 url path 가 설정된다.

// host의 webpack.config.prod.js 내역

const { composePlugins, withNx } = require('@nx/webpack');
const { withReact } = require('@nx/react');
const { withModuleFederation } = require('@nx/react/module-federation');

const baseConfig = require('./module-federation.config');

const prodConfig = {
  ...baseConfig,
  /*
   * Remote overrides for production.
   * Each entry is a pair of a unique name and the URL where it is deployed.
   *
   * e.g.
   * remotes: [
   *   ['app1', 'http://app1.example.com'],
   *   ['app2', 'http://app2.example.com'],
   * ]
   *
   * You can also use a full path to the remoteEntry.js file if desired.
   *
   * remotes: [
   *   ['app1', 'http://example.com/path/to/app1/remoteEntry.js'],
   *   ['app2', 'http://example.com/path/to/app2/remoteEntry.js'],
   * ]
   */
  remotes: [['mf-store', 'http://localhost:3001/']],
};

// Nx plugins for webpack to build config object from Nx options and context.
module.exports = composePlugins(withNx(), withReact(), withModuleFederation(prodConfig), (config) => {
  // Update the webpack config as needed here.
  // e.g. `config.plugins.push(new MyPlugin())`

  // .tsx 에서  import 구문 ordering 경고 문구 발생 해결하기
  // https://github.com/facebook/create-react-app/issues/5372
  const instanceOfMiniCssExtractPlugin = config.plugins.find(
    (plugin) => plugin.constructor.name === 'MiniCssExtractPlugin'
  );

  if (instanceOfMiniCssExtractPlugin) {
    instanceOfMiniCssExtractPlugin.options.ignoreOrder = true;
  }

  return config;
});

- module-federation.config.js host 자신 app의 명칭과 remote app 의 명칭을 설정한다. 

module.exports = {
  name: 'mf-host',
  remotes: ['mf-store'],
  shared: (libraryName, config) => {
    // ref: https://www.angulararchitects.io/en/aktuelles/the-microfrontend-revolution-part-2-module-federation-with-angular/
    if (libraryName && libraryName.indexOf('@gv') >= 0) {
      config = { singleton: true, strictVersion: true, requiredVersion: '1.0.0' };
    }
    console.log('--- host libraryName:', libraryName, config);
    return config;
  },
};

  + node_modules/@nx/react/src/module-federation/with-module-federation.js 소스 내역

      > NX에서 host의 sharedLibraries 에 대해 자동으로 목록을 만들어 준다. 

"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.withModuleFederation = void 0;
const tslib_1 = require("tslib");
const utils_1 = require("./utils");
const ModuleFederationPlugin = require("webpack/lib/container/ModuleFederationPlugin");
/**
 * @param {ModuleFederationConfig} options
 * @return {Promise<AsyncNxWebpackPlugin>}
 */
function withModuleFederation(options) {
    return tslib_1.__awaiter(this, void 0, void 0, function* () {
        const { sharedDependencies, sharedLibraries, mappedRemotes } = yield (0, utils_1.getModuleFederationConfig)(options);
        return (config, ctx) => {
            var _a;
            config.output.uniqueName = options.name;
            config.output.publicPath = 'auto';
            config.optimization = {
                runtimeChunk: false,
            };
            config.experiments = Object.assign(Object.assign({}, config.experiments), { outputModule: true });
            config.plugins.push(new ModuleFederationPlugin({
                name: options.name,
                library: (_a = options.library) !== null && _a !== void 0 ? _a : { type: 'module' },
                filename: 'remoteEntry.js',
                exposes: options.exposes,
                remotes: mappedRemotes,
                shared: Object.assign({}, sharedDependencies),
            }), sharedLibraries.getReplacementPlugin());
            return config;
        };
    });
}
exports.withModuleFederation = withModuleFederation;
//# sourceMappingURL=with-module-federation.js.map

 

- remotes.d.ts 는 ts에서 remote app의 모듈을 import 하기위한 definition 파일이다. 

// Declare your remote Modules here
// Example declare module 'about/Module';
declare module 'mf-store/Module';

- project.json 에 implicitDependencies 설정하면 mf-host와 mf-store는 하나의 애플리케이션으로 간주된다. (참조)

{
  "name": "mf-host",
  "$schema": "../../../node_modules/nx/schemas/project-schema.json",
  "sourceRoot": "apps/mf/host/src",
  "projectType": "application",
  "implicitDependencies": ["mf-store"],

 

 

<참조>

https://fe-developers.kakaoent.com/2022/220623-webpack-module-federation/

 

Webpack Module Federation 도입 전에 알아야 할 것들 | 카카오엔터테인먼트 FE 기술블로그

유동식(rich) 실용성 있는 프로그램을 추구합니다. 클래식 기타와 Nutrition 공부를 취미로 삼고 있습니다.

fe-developers.kakaoent.com

https://stackblitz.com/github/webpack/webpack.js.org/tree/main/examples/module-federation?file=README.md 

 

Webpack.js Module Federation Example - StackBlitz

Run official live example code for Webpack.js Module Federation, created by Webpack on StackBlitz

stackblitz.com

NX module federation: https://nx.dev/recipes/module-federation

 

Module Federation and Micro Frontends

How to work with and setup Module Federation with Nx.

nx.dev

연재글: https://www.angulararchitects.io/en/aktuelles/the-microfrontend-revolution-module-federation-in-webpack-5/

 

The Microfrontend Revolution: Module Federation in Webpack 5 - ANGULARarchitects

Module Federation allows loading separately compiled program parts. This makes it an official solution for implementing microfrontends.

www.angulararchitects.io

 

posted by 윤영식
2023. 5. 3. 17:34 React/Architecture

MS 시리즈를 시작한 것이 벌써 2년전이 되어서 nx 최신 버전으로 업데이트를 진행한다. 

 

NX 환경 버전업

nodejs는 최신 LST 버전인 18.16.0 을 사용한다.

$ nvm install 18.16.0
또는 설치되어 있다면
$ nvm use 18.16.0

create-nx-workspace 최신으로 frontend 폴더 밑으로 비어있는 apps, libs 환경을 생성한다. 

$ npx create-nx-workspace@latest frontend

// 옵션 선택
> integrated monorepos
> react
> portal/web (애플리케이션 위치)
> webpack (번들러)
> SCSS (스타일 프리컴파일러)
> No CI

옵션 선택 결과

생성결과 

apps/portal/web 이 생성되었다. 사용하지 않는 web-e2e를 정리한다.  별도의 애플리케이션을 생성하려면 workspace.json 파일을 루트 폴더에 생성한다. 

// workspace.json 내용
{
  "version": 2,
  "projects": {
    "asset-web": "apps/micro-apps/asset",
    "dashboard-web": "apps/micro-apps/dashboard",
    "management-web": "apps/micro-apps/management",
    "portal-web": "apps/portal/web",
    "system-web": "apps/micro-apps/system",
    "user-web": "apps/micro-apps/user"
  },
  "cli": {
    "defaultCollection": "@nrwl/react"
  },
  "generators": {
    "@nrwl/react": {
      "application": {
        "style": "scss",
        "linter": "eslint",
        "babel": true
      },
      "component": {
        "style": "scss"
      },
      "library": {
        "style": "scss",
        "linter": "eslint"
      }
    }
  },
  "defaultProject": "portal-web"
}

애플리케이션 생성 명령어

// 5개의 애플리케이션을 생성하고, SASS, webpack을 선택하여 생성한다. 

nx g @nrwl/react:app micro-apps/dashboard
nx g @nrwl/react:app micro-apps/asset
nx g @nrwl/react:app micro-apps/management
nx g @nrwl/react:app micro-apps/system
nx g @nrwl/react:app micro-apps/user

패키지를 생성한다. 

// SASS, jest, rollup 을 선택한다. 

nx g @nrwl/react:lib web/login/default --publishable --importPath=@gv/web-login-default

 

새로운 패키지와 애플리케이션이 생성된 폴더에 모든 soucre files 을 copy & paste 한다. 

 

버전업 이후 수정사항

React v17 -> v18 업데이트후 변경점. main.tsx 에서 root 생성 방법이 변경되었다.

// React v17
import * as ReactDOM from 'react-dom';
...
ReactDOM.render(
  <Suspense fallback={<GVSpinner isFull />}>
    <GVMicroApp />
  </Suspense>,
  document.getElementById('root')
);

// React v18
import * as ReactDOM from 'react-dom/client';
...
const root = ReactDOM.createRoot(document.getElementById('root') as HTMLElement);
root.render(
  <Suspense fallback={<GVSpinner isFull />}>
    <GVMicroApp />
  </Suspense>
);

AntD v4 -> v5 로 변경되면서 v5에서 cssinjs 방식을 사용하면서 *.less 방식이 사라졌다. 기본적인 reset.css만을 설정한다. 

// styles.scss 에 reset.css 포함
// AntD reset
@import "~antd/dist/reset.css";

// project.json에 styles.scss 포함 
      "options": {
        "compiler": "babel",
        ...
        "styles": ["apps/micro-apps/dashboard/src/styles.scss"],
        ...
        "webpackConfig": "apps/micro-apps/dashboard/webpack.config.js"
      },

Webpack의 min-css-extract-plugin을 사용하면서 build warning 나오는 import ordering 메세지 제거하기 

// webpack.config.js
module.exports = composePlugins(withNx(), withReact(), (config) => {
  // Update the webpack config as needed here.
  // e.g. `config.plugins.push(new MyPlugin())`

  // .tsx 에서  import 구문 ordering 경고 문구 발생 해결하기
  // https://github.com/facebook/create-react-app/issues/5372
  const instanceOfMiniCssExtractPlugin = config.plugins.find(
    (plugin) => plugin.constructor.name === 'MiniCssExtractPlugin'
  );

  if (instanceOfMiniCssExtractPlugin) {
    instanceOfMiniCssExtractPlugin.options.ignoreOrder = true;
  }

  return config;
});

 

 

Nx를 업데이트하면 기존의 workspace.json 파일을 사용하지 않는다. 그리고 webpack v5.* 버전을 사용한다. webpack v5는 Module Federation을 지원하므로 이에 대한 설정을 진행해 본다. 

 

<참조>

https://mobicon.tistory.com/586

 

[MS-2] React & Nest 기반 애플리케이션 및 Micro Service 통신 설정

React와 Nest를 기반으로 마이크로 서비스를 구축해 본다. 개발 환경은 Nx를 사용한다. Nx 환경구축은 [React HH-2] 글을 참조한다. 목차 Gateway, Dashboard등의 Application 생성 Application에서 사용하는 Library

mobicon.tistory.com

https://webpack.kr/concepts/module-federation/

 

Module Federation | 웹팩

웹팩은 모듈 번들러입니다. 주요 목적은 브라우저에서 사용할 수 있도록 JavaScript 파일을 번들로 묶는 것이지만, 리소스나 애셋을 변환하고 번들링 또는 패키징할 수도 있습니다.

webpack.kr

 

posted by 윤영식
2023. 1. 25. 14:57 Elixir/Basic

OS 별 Elixir 설치

https://elixir-lang.org/install.html

// MacOS
brew install elixir

brew로 설치하며 brew 에러가 발생하여 다음 두가지 명령을 통해 brew 업데이트가 필요하다. 시간이 10분가량 소요된다.

// To `brew update`, first run:
  git -C /usr/local/Homebrew/Library/Taps/homebrew/homebrew-core fetch --unshallow
  git -C /usr/local/Homebrew/Library/Taps/homebrew/homebrew-cask fetch --unshallow
  
// 완료시
  brew update
  
// 재 실행
  brew install elixir
// 설치 dependencies
==> Fetching dependencies for elixir: ca-certificates, openssl@1.1, m4, libtool, unixodbc, jpeg-turbo, libpng, lz4, xz, zstd, libtiff, pcre2, wxwidgets and erlang
==> Fetching ca-certificates

// 정상 설치 확인
  elixir -v
Erlang/OTP 25 [erts-13.1.3] [source] [64-bit] [smp:8:8] [ds:8:8:10] [async-threads:1] [jit:ns] [dtrace]
Elixir 1.14.3 (compiled with Erlang/OTP 25)

Erlang VM 위에서 Elixir가 수행되기에 Erlang/OTP (Open Telecom Platform)과 Elixir 버전이 같이 나오는 것 같다. 

 

 

IEX를 통한 연산자 실습

Interactive EliXir 를 통해 기본 실습을 한다. 

$ iex
Erlang/OTP 25 [erts-13.1.3] [source] [64-bit] [smp:8:8] [ds:8:8:10] [async-threads:1] [jit:ns] [dtrace]
Interactive Elixir (1.14.3) - press Ctrl+C to exit (type h() ENTER for help)

- Integer, Float, Boolean

- Atom(애텀): 문자가 값이 되는 상수, 앞에 :를 붙인다. Boolean도 atom이고, Module명도 atom이다.

iex> :test
:test
iex> :true
true
iex(11)> is_atom(MyApp.Module)
true
iex(12)> is_atom(MyApp)
true
iex(14)> is_atom(true)
true

- String: UTF-8

- 수치 연산: + - * / 지원, 

- 논리 연산: && || !  (and or not은 첫번째 인자가 boolean일 경우 사용 가능)

iex(15)> true and false
false
iex(16)> true or false
true

- 비교 연산: == != === <= >= < >  (정수 실수 비교는 ===)

iex(17)> 2 == 2.0
true
iex(18)> 2 === 2.0
false

타입에 따른 비교연산을 수행할 수도 있다. 

iex(19)> :hi > 999999
true

- Text interpolation: " #{value}"

iex(20)> name = "hi"
"hi"
iex(21)> "#{name} elixir"
"hi elixir"

- Text concatenation: <> 

iex(22)> hello = "hello"
"hello"
iex(23)> hello <> "elixir"
"helloelixir"

 

Collection 실습

- 종류: 리스트, 튜플, 키워드 리스트, 맵

- 리스트: value collection, | 로 연결시 0(n) 선형복잡도를 갖는다. 이런 이유로 리스트는 추가하는 것을 앞에 두는게 뒤보다 빠르다. linked list로 관리하고, 요소가 늘었다 줄었다 할 수 있고, 추가시에는 앞에 넣는게 속도면에서 낳다

// 여러 타입
iex(24)> [3, "hi", "dowon"]
[3, "hi", "dowon"]
iex(25)> list = [ 3, :hi, "peter"]
[3, :hi, "peter"]
// 합치기
iex(26)> ["test" | list]
["test", 3, :hi, "peter"]

- ++ : 좌 우 더하기

- -- : 오른쪽 모든 요소에 대해 왼쪽에서 처음 만난 요소만 지움, (it's safe to subtract a missin value)

iex(27)> [1, 2] ++ [ 3,4]
[1, 2, 3, 4]
iex(28)> [1,2]++[1,2]
[1, 2, 1, 2]
iex(29)> [1,2]--[1,2]
[]
iex(30)> [1,2,3]--[2,4]
[1, 3]
iex(31)> [1,2,3,4,5] -- [1,3,4]

- hd: head는 첫번째 요소 하나

- tl: tail은 head 첫번째 뺀 나머지

iex(32)> hd [1,2,3,4,"5"]
1
iex(33)> tl [1,2,3,4,"5"]
[2, 3, 4, "5"]
iex(34)> [h | t]=[1,2,3,4,5,"6"]
[1, 2, 3, 4, 5, "6"]
iex(35)> h
1
iex(36)> t
[2, 3, 4, 5, "6"]

- 튜플(Tuple): 메모리에 연속적으로 저장됨. 길이 구하는 것은 빠르나 수정은 비용이 비싸다. 즉, 추가하는 데이터가 아니라면 메모리 블락으로 움직이니 경우 사용하면 좋다. 함수의 추가정보 반환에 쓰임

iex(37)> {1,2,3, :hi, "peter"}
{1, 2, 3, :hi, "peter"}

- 키워드 리스트(Keyword list): atom을 key로 튜플의 리스트와 같다. 함수의 옵션 전달에 사용한다. (애텀은 :<name>형식이다), 애텀을 해쉬 테이블로 관리한다. 

  + 모든 키는 Atom이다

  + 키는 정렬되어 있다

  + 키는 유니크하지 않다

iex(38)> [say: "hi", name: "peter"]
[say: "hi", name: "peter"]
iex(39)> [{:say, "hi"}, {:name, "peter"}]
[say: "hi", name: "peter"]

 - 맵(Map): %{} 문법, keyword list와 틀리게 어떤 type의 key든 허용하고, 순서를 따르지 않는다.

   + %{atom => value} ---> %{key: value}

iex(40)> map = %{:say => "hi", :name => 4}
%{name: 4, say: "hi"}
iex(41)> map[:name]
4
iex(42)> map2 = %{"hi" => "hello"}
%{"hi" => "hello"}
iex(43)> map2["hi"]
"hello"
iex(44)> %{say: "hi", name: "peter"}
%{name: "peter", say: "hi"}
// 비교
iex(45)> %{hi: "yo"} === %{:hi => "yo"}
true
iex(46)> %{hi: "yo"} == %{:hi => "yo"}
true
iex(47)> map = %{hi: "yo"}
%{hi: "yo"}
iex(48)> map.hi
"yo"
// 같은 key는 뒤에 것으로 대체
iex(49)> %{hi: "yo", hi: "hello"}
warning: key :hi will be overridden in map
  iex:49

%{hi: "hello"}

 - | 를 통한 갱신, 새로운 맵을 생성하는 것이다. 이것은 추가가 아닌 기존 애텀 key가 매칭된 값을 업데이트 한다. 

iex(51)> map = %{ hi: "hello" }
%{hi: "hello"}
iex(52)> %{ map | name: "peter"}
** (KeyError) key :name not found in: %{hi: "hello"}
    (stdlib 4.2) :maps.update(:name, "peter", %{hi: "hello"})
    (stdlib 4.2) erl_eval.erl:309: anonymous fn/2 in :erl_eval.expr/6
    (stdlib 4.2) lists.erl:1350: :lists.foldl/3
    (stdlib 4.2) erl_eval.erl:306: :erl_eval.expr/6
    (elixir 1.14.3) src/elixir.erl:294: :elixir.eval_forms/4
    (elixir 1.14.3) lib/module/parallel_checker.ex:110: Module.ParallelChecker.verify/1
    (iex 1.14.3) lib/iex/evaluator.ex:329: IEx.Evaluator.eval_and_inspect/3
    (iex 1.14.3) lib/iex/evaluator.ex:303: IEx.Evaluator.eval_and_inspect_parsed/3
iex(52)> %{ map | hi: "yo"}
%{hi: "yo"}

 

 

Enum 실습

Enum (enumerable, 열거형) 모듈은 70 가량의 함수를 가지고 있고, 열거형에 작동한다. 튜플은 제외

- all?: Enum.all?(collection, function) 모든 요소가 true 이어야 true 이다.

- any?: Enum.any?(collection, function) 하나 요소라도 true 이면 true 이다.

iex(54)> Enum.all?(["hi", "peter"], fn(s) -> String.length(s) == 2 end)
false
iex(55)> Enum.all?(["hi", "peter"], fn(s) -> String.length(s) >= 2 end)
true
iex(56)> Enum.any?(["hi", "peter"], fn(s) -> String.length(s) == 2 end)
true

- chunk_every(collection, count): count 만큼씩 나눔

- chunk_by(collection, function): function 반환되는 결과값이 변할때 마다 나눔

- max_every(collection, count, function): count 만큼 묶고, 첫번째에 함수 반환값으로 대체한다.

iex(57)> Enum.chunk_every([1,2,3,4,5,6], 2)
[[1, 2], [3, 4], [5, 6]]
iex(58)> Enum.chunk_by(["hi", "yo", "yun", "do", "dowon"], fn(s) -> String.length(s) end)
[["hi", "yo"], ["yun"], ["do"], ["dowon"]]
iex(59)> Enum.map_every([1,2,3,4,5,6,7], 2, fn(s) -> s + 1000 end)
[1001, 2, 1003, 4, 1005, 6, 1007]

- each: 새로운 값을 만들지 않고 열거 하고 싶을 경우 사용한다

- map: 새로운 값을 생성하여 collection을 반환한다

- min, max: 최소, 최대값 반환 

- filter: true인 것만 반환

- reduce: 하나의 값으로 추려줌, function을 통해 선택적으로 추릴 수도 있음

- sort: 정렬 순서로 Erlang의 텀(Term)순서를 사용한다. function을 통한 정렬도 가능하다.

iex(60)> Enum.each([1,2,3,4], fn(s) -> IO.puts(s) end)
1
2
3
4
:ok
iex(61)> Enum.map([1,2,3,4], fn(s) -> s + 10 end)
[11, 12, 13, 14]
iex(62)> Enum.min([1,2,3,4])
1
iex(63)> Enum.max([1,2,3,4])
4
iex(64)> Enum.filter([1,2,3,4], fn(s) -> s/2 == 0 end)
[]
iex(65)> Enum.filter([1,2,3,4], fn(s) -> s/2 === 0 end)
[]
iex(66)> Enum.filter([1,2,3,4], fn(s) -> rem(s, 2) == 0 end)
[2, 4]
iex(67)> Enum.reduce([1,2,3,4], fn(s, acc) -> s+acc end)
10
iex(68)> Enum.sort([5,6,7,4,1])
[1, 4, 5, 6, 7]
iex(69)> Enum.sort([5,6,7,Enum, :foo, 4,"hi", 1])
[1, 4, 5, 6, 7, Enum, :foo, "hi"]
iex(70)> Enum.sort([5,6,7,Enum, :foo, 4,"hi", 1], :asc)
[1, 4, 5, 6, 7, Enum, :foo, "hi"]
iex(71)> Enum.sort([5,6,7,Enum, :foo, 4,"hi", 1], :desc)
["hi", :foo, Enum, 7, 6, 5, 4, 1]

- uniq: 중복 제거한 collection 반환 

- uniq_by: function을 통해 중복되는 것 제거한 collection 반환 

iex(72)> Enum.uniq([1,2,3,2,3,4])
[1, 2, 3, 4]
iex(74)> Enum.uniq_by([%{x: 1, y: 1}, %{x: 2, y: 1}, %{x: 3, y: 2}], fn(s) -> s.y end)
[%{x: 1, y: 1}, %{x: 3, y: 2}]

- Capture operator(&): 익명함수를 간결하게 표현한다, & 는 익명함수로 (내용) 괄호로 감쌈. 변수 &1 인 전달 요소 할당한다. 익명함수를 변수에 할당하여 사용도 가능

iex(74)> Enum.uniq_by([%{x: 1, y: 1}, %{x: 2, y: 1}, %{x: 3, y: 2}], fn(s) -> s.y end)
[%{x: 1, y: 1}, %{x: 3, y: 2}]
iex(75)> Enum.uniq_by([%{x: 1, y: 1}, %{x: 2, y: 1}, %{x: 3, y: 2}], &(&1.y))
[%{x: 1, y: 1}, %{x: 3, y: 2}]
// 변수에 할당
iex(76)> check = &(&1.y)
#Function<42.3316493/1 in :erl_eval.expr/6>
iex(77)> Enum.uniq_by([%{x: 1, y: 1}, %{x: 2, y: 1}, %{x: 3, y: 2}], check)
[%{x: 1, y: 1}, %{x: 3, y: 2}]

- 함수에 이름할당하여 사용

// First 모듈에 check 함수 만들기
iex(78)> defmodule First do
...(78)>   def check(s), do: s.y
...(78)> end
{:module, First,
 <<70, 79, 82, 49, 0, 0, 5, 204, 66, 69, 65, 77, 65, 116, 85, 56, 0, 0, 0, 195,
   0, 0, 0, 22, 12, 69, 108, 105, 120, 105, 114, 46, 70, 105, 114, 115, 116, 8,
   95, 95, 105, 110, 102, 111, 95, 95, 10, ...>>, {:check, 1}}

// 호출 에러 사례
iex(79)> Enum.uniq_by([%{x: 1, y: 1}, %{x: 2, y: 1}, %{x: 3, y: 2}], First.check)
** (UndefinedFunctionError) function First.check/0 is undefined or private. Did you mean:

      * check/1

    First.check()
    iex:79: (file)
iex(79)> Enum.uniq_by([%{x: 1, y: 1}, %{x: 2, y: 1}, %{x: 3, y: 2}], First.check(s))
warning: variable "s" does not exist and is being expanded to "s()", please use parentheses to remove the ambiguity or change the variable name
  iex:79

** (CompileError) iex:79: undefined function s/0 (there is no such import)
    (elixir 1.14.3) src/elixir_expand.erl:587: :elixir_expand.expand_arg/3
    (elixir 1.14.3) src/elixir_expand.erl:603: :elixir_expand.mapfold/5
    (elixir 1.14.3) src/elixir_expand.erl:867: :elixir_expand.expand_remote/8
    (elixir 1.14.3) src/elixir_expand.erl:587: :elixir_expand.expand_arg/3
    (elixir 1.14.3) src/elixir_expand.erl:603: :elixir_expand.mapfold/5
    (elixir 1.14.3) src/elixir_expand.erl:867: :elixir_expand.expand_remote/8
    (elixir 1.14.3) src/elixir.erl:376: :elixir.quoted_to_erl/4
    
// 익명함수로 호출
iex(79)> Enum.uniq_by([%{x: 1, y: 1}, %{x: 2, y: 1}, %{x: 3, y: 2}], &(First.check(&1)))
[%{x: 1, y: 1}, %{x: 3, y: 2}]

 

 

<참조>

데이터 형에 대한 이행: https://www.bitzflex.com/6

 

Elixir 의 데이터 형(Type)

사족일 수 있지만, 컴퓨터는 사실 모든 정보를 숫자로 저장, 연산 처리를 합니다. MP3, JPG 이미지, 워드 문서 등등 컴퓨터가 처리하는 모든 정보 내용은 컴퓨터 내에서 수치화되어서 처리가 됩니

www.bitzflex.com

애텀과 변수의 이해: https://www.bitzflex.com/8

 

변수와 애텀(Atom)

Elixir를 배우기 시작하면서 가장 혼동이 오는 부분이 변수와 애텀이었습니다. 애텀은 ? 거의 모든 언어에서 부울린값으로 true, false 를 사용합니다. 그냥 0, 1 을 true, false의 의미로 사용할 수도 있

www.bitzflex.com

https://namu.wiki/w/Erlang

 

Erlang - 나무위키

병행성 프로그래밍 언어인 Erlang은 가벼운 프로세스를 아주 빠르게 생성한다. 각각의 프로세스들은 메시지 패싱에 의해 작업을 지시받고 결과를 출력하며 ETS, DETS 메모리 영역을 제외하면 공유

namu.wiki

https://elixirschool.com/ko/lessons/basics/basics

 

기본 · Elixir School

Elixir를 시작합시다. 기본적인 타입과 연산자를 배워봅시다. elixir-lang.org 홈페이지의 Installing Elixir 가이드에서 운영체제별로 설치하는 방법을 알아볼 수 있습니다. Elixir를 설치하고 나서 어떤 버

elixirschool.com

0(n) 선형 복잡도: https://hanamon.kr/%EC%95%8C%EA%B3%A0%EB%A6%AC%EC%A6%98-time-complexity-%EC%8B%9C%EA%B0%84-%EB%B3%B5%EC%9E%A1%EB%8F%84/

 

[알고리즘] Time Complexity (시간 복잡도) - 하나몬

⚡️ Time Complexity (시간 복잡도) Time Complexity (시간 복잡도)를 고려한 효율적인 알고리즘 구현 방법에 대한 고민과 Big-O 표기법을 이용해 시간 복잡도를 나타내는 방법에 대해 알아봅시다. ❗️효

hanamon.kr

- Erlang Term 비교: https://www.erlang.org/doc/reference_manual/expressions.html#term-comparisons 

 

Erlang -- Expressions

maybe is an experimental new feature introduced in OTP 25. By default, it is disabled. To enable maybe, use compiler option {feature,maybe_expr,enable}. The feature must also be enabled in the runtime using the -enable-feature option to erl.

www.erlang.org

 

posted by 윤영식
2021. 10. 7. 10:59 React/Architecture

운영환경을 만들경우 번들링 파일간의 충돌을 최소화하기 위해 i18n 파일의 위치를 변경한다. 

  • Backend i18n은 public에 있을 필요가 없다. 
  • Frontend i18n은 위치도 간소화 한다. 

 

i18N 메세지 파일 위치 변경

Backend i18n 변경

apps/gateway/api/src/public/assets/i18n 의 assets 폴더를 apps/gateway/api/src 폴더 밑으로 위치 변경하고, assets/i18n/api 폴더를 assets/i18n 폴더 밑으로 이동한다. 

apps/gateway/api/project.json 에 assets 경로 추가하여 번들링시 포함되도록 한다. 

apps/gateway/api/src/environments/config.json 에서 i18n 위치를 변경한다.

Dashboard, Configuration, Back-Office의 API Backend에도 동일 환경을 적용한다. 특히 app.module.ts에 내역중 i18n고 TypeORM, Exception Filter 내역을 추가한다. 

import { join } from 'path';
import { MiddlewareConsumer, Module, NestModule, RequestMethod } from '@nestjs/common';
import { APP_FILTER, APP_GUARD } from '@nestjs/core';
import { ServeStaticModule } from '@nestjs/serve-static';
import { TypeOrmModule } from '@nestjs/typeorm';
import { getMetadataArgsStorage } from 'typeorm';

import { GatewayApiAppService, EntitiesModule, AuthModule, AuthMiddleware } from '@rnm/domain';
import { GlobalExceptionFilter, ormConfigService, RolesGuard, TranslaterModule } from '@rnm/shared';

import { environment } from '../environments/environment';
import { DashboardModule } from './dashboard/microservice/dashboard.module';
import { ConfigurationModule } from './configuration/microservice/configuration.module';
import { BackOfficeModule } from './back-office/microservice/back-office.module';
import { AppController } from './app.controller';
import { AuthController } from './auth/auth.controller';
import { UserController } from './user/user.controller';

@Module({
  imports: [
    ServeStaticModule.forRoot({
      rootPath: join(__dirname, 'public'),
      exclude: [
        '/api/auth*',
        '/api/gateway*', '/api/dashboard*', '/api/configuration*', '/api/back-office*',
        '/dashboard*', '/configuration*', '/back-office*'
      ],
    }),
    // ORM
    TypeOrmModule.forRoot({
      ...ormConfigService.getTypeOrmConfig(),
      entities: getMetadataArgsStorage().tables.map(tbl => tbl.target)
    }),
    // i18n
    TranslaterModule,
    // TypeORM
    EntitiesModule,
    // MicroService
    DashboardModule,
    ConfigurationModule,
    BackOfficeModule,
    // Auth
    AuthModule
  ],
  controllers: [
    AuthController,
    AppController,
    UserController
  ],
  providers: [
    GatewayApiAppService,
    // Global Exception Filter
    {
      provide: APP_FILTER,
      useClass: GlobalExceptionFilter,
    },
    {
      provide: APP_GUARD,
      useClass: RolesGuard,
    },
  ]
})
export class AppModule implements NestModule {
  configure(consumer: MiddlewareConsumer) {
    if(!environment || !environment.production) {
      return;
    }

    consumer
      .apply(AuthMiddleware)
      .forRoutes(...[
        { path: '/dashboard*', method: RequestMethod.ALL },
        { path: '/configuration*', method: RequestMethod.ALL },
        { path: '/back-office*', method: RequestMethod.ALL },
        // { path: '/api/*', method: RequestMethod.ALL },
      ]);
  }
}

 

 

Frontend i18n 위치 변경

apps/gateway/web/src/assets/i18n/web/locale-en.json 파일의 위치를 apps/gateway/web/src/assets/i18n/locale-en.json 로 옮긴다. 

apps/gateway/web/src/environments/config.json 파일을 위의 그림처럼 추가하고, i18n, auth 관련 설정을 넣는다. I18N_JSON_PATH 앞에 /dashboard 가 추가된것에 주의 한다. 

// config.json
{
  "AUTH": {
    "SECRET": "iot_secret_auth",
    "EXPIRED_ON": "1d",
    "REFRESH_SECRET": "iot_secret_refresh",
    "REFRESH_EXPIRED_ON": "7d"
  },
  "I18N_LANG": "en",
  "I18N_JSON_PATH": "/dashboard/assets/i18n/"
}

apps/gateway/web/src/environments/environment.ts 파일에 config.json을 import하여 export 한다.

export const environment = {
  production: false,
};

export const config = require('./config.json');

apps/gateway/web/src/app/core/i18n.ts 파일을 libs/ui/src/lib/i18n 폴더 밑으로 옮기고, 내역을 수정한다. 

import i18next from 'i18next';
import { initReactI18next } from 'react-i18next';
import XHR from 'i18next-xhr-backend';

export function initI18N(config: any) {
  const backendOptions = {
    loadPath: (config.I18N_JSON_PATH || '/assets/i18n/') + 'locale-{{lng}}.json',
    crossDomain: true,
  };
  
  i18next
    .use(XHR)
    .use(initReactI18next)
    .init({
      backend: backendOptions,
      debug: true,
      lng: config.I18N_LANG || 'en',
      fallbackLng: false,
      react: {
        useSuspense: true
      }
    });
}

libs/ui/src/index.ts export를 추가한다. 

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 한다.

{
  "/dashboard/api/*": {
    "target": "http://localhost:8001",
    "secure": false,
    "changeOrigin": true,
    "ws": true
  }
}

apps/dashboard/web/project.json 에 설정한다. 

  • proxyConfig
  • port
  • 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 이 호출되는지 체크한다. 

proxy통해 dashboard web의 index.html 호출 성공

 

소스: https://github.com/ysyun/rnm-stack/releases/tag/ms-11

 

Release ms-11 · ysyun/rnm-stack

fixed i18n config and test env

github.com

 

posted by 윤영식
2021. 10. 2. 21:10 React/Architecture

NestJS과 React에 i18n을 적용하고, config 파일로딩에 대한 리팩토링과 기타 기능들을 추가로 적용한다. 

 

 

NestJS에 i18n 적용

nestjs-i18n 패키지를 사용한다. 

$> yarn add nestjs-i18n

 

i18n message 파일은 json 포멧이고, 이를 위해 apps/gateway/api/src/public/assets/i18n/api 폴더를 생성한다. i18n/api 폴더에는 언어에 맞는 폴더를 생성한다. 

  • nestjs 번들링 배포시 api 서버의 i18n 파일은 public/assets/i18n/api 폴더 하위에 위치한다. 
  • react 번들링 파일의 i18n 파일은 public/assets/i18n/web 폴더 하위에 위치한다.

libs/shared/src/lib/configuration/config.model.ts 의 GatewayConfiguration에 I18N_LANG 을 추가한다.

// config.model.ts
export interface MicroServiceConfiguration {
  REVERSE_CONTEXT?: string;
  REVERSE_ADDRESS?: string;
  HTTP_PORT?: number,
  TCP_HOST?: string;
  TCP_PORT?: number,
  GLOBAL_API_PREFIX?: string;
  AUTH?: AuthConfig;
  I18N_LANG?: string; // <== 요기
  I18N_JSON_PATH?: string; // <== 요기
}

export interface GatewayConfiguration {
  HTTP_PORT?: number,
  DASHBOARD?: MicroServiceConfiguration;
  CONFIGURATION?: MicroServiceConfiguration;
  BACK_OFFICE?: MicroServiceConfiguration;
  AUTH?: AuthConfig;
  I18N_LANG?: string; // <== 요기
  I18N_JSON_PATH?: string; // <== 요기
}

apps/gateway/api/src/environments/config.json 파일에 환경을 설정한다. 

// config.json
{
  "HTTP_PORT": 8000,
  "AUTH": {
    "SECRET": "iot_secret_auth",
    "EXPIRED_ON": "1d",
    "REFRESH_SECRET": "iot_secret_refresh",
    "REFRESH_EXPIRED_ON": "7d"
  },
  "I18N_LANG": "en",
  "I18N_JSON_PATH": "/public/assets/i18n/api/",
  ...
}

i18n 파일을 apps/gateway/api/src/public/assets/i18n/api/en(ko)/message.json 파일을 생성하고, 설정한다. 

{
  "USER_NOT_EXIST": "User {username} with this id does not exist"
}

다음으로 libs/shared 쪽에 libs/shared/src/lib/i18n/translater.service.ts 파일을 생성한다.

// translater.service.ts
import { Injectable } from '@nestjs/common';
import { I18nService } from 'nestjs-i18n';

@Injectable()
export class TranslaterService {
  constructor(private readonly i18nService: I18nService) { }

  async message(key: string, message: (string | { [k: string]: any; })[] | { [k: string]: any; }): Promise<string> {
    return this.i18nService.translate(`message.${key}`, { args: message });
  }
}

translater module도 libs/shared/src/lib/i18n/translater.module.ts 파일도 생성한다. 

// translater.module.ts
import { join } from 'path';
import { Module } from '@nestjs/common';
import { I18nModule, I18nJsonParser } from 'nestjs-i18n';
import { loadConfigJson } from '@rnm/shared';
import { TranslaterService } from './translater.service';

const config: any = loadConfigJson();

@Module({
  imports: [
    I18nModule.forRoot({
      fallbackLanguage: config.I18N_LANG,
      parser: I18nJsonParser,
      parserOptions: {
        path: join(__dirname, config.I18N_JSON_PATH),
      },
    })
  ],
  providers: [TranslaterService],
  exports: [TranslaterService]
})
export class TranslaterModule { }


// libs/shared/src/index.ts 안에 export도 추가한다. 
export * from './lib/i18n/translater.service';
export * from './lib/i18n/translater.module';

이제 사용을 해본다.

  • apps/gateway/api/src/app/app.module.ts 에 TranslaterModule을 추가한다.
  • apps/gateway/api/src/app/app.controller.ts 에 Service를 사용한다. translate key로는 [fileName].[jsonKey] 를 넣는다. 
// app.module.ts
import { TranslaterModule } from '@rnm/shared';
@Module({
  imports: [
    ...
    // i18n
    TranslaterModule,
    ...
}


// app.controller.ts
import { Controller, Get } from '@nestjs/common';
import { GatewayApiAppService } from '@rnm/domain';
import { TranslaterService } from '@rnm/shared';

@Controller('api/gateway')
export class AppController {
  constructor(
    private readonly appService: GatewayApiAppService,
    private readonly translater: TranslaterService
  ) { }

  @Get()
  getData() {
    return this.translater.message('USER_NOT_EXIST', { username: 'Peter Yun' });
  }
}

Gateway API를 디버깅 시작하고, 호출 테스트한다.  Forbidden 에러가 떨어지면 app.module.ts의 AuthMiddleware 경로에서 잠시 "/api*" 설정을 제거후 테스트 한다. 

// apps/gateway/api/src/app/app.module.ts
export class AppModule implements NestModule {
  configure(consumer: MiddlewareConsumer) {
    consumer
      .apply(AuthMiddleware)
      .forRoutes(...[
        { path: '/dashboard*', method: RequestMethod.ALL },
        { path: '/configuration*', method: RequestMethod.ALL },
        { path: '/back-office*', method: RequestMethod.ALL },
        // { path: '/api/*', method: RequestMethod.ALL }, <== 요기
      ]);
  }
}

맵핑되어 정보가 나옴

에러 메세지에 대해 Global Exception에 적용해 본다. 

import { Request, Response } from 'express';
import { ArgumentsHost, Catch, ExceptionFilter, HttpException, HttpStatus, Logger } from '@nestjs/common';
import { TranslaterService } from '../i18n/translater.service';

@Catch()
export class GlobalExceptionFilter implements ExceptionFilter {
  constructor(private readonly translater: TranslaterService) { }

  // async로 Promise 반환
  async catch(exception: any, host: ArgumentsHost) {
    const ctx = host.switchToHttp();
    const response = ctx.getResponse<Response>();
    const request = ctx.getRequest<Request>();
    let message = (exception as any).message;
    // key, args가 있으면 translater
    if (message && message.key && message.args) {
      message = await this.translater.message(message.key, message.args);
    }
    ...
  }
}

 

 

React에 i18n 적용

react-i18next를 사용한다.

$> yarn add react-i18next i18next i18next-xhr-backend

i18n 설정을 위해 apps/gateway/web/src/app/core/i18n.ts 파일을 생성한다. 

// i18n.ts
import i18next from 'i18next';
import { initReactI18next } from 'react-i18next';
import XHR from 'i18next-xhr-backend';

const backendOptions = {
  loadPath: '/assets/i18n/web/locale-{{lng}}.json',
  crossDomain: true,
};

i18next
  .use(XHR)
  .use(initReactI18next)
  .init({
    backend: backendOptions,
    debug: true,
    lng: 'en',
    fallbackLng: false,
    react: {
      useSuspense: true
    }
  });

export default i18next;

설정파일을 apps/gateway/web/src/assets/i18n/web/locale-en.json 을 생성한다. 

{
  "LOGIN": {
    "USERNAME": "Username",
    "PASSWORD": "Password"
  }
}

apps/gateway/web/src/app/app.tsx 파일에 i18n 파일을 로딩한다. 

// app.tsx
import { Suspense } from 'react';
import styles from './app.module.scss';
import Login from './login/login';

import './core/i18n';

const Loader = () => (
  <div className={styles.loading}>
    {/* <img src={logo} className="App-logo" alt="logo" /> */}
    <div>loading...</div>
  </div>
);

export function App() {
  return (
    <Suspense fallback={<Loader />}>
      <Login />;
    </Suspense>
  );
}
export default App;

apps/gateway/web/src/app/login/login.tsx 에서 useTranslation() hook을 사용한다. 

import { Row, Col, Form, Input, Button } from 'antd';
// import 
import { useTranslation } from 'react-i18next';
...

function Login() {
  const { t, i18n } = useTranslation();
  return (
    <div className={styles.login_container}>
     ...
              // t를 통해 translation
              <Form.Item
                label={t('LOGIN.USERNAME')}
                name="username"
                rules={[{ required: true, message: 'Please input your username!' }]}
              >
                <Input />
              </Form.Item>

              <Form.Item
                label={t('LOGIN.PASSWORD')}
                name="password"
                rules={[{ required: true, message: 'Please input your password!' }]}
              >
                <Input.Password />
              </Form.Item>
     ...
   </div>
  );
}

 

 

Configuration 리팩토링

NestJS에서 사용하는 config.json 파일을 한번만 로딩하도록 libs/shared/src/lib/configuration/config.service.ts 파일을 리팩토링한다. 

// config.service.ts
import * as fs from "fs";
import { join } from 'path';
import { GatewayConfiguration, MicroServiceConfiguration, OrmConfiguration } from "./config.model";

export const loadConfigJson = (message = '[LOAD] config.json file'): MicroServiceConfiguration | GatewayConfiguration => {
  let config: any = process.env.config;
  if (!config) {
    console.log(`${message}:`, `${__dirname}/environments/config.json`);
    const jsonFile = fs.readFileSync(join(__dirname, 'environments', 'config.json'), 'utf8');
    process.env.config = jsonFile;
    config = JSON.parse(jsonFile);
  } else {
    config = JSON.parse(process.env.config as any);
  }
  return config;
}

export const loadOrmConfiguration = (message = '[LOAD] orm-config.json file'): OrmConfiguration => {
  let config: any = process.env.ormConfig;
  if (!config) {
    console.log(`${message}:`, `${__dirname}/environments/orm-config.json`);
    const jsonFile = fs.readFileSync(join(__dirname, 'environments', 'orm-config.json'), 'utf8');
    process.env.ormConfig = jsonFile;
    config = JSON.parse(jsonFile);
  } else {
    config = JSON.parse(process.env.ormConfig as any);
  }
  return config;
}

 

소스: https://github.com/ysyun/rnm-stack/releases/tag/ms-10

 

 

<참조>

- nestjs-i18n 적용하기 

https://github.com/ToonvanStrijp/nestjs-i18n

 

GitHub - ToonvanStrijp/nestjs-i18n: Add i18n support inside your nestjs project

Add i18n support inside your nestjs project. Contribute to ToonvanStrijp/nestjs-i18n development by creating an account on GitHub.

github.com

- react best i18n libraries 

https://phrase.com/blog/posts/react-i18n-best-libraries/

 

Curated List: Our Best of Libraries for React I18n – Phrase

There may be no built-in solution for React i18n, but these amazing libraries will help you manage your i18n projects from start to finish.

phrase.com

- react-i18next 공식 홈페이지

https://react.i18next.com/

 

Introduction

 

react.i18next.com

- i18next의 react 사용예

https://github.com/i18next/react-i18next/blob/master/example/react/src/App.js

 

GitHub - i18next/react-i18next: Internationalization for react done right. Using the i18next i18n ecosystem.

Internationalization for react done right. Using the i18next i18n ecosystem. - GitHub - i18next/react-i18next: Internationalization for react done right. Using the i18next i18n ecosystem.

github.com

 

posted by 윤영식