블로그 이미지
Peter Note
Web & LLM FullStacker, Application Architecter, KnowHow Dispenser and Bike Rider

Publication

Category

Recent Post

2024. 8. 4. 14:00 LLM FullStacker/LangChain

LangChain은 LLM 애플리케이션을 구축하기 위한 개발 프레임워크로써 다양한 컴포넌트를 제공한다. 프러덕션 애플리케이션 개발시 RAG(Retrieval Augmented Generation)를 기반으로 할 때 LangChain 컴포넌트를 통해 일관된 코드 인터페이스를 유지할 수 있다.

LangChain Components

Prompts

  • Prompt Templates
  • Output Parsers: 5+ implementations
    • Retry/fixing logic
  • Example Selectors: 5+ implementations

Models

  • LLM's: 20+ integrations
  • Chat Models
  • Text Embedding Models: 10+ integrations

Indexes

  • Document Loaders: 50+ implementations
  • Text Splitters: 10+ implementations
  • Vector stores: 10+ integrations
  • Retrievers: 5+ integrations/implementations

Chains

  • Can be used as building blocks for other chains
  • More application specific cahins: 20+ different types

Agents

  • Agent Types: 5+ types
    • Algorithms for getting LLMs to use tools
  • Agent Tookkits: 10+ implementations
    • Agents armed with specific tools for a specific application

LangChain API

각 컴포넌트는 구현된 패키지와 API랑 함께 볼 필요가 있다.

langchain

  • document_loaders
  • text_splitter
  • embeddings
  • retrievers
  • prompts
  • chat_models
  • output_parsers
  • chains
  • agents

등의 패키지에서 langchain_community, langchain_core 패키지의 모듈을 re-export 하고 있다. 중요 패키지는 langchain.* 으로 import 할 수 있다.

  • text_splitter.py : text-splitters를 re-export하고 있다.
  • text_splitter 별도 패키지

langchain-core

langchain-community


RAG Step

RAG는 크게 2단계로 볼 수 있다. 사전에 프라이빗 정보를 로딩->쪼개기->임베딩벡터->저장하기 단계를 거쳐서 준비를 한다. 이때 LangChain의 Indexes 영역의 컴포넌트를 사용한다. 다음으로 사용자가 질의를 하게되면 프라이빗 정보를 기반으로 증강검색->프롬프트생성->LLM응답->응답처리 등의 과정을 거쳐, 유의미한 응답을 생성한다. 이때 LangChain의 Prompts, Models, Chains, Agents 영역의 컴포넌트를 사용한다.

 

출처: DeepLearning.ai

1 단계

  • Document Loader
    • Structured/Unstructured 미디어를 로딩한다.
      • 문서, 이미지, 동영상, 음성
  • Splitting
  • Embedding
  • Vector Storing

2단계

  • Retrieval
  • Prompting
  • LLM
  • Output Parsing
posted by Peter Note
2024. 7. 22. 22:16 LLM FullStacker/Python

튜플 (Tuple)

튜플(tuple)은 파이썬의 내장 데이터 타입 중 하나로, 여러 값을 하나의 순서 있는 집합으로 저장할 수 있는 자료형입니다. 리스트(list)와 유사하지만, 몇 가지 중요한 차이점이 있습니다.

주요 특징

  1. 변경 불가능(Immutable): 한 번 생성된 튜플의 원소는 변경할 수 없습니다. 따라서 튜플의 내용을 수정, 추가, 삭제하는 작업은 불가능합니다.
  2. 순서가 있음(Ordered): 튜플은 순서가 있는 데이터 타입으로, 각 원소는 인덱스를 통해 접근할 수 있습니다.
  3. 중복 허용: 튜플 내에 동일한 값을 여러 번 포함할 수 있습니다.

생성 방법

튜플은 소괄호 ()를 사용하여 생성하며, 각 요소는 쉼표(,)로 구분합니다.

# 튜플 생성 예시
empty_tuple = ()
single_element_tuple = (42,)  # 하나의 요소를 가진 튜플 생성 시 쉼표 필요
multiple_elements_tuple = (1, 2, 3)

튜플은 괄호 없이도 생성할 수 있습니다. 예를 들어:

another_tuple = 1, 2, 3

접근 방법

튜플의 각 요소에 접근하려면 리스트와 마찬가지로 인덱스를 사용합니다.

example_tuple = (10, 20, 30)
print(example_tuple[0])  # 출력: 10
print(example_tuple[1])  # 출력: 20
print(example_tuple[-1])  # 출력: 30 (마지막 요소)

활용 예

튜플은 주로 다음과 같은 상황에서 사용됩니다:

  • 변경 불가능한 데이터 구조가 필요할 때
  • 여러 값을 하나로 묶어 함수의 인수나 반환값으로 사용하고자 할 때
  • 딕셔너리의 키로 사용하고자 할 때 (리스트는 키로 사용할 수 없음)

기타 메서드

튜플은 몇 가지 유용한 메서드를 제공합니다:

example_tuple = (1, 2, 3, 2, 1)

# 특정 값의 개수 세기
print(example_tuple.count(2))  # 출력: 2

# 특정 값의 인덱스 찾기
print(example_tuple.index(3))  # 출력: 2

이와 같이, 튜플은 파이썬에서 매우 유용한 데이터 타입으로, 특정 상황에서 매우 효율적으로 사용될 수 있습니다.


리스트 (List)

리스트는 순서가 있는 변경 가능한(mutable) 데이터 타입으로, 다양한 유형의 값을 포함할 수 있습니다. 리스트는 대괄호 []를 사용하여 생성하며, 각 요소는 쉼표로 구분합니다.

주요 특징

  1. 변경 가능(Mutable): 리스트의 요소를 추가, 수정, 삭제할 수 있습니다.
  2. 순서가 있음(Ordered): 리스트는 순서를 유지하며, 인덱스를 통해 각 요소에 접근할 수 있습니다.
  3. 중복 허용: 리스트는 동일한 값을 여러 번 포함할 수 있습니다.

생성 방법 및 사용 예

# 리스트 생성
empty_list = []
numbers = [1, 2, 3, 4, 5]
mixed_list = [1, "hello", 3.14, True]

# 리스트 요소 접근
print(numbers[0])  # 출력: 1
print(numbers[-1])  # 출력: 5

# 리스트 수정
numbers[0] = 10
print(numbers)  # 출력: [10, 2, 3, 4, 5]

# 리스트 요소 추가
numbers.append(6)
print(numbers)  # 출력: [10, 2, 3, 4, 5, 6]

# 리스트 요소 삭제
numbers.remove(10)
print(numbers)  # 출력: [2, 3, 4, 5, 6]

 

딕셔너리 (Dictionary)

딕셔너리는 키(key)와 값(value) 쌍을 저장하는 변경 가능한(mutable) 데이터 타입입니다. 딕셔너리는 중괄호 {}를 사용하여 생성하며, 각 키-값 쌍은 콜론 :으로 구분합니다.

주요 특징

  1. 변경 가능(Mutable): 딕셔너리의 키-값 쌍을 추가, 수정, 삭제할 수 있습니다.
  2. 순서가 없음(Unordered) (파이썬 3.7 이후로는 삽입 순서 유지)
  3. 고유 키(Unique Key): 딕셔너리의 각 키는 고유해야 하며, 중복된 키를 가질 수 없습니다.

생성 방법 및 사용 예

# 딕셔너리 생성
empty_dict = {}
person = {"name": "Alice", "age": 25, "city": "New York"}

# 딕셔너리 값 접근
print(person["name"])  # 출력: Alice
print(person.get("age"))  # 출력: 25

# 딕셔너리 값 수정
person["age"] = 30
print(person)  # 출력: {'name': 'Alice', 'age': 30, 'city': 'New York'}

# 딕셔너리 키-값 쌍 추가
person["job"] = "Engineer"
print(person)  # 출력: {'name': 'Alice', 'age': 30, 'city': 'New York', 'job': 'Engineer'}

# 딕셔너리 키-값 쌍 삭제
del person["city"]
print(person)  # 출력: {'name': 'Alice', 'age': 30, 'job': 'Engineer'}

리스트와 딕셔너리 비교

특징 리스트 (List) 딕셔너리 (Dictionary)
변경 가능성 변경 가능 변경 가능
순서 순서가 있음 파이썬 3.7 이후로는 삽입 순서 유지, 그 전에는 순서가 없음
중복 요소 중복 허용 키 중복 불가, 값 중복 허용
접근 방법 인덱스를 통해 요소에 접근 (list[0]) 키를 통해 값에 접근 (dict["key"])

 

리스트와 딕셔너리는 각각의 장점이 있으며, 특정 상황에 맞게 선택하여 사용할 수 있습니다. 데이터를 순차적으로 처리해야 할 때는 리스트를, 키-값 쌍으로 데이터를 관리해야 할 때는 딕셔너리를 사용하면 됩니다.

posted by Peter Note
2024. 7. 14. 12:02 LLM FullStacker

LLM의 Embedding 이해하기

LLM(Language Modeling Layer)은 자연어 처리(NLP) 작업에서 사용되는 신경망 아키텍처의 한 유형입니다. LLM은 입력된 텍스트를 분석하고 그 의미와 구조를 이해하기 위해 훈련됩니다. 이를 통해 LLM은 주어진 문맥에 기반하여 다음 단어나 구절을 예측하는 것과 같은 작업을 수행할 수 있습니다.

LLM이 텍스트를 분석할 때, 각 단어 또는 구절의 의미를 나타내는 수치적 표현인 임베딩을 생성합니다. 이 임베딩들은 일반적으로 벡터라고 불리는 다차원 공간에서 표현됩니다. 각 차원은 특정 의미나 속성을 나타내며, 예를 들어 감정이나 주제 등이 있습니다.

LLM이 생성하는 임베딩과 벡터는 NLP 작업에 매우 유용합니다. 이들은 단어와 구절 사이의 관계를 분석하고, 유사성을 측정하며, 새로운 문장을 생성하거나 기존 문장을 수정하는 데 사용될 수 있습니다. 또한 LLM은 다양한 언어와 도메인에서 훈련되어 다재다능하고 적응성이 뛰어난 모델을 만들 수 있습니다.

예를 들어, LLM이 'The quick brown fox jumps over the lazy dog'라는 문장에 대해 훈련된 경우, 각 단어에 대한 임베딩과 그 사이의 관계를 생성할 것입니다. 이 정보를 사용하여 LLM은 다음 단어나 구절을 예측하거나, 주어진 텍스트의 감정이나 주제를 분석하거나, 새로운 문장을 생성하는 데 사용할 수 있습니다.

요약하자면, LLM은 NLP 작업에서 사용되는 신경망 아키텍처로, 입력된 텍스트를 분석하고 그 의미와 구조를 이해하기 위해 훈련됩니다. LLM이 생성하는 임베딩과 벡터는 단어와 구절 사이의 관계를 분석하고, 유사성을 측정하며, 새로운 문장을 생성하거나 기존 문장을 수정하는 데 사용될 수 있습니다. 다양한 언어와 도메인에서 훈련된 LLM은 다재다능하고 적응성이 뛰어난 모델을 만들 수 있어 NLP 작업에 매우 유용합니다. 

 

from Llama 3

 

- 다음 문장을 예측 생성

- 문장안의 감정을 이해

 

References

- 남세동대표의 Embedding Vector 설명

https://youtu.be/wdwHxyz3Hbo?si=-PkBpWHroSikaVVD


- 김길호님의 Embedding이란 무엇이고, 어떻게 사용하는가?

https://www.syncly.kr/blog/what-is-embedding-and-how-to-use

 

Embedding이란 무엇이고, 어떻게 사용하는가? - 싱클리(Syncly)

본 글에서는, AI에서 중요하게 취급되는 개념 중 하나인 embedding에 대해서 알아보고자 합니다. 현재 Syncly에서도 Feedback Auto-Categorization, Sentiment Classification 등의 기능에 embedding이 활용되고 있습니

www.syncly.kr

- text간 vector 시각화 도구 

https://projector.tensorflow.org/

 

Embedding projector - visualization of high-dimensional data

Visualize high dimensional data.

projector.tensorflow.org

- GPT Tokenizer 카운팅 
https://platform.openai.com/tokenizer

 

posted by Peter Note
2024. 7. 2. 23:49 LLM FullStacker/Python

pyproject.toml과 requirements.txt는 Python 프로젝트에서 의존성을 관리하는 데 사용되는 파일이지만, 그 목적과 기능은 다릅니다. 두 파일 간의 주요 차이점을 살펴보겠습니다.

pyproject.toml

pyproject.toml은 Python 프로젝트의 메타데이터 및 의존성을 선언하는 데 사용되는 파일입니다. 이 파일은 PEP 518에 정의되어 있으며, 프로젝트 빌드 시스템과 빌드 의존성을 지정하는 데 사용됩니다. Poetry와 같은 현대적인 패키지 관리 도구는 pyproject.toml을 사용하여 프로젝트의 모든 의존성을 관리합니다.

주요 특징

  1. 메타데이터 관리: 프로젝트 이름, 버전, 설명, 저자 등의 메타데이터를 포함합니다.
  2. 의존성 관리: 개발 및 런타임 의존성을 모두 포함할 수 있습니다.
  3. 빌드 시스템 설정: 빌드 백엔드(예: setuptools, poetry 등)를 지정할 수 있습니다.
  4. Poetry 통합: Poetry는 pyproject.toml 파일을 사용하여 패키지 관리와 의존성 설치를 수행합니다.

예시

[tool.poetry] 
name = "my_project" 
version = "0.1.0" 
description = "A sample project" 
authors = ["Your Name <you@example.com>"] 

[tool.poetry.dependencies] 
python = "^3.8" 
requests = "^2.25.1" 

[tool.poetry.dev-dependencies] 
pytest = "^6.2.3" 

[build-system] 
requires = ["poetry-core>=1.0.0"] 
build-backend = "poetry.core.masonry.api"

requirements.txt

requirements.txt는 전통적인 방법으로 Python 프로젝트의 의존성을 관리하는 파일입니다. 주로 pip를 사용하여 의존성을 설치할 때 사용되며, 각 의존성을 별도의 줄에 작성합니다.

주요 특징

  1. 단순성: 각 줄에 하나의 패키지와 선택적인 버전 제한을 작성하여 의존성을 명시합니다.
  2. 의존성 설치: pip install -r requirements.txt 명령어를 사용하여 의존성을 설치합니다.
  3. 개발 및 런타임 의존성 분리 어려움: 개발 및 런타임 의존성을 명확히 분리하기 어렵습니다.

예시

requests==2.25.1 
pytest==6.2.3

비교 및 차이점

특징pyproject.tomlrequirements.txt

목적 프로젝트 메타데이터 및 의존성 관리 의존성 목록 관리
포맷 TOML 단순 텍스트
의존성 종류 분리 개발 및 런타임 의존성 분리 가능 분리하기 어려움
빌드 시스템 통합 빌드 시스템 및 백엔드 지정 가능 해당 없음
사용 도구 Poetry, setuptools 등 pip
기타 설정 빌드 설정, 스크립트 등 다양한 설정 포함 가능 해당 없음

선택 기준

  • 현대적인 프로젝트 관리: Poetry와 같은 도구를 사용하여 의존성을 보다 체계적으로 관리하고 싶다면 pyproject.toml을 사용하는 것이 좋습니다.
  • 전통적인 방법: 간단하게 의존성만 관리하고 싶다면 requirements.txt가 충분할 수 있습니다.

함께 사용하기

Poetry를 사용하면서도 requirements.txt 파일을 생성하여 기존의 워크플로우와 호환성을 유지할 수 있습니다. 다음 명령어를 사용하여 requirements.txt 파일을 생성할 수 있습니다:

poetry export -f requirements.txt --output requirements.txt
 

이렇게 하면, pyproject.toml에서 관리되는 의존성을 requirements.txt 형식으로 내보낼 수 있어 기존 도구들과 호환성을 유지할 수 있습니다.

요약

  • pyproject.toml은 프로젝트 메타데이터, 의존성 및 빌드 설정을 포함하는 현대적인 방식의 관리 파일입니다.
  • requirements.txt는 단순히 의존성을 나열하는 전통적인 텍스트 파일입니다.
  • pyproject.toml은 더 많은 기능과 유연성을 제공하며, 특히 Poetry와 같은 도구와 함께 사용할 때 유용합니다.
  • requirements.txt는 간단하고 널리 사용되지만, 개발 및 런타임 의존성 분리가 어렵고, 빌드 시스템 설정과 같은 고급 기능을 제공하지 않습니다.

 

References

https://teddylee777.github.io/poetry/poetry-tutorial/

 

poetry 의 거의 모든것 (튜토리얼)

poetry 로 가상환경을 구축하는 방법을 단계별로 설명합니다.

teddylee777.github.io

 

posted by Peter Note
2024. 7. 2. 22:27 LLM FullStacker/Python

Python에서 lambda 키워드는 익명 함수(anonymous function)를 생성하는 데 사용됩니다. lambda 함수를 사용하면 이름 없이도 함수 객체를 생성할 수 있습니다. 일반적으로 lambda 함수는 간단한 기능을 수행하는 짧은 함수가 필요할 때 사용됩니다.

lambda 함수의 구문

lambda 키워드를 사용하여 함수를 정의하는 구문은 다음과 같습니다:

lambda arguments: expression
  • arguments: 함수에 전달될 인수들입니다.
  • expression: 함수가 반환할 표현식입니다.

예시

기본 사용 예시

# 일반 함수 정의 
def add(x, y): 
  return x + y 
  
# lambda 함수 정의 
add_lambda = lambda x, y: x + y 

# 함수 호출 
print(add(2, 3)) # 5 
print(add_lambda(2, 3)) # 5

리스트의 각 요소에 함수를 적용하는 예시

# lambda 함수를 사용하여 리스트의 각 요소에 2를 곱함 
numbers = [1, 2, 3, 4, 5] 
doubled = list(map(lambda x: x * 2, numbers)) 
print(doubled) # [2, 4, 6, 8, 10]
 

정렬 시에 키로 사용하는 예시

# lambda 함수를 사용하여 리스트를 정렬 
pairs = [(1, 'one'), (2, 'two'), (3, 'three'), (4, 'four')] 
# 두 번째 요소(문자열) 기준으로 정렬 
pairs.sort(key=lambda pair: pair[1]) 
print(pairs) # [(4, 'four'), (1, 'one'), (3, 'three'), (2, 'two')]

lambda 함수와 일반 함수 비교

lambda 함수는 익명 함수로, 보통 한 줄로 표현되며, 작은 연산이나 함수 객체가 필요한 곳에서 주로 사용됩니다. 일반 함수는 def 키워드를 사용하여 이름이 있는 함수로 정의되며, 여러 줄로 이루어질 수 있고 복잡한 논리를 포함할 수 있습니다.

예시: lambda 함수와 일반 함수의 비교

# lambda 함수 
multiply = lambda x, y: x * y 
print(multiply(2, 3)) # 6 

# 일반 함수 
def multiply_def(x, y): 
  return x * y 
  
print(multiply_def(2, 3)) # 6

사용 시 주의사항

  • 간결성: lambda 함수는 단일 표현식만 포함할 수 있으며, 여러 문장을 포함할 수 없습니다. 따라서 복잡한 로직을 처리하기에는 적합하지 않습니다.
  • 가독성: 짧고 간단한 경우에는 lambda 함수가 유용하지만, 너무 복잡한 경우 가독성을 해칠 수 있으므로 일반 함수를 사용하는 것이 좋습니다.

요약

  • lambda 키워드는 익명 함수를 생성하는 데 사용됩니다.
  • 간단한 함수나 일회성 함수 객체가 필요한 곳에서 유용합니다.
  • 구문은 lambda arguments: expression 형태를 가지며, 단일 표현식만 포함할 수 있습니다.
  • 일반 함수(def 사용)와 비교할 때, 더 간단하고 간결하지만 복잡한 로직에는 적합하지 않습니다.

이를 통해 lambda 키워드의 역할과 사용 방법을 이해할 수 있습니다.

posted by Peter Note
2024. 6. 30. 14:17 LLM FullStacker/LangChain

"LangChain 은 LLM기반  AI 애플리케이션 개발에 있어서의  Spring Framework이다."

LangChain vs Spring Framework, Created by ChatGPT

 

Java로 웹개발을 한다면 기본 Framework으로 개발자들이 Spring Framework을 사용하듯이 LLM 기반 AI 애플리케이션 개발은 LangChain 프레임워크로 시작하게 될 것이다. 따라서 LLM 기반 AI 애플리케이션을 개발하고 싶다면 기본 LangChain Framework을 사용하게되는 시즌이 시작되었다는 뜻이다. 

 

Python vs Javascript(Typescripit)

LangChain, LangGraph, LangSmith등 서비스를 사용하기 위하여 Python 또는 Javascript 중 하나를 선택해서 시작할 수 있다. 

  - LangGraph Cloud는 아직 Python만 지원한다.

  - LangSmith는 상업적 이용시 Commercial 이다.  

 

Javascript를 해보았으니, Python으로 시작해 본다. 

 

ChatGPT 설명

LangChain Package Structure

LangChain은 언어 모델을 사용한 애플리케이션 개발을 위한 프레임워크로, 다양한 패키지와 모듈로 구성되어 있습니다. 아래는 LangChain의 일반적인 패키지 구조입니다:

 

LangChain: 최상위 패키지

Chains: 체인 및 워크플로우 관리

LLMChain: 대형 언어 모델 체인 클래스

VectorDBQAChain: 벡터 데이터베이스 기반 QA 체인 클래스

SQLDBQAChain: SQL 데이터베이스 기반 QA 체인 클래스

Prompts: 프롬프트 관리 및 생성

PromptTemplate: 프롬프트 템플릿 클래스

FewShotPromptTemplate: 몇 샷 학습 프롬프트 템플릿 클래스

Agents: 에이전트 및 도구 통합

AgentExecutor: 에이전트 실행기 클래스

Tool: 도구 클래스

Memory: 메모리 관리

BufferMemory: 버퍼 메모리 클래스

VectorStoreRetrieverMemory: 벡터 저장소 리트리버 메모리 클래스

LLMs: 대형 언어 모델 통합

OpenAI: OpenAI 모델 통합 클래스

Cohere: Cohere 모델 통합 클래스

Utilities: 유틸리티 도구

Wikipedia: Wikipedia 통합 유틸리티

PythonREPL: Python REPL 유틸리티

 

이 구조는 LangChain의 주요 구성 요소와 각 구성 요소가 제공하는 기능을 잘 나타내고 있습니다. LangChain은 체인 관리, 프롬프트 생성, 에이전트 통합, 메모리 관리, 대형 언어 모델 통합, 그리고 다양한 유틸리티 도구를 통해 언어 모델 기반 애플리케이션 개발을 지원합니다.

 

LangGraph Package Structure

LangGraph는 언어 모델을 활용한 그래프 기반 애플리케이션을 구축하기 위한 프레임워크입니다. 아래는 LangGraph의 일반적인 패키지 구조입니다

 

LangGraph: 최상위 패키지

Graphs: 그래프 생성 및 관리

GraphBuilder: 그래프 빌더 클래스

Node: 그래프의 노드 클래스

Edge: 그래프의 엣지 클래스

Algorithms: 그래프 알고리즘

PathFinding: 경로 찾기 알고리즘 (예: Dijkstra, A*)

Centrality: 중심성 측정 알고리즘 (예: Betweenness, Closeness)

Clustering: 그래프 클러스터링 알고리즘

Visualization: 그래프 시각화 도구

GraphVisualizer: 그래프 시각화 클래스

PlotSettings: 시각화 설정 클래스

Data: 데이터 처리 및 로딩

DataLoader: 데이터 로딩 클래스

DataProcessor: 데이터 처리 클래스

Models: 언어 모델 통합

ModelInterface: 언어 모델 인터페이스

LLMIntegration: 대형 언어 모델 통합 클래스 (예: GPT-4, BERT)

Utilities: 유틸리티 도구

Logger: 로깅 유틸리티

ConfigManager: 설정 관리 유틸리티

 

이 패키지 구조는 LangGraph의 주요 구성 요소와 각 구성 요소가 제공하는 기능을 잘 나타내고 있습니다. LangGraph는 그래프 기반의 데이터 구조를 생성, 관리, 분석 및 시각화하는 데 필요한 다양한 도구를 제공하며, 언어 모델과의 통합을 통해 더욱 강력한 기능을 지원합니다.

---

LangChain과 Spring Framework는 각각의 도메인에서 비슷한 역할을 수행합니다, 단지 다른 맥락에서 사용된다는 차이가 있습니다:

 

LangChain:

 

도메인: 언어 모델 및 자연어 처리.

목적: 언어 모델(예: GPT-3, GPT-4 등)로 구동되는 애플리케이션을 구축하기 위한 프레임워크를 제공합니다. LangChain은 언어 모델의 통합, 배포 및 오케스트레이션을 간소화하는 것을 목표로 합니다.

특징:

체인 관리: 여러 언어 모델과 작업을 관리하고 오케스트레이션하는 데 도움을 줍니다.

모듈성: 언어 모델 애플리케이션을 구축, 미세 조정 및 배포하는 모듈식 접근 방식을 제공합니다.

확장성: 기능을 향상시키기 위해 다양한 API 및 외부 도구와 쉽게 통합할 수 있습니다.

유틸리티 도구: 프롬프트 엔지니어링, 대화 흐름 관리 등을 위한 유틸리티를 제공합니다.

 

Spring Framework:

 

도메인: 엔터프라이즈 자바 애플리케이션.

목적: 자바 기반의 엔터프라이즈 애플리케이션을 개발하기 위한 포괄적인 프레임워크입니다. Spring은 인프라 지원을 제공하여 애플리케이션 개발을 단순화합니다.

특징:

의존성 주입: 객체 생성과 의존성을 유연하고 느슨하게 관리합니다.

관점 지향 프로그래밍(AOP): 횡단 관심사(예: 로깅, 보안 등)를 분리할 수 있습니다.

데이터 접근: 데이터베이스 상호작용과 트랜잭션 관리를 위한 템플릿을 제공합니다.

웹 프레임워크: 웹 애플리케이션, RESTful 서비스 등을 구축하기 위한 모듈을 포함합니다.

보안: 애플리케이션을 보호하기 위한 강력한 보안 기능을 제공합니다.

 

유사점:

 

1. 프레임워크 목적: 각 도메인에서 애플리케이션 구축에 구조적인 접근 방식을 제공하여 보일러플레이트 코드를 줄이고 생산성을 높이는 것을 목표로 합니다.

2. 모듈성: 모듈성 및 확장성을 강조하여 개발자가 필요에 따라 다양한 구성 요소를 플러그인할 수 있습니다.

3. 통합: 다른 도구 및 기술과의 광범위한 통합을 지원하여 원활한 개발 워크플로우를 가능하게 합니다.

4. 커뮤니티와 생태계: 강력한 커뮤니티 지원과 풍부한 확장 및 플러그인 생태계를 가지고 있습니다.

 

차이점:

 

1. 도메인: LangChain은 언어 모델 애플리케이션에 특화되어 있고, Spring은 자바 엔터프라이즈 애플리케이션을 위한 일반 목적의 프레임워크입니다.

2. 언어 및 플랫폼: LangChain은 일반적으로 파이썬과 언어 모델 API를 사용하고, Spring은 자바 및 JVM 기반 애플리케이션과 함께 사용됩니다.

3. 범위: LangChain은 대형 언어 모델의 사용을 간소화하는 데 중점을 두고 있으며, Spring은 데이터 접근, 보안, 웹 개발 등 엔터프라이즈 애플리케이션 개발의 다양한 측면을 위한 도구를 제공합니다.

 

요약하면, LangChain과 Spring Framework는 다른 기술적 맥락에서 작동하지만, 각각의 도메인에서 애플리케이션 개발을 단순화하고 구조화하는 공통 목표를 공유합니다.

 

References

https://python.langchain.com/v0.2/docs/introduction/

 

Introduction | 🦜️🔗 LangChain

LangChain is a framework for developing applications powered by large language models (LLMs).

python.langchain.com

https://js.langchain.com/v0.2/docs/introduction/

 

Introduction | 🦜️🔗 Langchain

LangChain is a framework for developing applications powered by large language models (LLMs).

js.langchain.com

 

posted by Peter Note
2024. 6. 29. 16:14 LLM FullStacker/LangChain

"LangSmith는 LLM 애플리케이션 개발, 모니터링 및 테스트를 위한 플랫폼이다. "

 

LangSmith 추적 기능

"추적은 LLM 애플리케이션의 동작을 이해하기 위한 강력한 도구이다.". Java, Javascript 의 stackoverflow같은 단계별 call 추적 가능.

 

- 예상치 못한 최종 결과

- 에이전트가 루팅되는 이유

- 체닝이 예상보다 느린 이유

- 에이전트가 각 단계에서 사용한 토큰 수

 

LangSmith 추적 사용

- https://smith.langchain.com/ 가입

- API Key를 발급 받는다. 참조

- poetry를 사용하여 "테디노트"의 패키지를 사용한다. 

    - poetry 설정 및 사용 참조

 

[1] langchain-teddynote 패키지 설치

poetry add langchain-teddynote

 

[2] 테스트 코드에 항상 입력

from langchain_teddynote import logging

# 프로젝트 이름을 입력합니다.
logging.langsmith("CH01-Basic")

 

[3] ipynb 에서 테스트 코드 수행하면 LangSmith 사이트에 자동으로 로깅됨

  - 본인 계정으로 로그인

  - 프로젝트명을 선택

 


  - RunnableSequence는 테디노트의 https://wikidocs.net/233344 LCEL 파트의 "prompt | model | output_parser" 에 대한 Runnable 내역이 각각 기록됨. 

 

 

LangSmith는 Application Performacne Monitoring 서비스중 하나인 DataDog같은 개념으로 LLM 의 사용 성능 및 과금내역등 그외 많은 기능을 제공하고 있다. 

- https://www.datadoghq.com/

 

Cloud Monitoring as a Service | Datadog

See metrics from all of your apps, tools & services in one place with Datadog's cloud monitoring as a service solution. Try it for free.

www.datadoghq.com

 

 

다양한 LLM Trace 도구들

LLM 요청에 대한 추적, Prompt 관리, DataSet 관리등의 다양한 기능을 제공한다. 물론 LangSmith도 같은 기능을 제공한다. 

- LangFuse: https://langfuse.com/

 

Langfuse

Open source LLM engineering platform - LLM observability, metrics, evaluations, prompt management.

langfuse.com

- Pheonix: https://docs.arize.com/phoenix

 

Arize Phoenix | Phoenix

AI Observability and Evaluation

docs.arize.com

 

 

References

https://wikidocs.net/250954

 

02. LangSmith 추적 설정

## LangSmith 추적 설정하기 LangSmith는 **LLM 애플리케이션 개발, 모니터링 및 테스트** 를 위한 플랫폼입니다. 프로젝트나 LangChain 학습을 시작…

wikidocs.net

https://mobicon.tistory.com/600

 

Python Version Manager & Poetry 설치, 사용하기

Mac 기준으로 Python Version Manager 설치pyenv 설치brew install pyenv .zshrc 설정 추가# pyenv settingeval "$(pyenv init --path)"eval "$(pyenv init -)" Python Virtual Environment 설치pyenv-virtualenv 설치brew install pyenv-virtualenv .zsh

mobicon.tistory.com

https://github.com/teddylee777/langchain-teddynote

 

GitHub - teddylee777/langchain-teddynote: LangChain 을 더 쉽게 구현하기 위한 유틸 함수, 클래스를 만들어서

LangChain 을 더 쉽게 구현하기 위한 유틸 함수, 클래스를 만들어서 패키지로 배포하였습니다. - teddylee777/langchain-teddynote

github.com

posted by Peter Note
2024. 6. 17. 23:04 LLM FullStacker/Python

Mac 기준으로

 

Python Version Manager 설치

pyenv 설치

brew install pyenv

 

.zshrc 설정 추가

# pyenv setting
eval "$(pyenv init --path)"
eval "$(pyenv init -)"

 

Python Virtual Environment 설치

pyenv-virtualenv 설치

brew install pyenv-virtualenv

 

.zshrc 설정 추가

# pyenv-virtualenv setting
eval "$(pyenv virtualenv-init -)"

 

Python Version 설치

python 버전 목록 확인

pyenv install --list

 

python 특정 버전 설치

pyenv install 3.12.4

 

현재 사용버전

pyenv versions

* system (set by /Users/dowon2yun/.pyenv/version)
  2.7.14
  3.6.2
  3.6.2/envs/mms
  3.12.4
  mms --> /Users/dowon2yun/.pyenv/versions/3.6.2/envs/mms

 

Python Version 적용 

전체 사용 설정

  - pyenv global [version]

pyenv global 3.12.4
pyenv versions
  system
  2.7.14
  3.6.2
  3.6.2/envs/mms
* 3.12.4 (set by /Users/dowon2yun/.pyenv/version)

 

로컬 프로젝트만 사용 설정

  - pyenv local [version]

~ mkdir test
~ cd test
~/test pyenv local 3.6.2
~/test ls -alrt
total 8
drwxr-xr-x    3 dowon2yun  staff    96 Jun 17 23:00 .
-rw-r--r--    1 dowon2yun  staff     6 Jun 17 23:00 .python-version
drwxr-xr-x+ 128 dowon2yun  staff  4096 Jun 17 23:00 ..
~/test cat .python-version
3.6.2

 

현재 shell만 사용 설정

  - pyenv shell [version]

~/test pyenv shell 3.12.4
~/test pyenv version
3.12.4 (set by PYENV_VERSION environment variable)

 

Python Virtual Env 설정

python 패키지의 버전 격리 환경을 만들어 준다. 

 

python 버전과 virtual 환경 생성

  - pyenv virtualenv [version] [virtual-name]

~ mkdir test2
~ cd test2
~/test2 pyenv virtualenv 3.12.4 venv
~/test2 pyenv versions
  system
  2.7.14
  3.6.2
  3.6.2/envs/mms
* 3.12.4 (set by PYENV_VERSION environment variable)
  3.12.4/envs/venv
  mms --> /Users/dowon2yun/.pyenv/versions/3.6.2/envs/mms
  venv --> /Users/dowon2yun/.pyenv/versions/3.12.4/envs/venv

 

Python Virtual Env 활성화/비활성화

  - pyenv activate [virtual-name]

~/test2 pyenv activate venv
(venv) ~/test2 pyenv versions
  system
  2.7.14
  3.6.2
  3.6.2/envs/mms
  3.12.4
  3.12.4/envs/venv
  mms --> /Users/dowon2yun/.pyenv/versions/3.6.2/envs/mms
* venv --> /Users/dowon2yun/.pyenv/versions/3.12.4/envs/venv (set by PYENV_VERSION environment variable)

 

  - pyenv deactivate [virtual-name]

(venv) ~/test2 pyenv deactivate venv
~/test2 pyenv versions
  system
  2.7.14
  3.6.2
  3.6.2/envs/mms
* 3.12.4 (set by /Users/dowon2yun/.pyenv/version)
  3.12.4/envs/venv
  mms --> /Users/dowon2yun/.pyenv/versions/3.6.2/envs/mms
  venv --> /Users/dowon2yun/.pyenv/versions/3.12.4/envs/venv
~/test2

 

 

Poetry 설치

전문적인 의존성 관리 및 패키지 배포관리 툴인 poetry를 설치한다. 

https://python-poetry.org/docs/

 

pipx 를 통해 설치힌다. 

https://pipx.pypa.io/stable/installation/

brew install pipx
pipx ensurepath
sudo pipx ensurepath --global

 

pipx를 통해 poetry를 설치한다. python 3.12.3 을 사용한다는 메세지가 출력된다. 

pipx install poetry

// result message
  installed package poetry 1.8.3, installed using Python 3.12.3
  These apps are now globally available
    - poetry

 

poetry를 실행한다. 

poetry

// result mesage
Poetry (version 1.8.3)

 

Poetry 다음 Tab으로 명령 목록 보기

oh-my-zsh 설정이 .zshrc 에 있음을 가정한다

// .zshrc 에서 ZSH_CUSTOM 주석 풀고 계정 폴더 밑으로 oh-my-zsh 설정
# Would you like to use another custom folder than $ZSH/custom?
ZSH_CUSTOM=/Users/peter/oh-my-zsh

// 저장후 변경 적용
. .zshrc

// 폴더 생성 
mkdir $ZSH_CUSTOM/plugins/poetry

 

oh-my-zsh 의 plugins 에 poetry 추가 

// .zshrc oh-my-zsh의 plugins 에 poetry 추가
plugins=(git poetry)

// .zshrc 변경 적용
. .zshrc

 

다음 명령 수행

poetry completions zsh > $ZSH_CUSTOM/plugins/poetry/_poetry

 

테스트 "peotry in" 까지 입력하고 tab key를 치면 아래와 같이 init, install 등의 poetry 명령 목록이 출력된다.

$ . .zshrc
$ poetry in
init     -- Creates a basic pyproject.toml file in the current directory.
install  -- Installs the project dependencies.

 

 

Poetry 통한  프로젝트, 패키지 추가

[1] Poetry 기반 프로젝트 생성

  - poetry new [project-name]

poetry new ai-agent

 

[2] 프로젝트로 이동해서 가상환경을 프로제트내로 설정한다.

poetry config virtualenvs.in-project true

 

[3] poetry 프로젝트 가상환경으로 변경

poetry shell

 

[4] ai_agent 패키지 폴더에 __main__.py 추가

  -  폴더 지정으로 run 하기 위해 __init__.py 위치에 __main__.py 파일을 추가한다.

  - .env 파일 생성후 KEY 값 설정

  -  명령

     - poetry shell 미수행시 : poetry run python [folder-name] 또는 [file-name]

     - poetry shell 수행시 : python [folder-name]

// .env 파일
OPENAI_API_KEY=sh-proj-xsdhfdrerjelrelreahahhahahaahaha

// __main__.py 내역 
import os
from dotenv import load_dotenv

load_dotenv()

print(f"[API KEY]\n{os.environ['OPENAI_API_KEY']}")

// 실행
poetry run python ai_agent
[API KEY]
sh-proj-xsdhfdrerjelrelreahahhahahaahaha

 

[5] Python version 변경

1) pyenv install [newVersion]

2) poetry env remove [oldVersion]

3) poetry env use [newVersion]

 

References

https://python-poetry.org/docs/

 

Introduction | Documentation | Poetry - Python dependency management and packaging made easy

If you installed using the deprecated get-poetry.py script, you should remove the path it uses manually, e.g. rm -rf "${POETRY_HOME:-~/.poetry}" Also remove ~/.poetry/bin from your $PATH in your shell configuration, if it is present.

python-poetry.org

 

https://blog.flynnpark.dev/15

 

Poetry로 파이썬 의존성 관리하기

Poetry? 파이썬에서 사용하는 의존성 관리자는 몇 가지가 있습니다. 파이썬의 공식 의존성 관리자인 pip, 그리고 pip와 virtualenv를 같이 사용할 수 있는 Pipenv가 있고, 이번에 소개할 Poetry가 있습니다.

blog.flynnpark.dev

https://github.com/ohmyzsh/ohmyzsh/wiki/Plugins

 

Plugins

🙃 A delightful community-driven (with 2,300+ contributors) framework for managing your zsh configuration. Includes 300+ optional plugins (rails, git, macOS, hub, docker, homebrew, node, php, pyth...

github.com

구성파일은 TOML 파일 형식

https://www.itworld.co.kr/news/248128#:~:text=TOML(Tom's%20Obvious%20Minimal%20Language)%EC%9D%80%20%EA%B5%AC%EC%84%B1%20%EB%8D%B0%EC%9D%B4%ED%84%B0%20%EC%A0%80%EC%9E%A5%EC%9D%84,%EC%95%84%EB%8B%88%EB%8B%A4(pip%EC%97%90%EC%84%9C%20%ED%8C%A8%ED%82%A4%EC%A7%80%20%EB%B9%8C%EB%93%9C%EB%A5%BC%20%EC%9C%84%ED%95%B4%20%EC%82%AC%EC%9A%A9%EB%90%98%EB%8A%94%20pyproject.toml%20%EC%B0%B8%EC%A1%B0).

 

파이썬 앱 구성을 더 쉽게⋯TOML의 이해와 기본 활용

소프트웨어 개발에서 흥미로운 반전은 가장 간단한 의사 결정이 때로는 가장 어려운 의사 결정이 되기도 한다는 것이다. 예를 들면 애플리케이션이나

www.itworld.co.kr

https://teddylee777.github.io/poetry/poetry-tutorial/

 

poetry 의 거의 모든것 (튜토리얼)

poetry 로 가상환경을 구축하는 방법을 단계별로 설명합니다.

teddylee777.github.io

 

posted by Peter Note
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 Peter Note
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 Peter Note
2023. 1. 25. 14:57 Languages/Elixir

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 Peter Note
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 Peter Note
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 Peter Note
2021. 9. 30. 19:48 React/Architecture

React로 Login 화면을 개발한다. 

 

 

Gateway의 api server와 web dev server의 연결

Gateway를 개발환경에서 api server를 구동하고, web 화면 테스트를 위하여 web dev server가 구동하면 web dev server의 요청이 api server로 proxy 되어야 한다. 

  • apps/gateway/web/proxy.conf.json 파일을 생성한다.
  • apps/gateway/web/project.json에 "proxyConfig" 위치와 "port"는 7000 번으로 설정한다.
// project.json 일부분 

"serve": {
      "executor": "@nrwl/web:dev-server",
      "options": {
        "buildTarget": "gateway-web:build",
        "hmr": true,
        "proxyConfig": "apps/gateway/web/proxy.conf.json", <== 요기
        "port": 7000  <== 요기
      },
      ...
}

proxy.conf.json 내역

{
  "/gateway/*": {
    "target": "http://localhost:8000",
    "secure": false,
    "changeOrigin": true,
    "ws": true
  },
  "/api/gateway/*": {
    "target": "http://localhost:8000",
    "secure": false,
    "changeOrigin": true,
    "ws": true
  },
  "/api/auth/login": {
    "target": "http://localhost:8000",
    "secure": false,
    "changeOrigin": true,
    "ws": true
  },
  "/dashboard/*": {
    "target": "http://localhost:8000",
    "secure": false,
    "changeOrigin": true,
    "ws": true
  },
  "/dashboard/api/*": {
    "target": "http://localhost:8000",
    "secure": false,
    "changeOrigin": true,
    "ws": true
  },
  "/configuration/*": {
    "target": "http://localhost:8000",
    "secure": false,
    "changeOrigin": true,
    "ws": true
  },
  "/configuration/api/*": {
    "target": "http://localhost:8000",
    "secure": false,
    "changeOrigin": true,
    "ws": true
  },
  "/back-office/*": {
    "target": "http://localhost:8000",
    "secure": false,
    "changeOrigin": true,
    "ws": true
  },
  "/back-office/api/*": {
    "target": "http://localhost:8000",
    "secure": false,
    "changeOrigin": true,
    "ws": true
  }
}

 

 

Web 패키지들 설치

rnn-stack의 글을 참조한다.

  • antd components 패키지 설치: "yarn add antd @ant-design/icons"
  • React Router 설치: "yarn add react-router react-router-dom"
  • Axios 설치: "yarn add axios"

VSCode의 디버깅 실행 환경파일인 launch.json파일에 Web Dev Server를 수행할 수 있도록 설정한다. VSCode에서 실행보다는 별도 terminal 에서 수행하는 것을 권장하고, 옵션으로 사용한다.

// launch.json 일부 내역 
    {
      "type": "node",
      "request": "launch",
      "name": "Gateway API",
      "program": "${workspaceFolder}/apps/gateway/api/src/main.ts",
      "preLaunchTask": "build:gateway-api",
      "outFiles": ["${workspaceFolder}/dist/apps/gateway/api/**/*.js"]
    },
    // 하기 내역
    {
      "type": "node",
      "request": "launch",
      "name": "Gatewy Web",
      "runtimeExecutable": "npm",
      "runtimeArgs": ["run", "start:gateway-web"],
      "outFiles": ["${workspaceFolder}/dist/apps/gateway/web/**/*.js"]
    },

npm의 script로 "start:gateway-web"을 설정한다.

// package.json 일부 

  "scripts": {
    "build:gateway-api": "nx build gateway-api",
    "start:gateway-api": "nx serve gateway-api",
    "build:gateway-web": "nx build gateway-web",
    "start:gateway-web": "nx serve gateway-web",
    ....
  },

이제 VS Code에서 Gateway web을 실행하고, http://localhost:7000/ 호출한다.

Gateway Web Dev Server 기동

 

 

Login 화면 저작

nx 명령을 이용해 login 컴포넌트를 생성한다.

$> nx g @nrwl/react:component login --project=gateway-web

생성된 파일들

login.tsx 내역

// login.tsx
import { Row, Col, Form, Input, Button } from 'antd';
import { httpService } from '@rnm/ui';
import { LoginDto } from '@rnm/model';
import styles from './login.module.scss';

export function Login() {
  const onFinish = (user: LoginDto) => {
    httpService.post<LoginDto>('/api/auth/login', user).subscribe((result: LoginDto) => {
      console.log('Success:', result);
      // redirect dashboard
      location.href = '/dashboard';
    });
  };

  const onFinishFailed = (errorInfo: any) => {
    console.log('Failed:', errorInfo);
  };

  return (
    <div className={styles.login_container}>
      <div className={styles.center_bg}>
        <Row justify="center" align="middle" className={styles.form_container}>
          <Col span={16} offset={6}>
            <Form
              name="basic"
              layout="vertical"
              labelCol={{ span: 16 }}
              wrapperCol={{ span: 16 }}
              initialValues={{ remember: true }}
              onFinish={onFinish}
              onFinishFailed={onFinishFailed}
              autoComplete="off"
            >
              <Form.Item
                label="Username"
                name="username"
                rules={[{ required: true, message: 'Please input your username!' }]}
              >
                <Input />
              </Form.Item>

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

              <Form.Item wrapperCol={{ span: 16 }}>
                <Button type="primary" htmlType="submit" block>
                  Submit
                </Button>
              </Form.Item>
            </Form>
          </Col>
        </Row>
      </div>
    </div>
  );
}

export default Login;

 

 

Gateway API 서버 호출하기

Web 에서 사용할 라이브러리는 분리하고, API와 Web이 공용하는 부분은 Model에만 국한한다. 따라서 @rnm/model 패키지를 생성한다. 

$> nx g @nrwl/react:lib model --publishable --importPath=@rnm/model

domain/entities/user/user.model.ts 을 libs/model/src/lib/user/user.model.ts 로 copy하고, domain에서는 삭제한다. 그리고 user.model사용하는 클래스를 일괄 수정한다. 

// user.model.ts
export interface User {
  id?: number;
  username: string;
  password?: string;
  email?: string;
  firstName?: string;
  lastName?: string;
  role?: string;
  sub?: string | number;
  currentHashedRefreshToken?: string;
}
export type LoginDto = Pick<User, 'username' | 'password'>;
export type TokenPayload = Omit<User, 'password'>;

export enum UserRole {
  ADMIN = 'ADMIN',
  MANAGER = 'MANAGER',
  CUSTOMER = 'CUSTOMER',
  GUEST = 'GUEST',
}

 

다음으로 libs/ui/src/lib/ajax/http.service.ts 파일을 생성하고, http.service.ts 코드에, error처리 notification을 추가한다.

// http.service.ts 일부
  import { notification } from 'antd';
  
  ...
  private executeRequest<T>(args: RequestArgs): Observable<T> {
    const { method, url, queryParams, payload } = args;
    let request: AxiosPromise<T>;
    switch (method) {
      case HttpMethod.GET:
        request = this.httpClient.get<T>(url, { params: queryParams });
        break;
      case HttpMethod.POST:
        request = this.httpClient.post<T>(url, payload);
        break;
      case HttpMethod.PUT:
        request = this.httpClient.put<T>(url, payload);
        break;
      case HttpMethod.PATCH:
        request = this.httpClient.patch<T>(url, payload);
        break;
      case HttpMethod.DELETE:
        request = this.httpClient.delete<T>(url);
        break;
    }

    return new Observable<T>((observer: Observer<T>) => {
      request
        .then((response: AxiosResponse) => {
          observer.next(response.data);
        })
        .catch((error: AxiosError | Error) => {
          this.abort(true);
          if (axios.isAxiosError(error)) {
            if (error.response) {
              // NestJS의 global-exception.filter.ts의 포멧
              const data: any = error.response?.data || {};
              this.showNotification(`[${data.statusCode}] ${data.error}`, data.message);
              console.log(`[${data.statusCode}] ${data.error}: ${data.message}`);
            }
          } else {
            this.showNotification('Unknow Error', error.message);
            console.log(error.message);
          }
        })
        .finally(() => {
          this.completed = true;
          observer.complete();
        });
      return () => this.abort();
    });
  }

  private showNotification(message: string, description: string): void {
    notification.error({
      message,
      description
    });
  }
}

에러처리는 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

라이브러 생성 폴더 구조

 

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

 

Release ms-9 · ysyun/rnm-stack

[ms-9] added login component and enhanced ajax error notification

github.com

 

 

<참조>

- React 라이브러리 환경 구성

https://mobicon.tistory.com/580

 

[React HH-3] 라이브러리 설정 - Axios, RxJS

React 외에 애플리케이션 개발을 위한 라이브러리를 설치한다. UI Components PrimeReact, EUI, MaterialUI, AntD 검토후 소스레벨 최신으로 반영하고 있고, 다양한 비즈니스 UX 대응 가능한 AntD를 선택한다. //..

mobicon.tistory.com

- location.href와 location.replace 차이점

https://opentutorials.org/module/2919/22904

 

location.href 와 location.replace 차이점 - JavaScript Tips

[출처] [자바스크립트] location.href 와 location.replace 의 차이점.|작성자 왕따짱 location.href location.replace   기능 새로운 페이지로 이동된다. 기존페이지를 새로운 페이지로 변경시킨다.   형태 속

opentutorials.org

 

posted by Peter Note
2021. 9. 30. 13:22 React/Architecture

 

NestJS에서 제공하는 Auth와 Role 기능을 확장해 본다. NestJS는 그외 Configuration, Logging, Filter, Interceptor등 다양한 기능을 확장하여 적용할 수 있도록 한다. 

 

 

Role 데코레이터 추가

Role 체크를 위한 데코레이터를 libs/shared/src/lib/decorator/roles.decorator.ts 를 추가한다.

// roles.decorator.ts
import { SetMetadata } from '@nestjs/common';

export const Roles = (...roles: string[]) => SetMetadata('roles', roles);

 

 

Role 가드 추가

request시에 user 정보의 role을 통해 match되는지를 체크하는 가드(guard)를 libs/shared/src/lib/guard/role.guard.ts 추가한다. 

  • 요구하는 roles가 없으면 bypass 한다.
  • user가 없다면 즉, 로그인한 사용자가 아니거나, Login Token이 없다면 Forbidden 에러를 발생시킨다.
// role.guard.ts
import { Injectable, CanActivate, ExecutionContext, ForbiddenException } from '@nestjs/common';
import { Reflector } from '@nestjs/core';

const matchRoles = (roles: string[], userRoles: string) => {
  return roles.some(role => role === userRoles);
};

@Injectable()
export class RolesGuard implements CanActivate {
  constructor(private reflector: Reflector) { }

  canActivate(context: ExecutionContext): boolean {
    const requiredRoles = this.reflector.getAllAndOverride<string[]>('roles', [
      context.getHandler(),
      context.getClass(),
    ]);
    if (!requiredRoles) {
      return true;
    }

    const req = context.switchToHttp().getRequest() as any;
    const user = req.user;
    if (!user) {
      throw new ForbiddenException('User does not exist');
    }
    return matchRoles(requiredRoles, user.role);
  }
}

 

로그인후 express의 request에 user 객체 할당

로그인을 하면 사용자 정보가 Token에 담긴다. @Role 데코레이터를 체크하기 전에 Token 정보를 기반으로 user 정보를 추출한다. 

  • 로그인 토큰: LOGIN_TOKEN

libs/domain/src/lib/auth/auth.middleware.ts 파일을 생성하고, 쿠키의 LOGIN_TOKEN에서 user정보를 얻는다.

import { ForbiddenException, Injectable, NestMiddleware } from '@nestjs/common';
import { Request, Response, NextFunction } from 'express';
import { verify } from 'jsonwebtoken';

import { loadConfigJson } from '@rnm/shared';
const config: any = loadConfigJson();

@Injectable()
export class AuthMiddleware implements NestMiddleware {
  use(req: Request, res: Response, next: NextFunction) {
    if (req.user) {
      next();
      return;
    }
    
    const accessToken = req?.cookies?.LOGIN_TOKEN;
    let user;
    try {
      user = verify(
        accessToken,
        config?.AUTH?.SECRET,
      );
    } catch (error) {
      throw new ForbiddenException('Please register or sign in.');
    }

    if (user) {
      req.user = user;
    }
    next();
  }
}

request에 user를 할당하는 미들웨어와 Role Guard를 apps/gateway/api/src/app/app.module.ts 에 설정한다. 

  • RolesGuard 등록
  • AuthMiddleware path들 등록
// app.module.ts
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 } from '@rnm/shared';

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)
    }),
    EntitiesModule,
    // MicroService
    DashboardModule,
    ConfigurationModule,
    BackOfficeModule,
    // Auth
    AuthModule
  ],
  controllers: [
    AuthController,
    AppController,
    UserController
  ],
  providers: [
    GatewayApiAppService,
    // Global Exception Filter
    {
      provide: APP_FILTER,
      useClass: GlobalExceptionFilter,
    },
    // 1) Role Guard 등록
    {
      provide: APP_GUARD,
      useClass: RolesGuard,
    },
  ]
})
export class AppModule implements NestModule {
  // 2) Auth Middleware 등록
  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 },
      ]);
  }
}

 

 

Role 사용하기

user 테이블에 Role이 저장되어있다. 

user.model.ts 소스에 UserRole enum을 추가한다. 

// user.model.ts
export interface User {
  id?: number;
  username: string;
  password?: string;
  email?: string;
  firstName?: string;
  lastName?: string;
  role?: string;
  sub?: string | number;
  currentHashedRefreshToken?: string;
}
export type LoginDto = Pick<User, 'username' | 'password'>;
export type TokenPayload = Omit<User, 'password'>;

// User Role
export enum UserRole {
  ADMIN = 'ADMIN',
  MANAGER = 'MANAGER',
  CUSTOMER = 'CUSTOMER',
  GUEST = 'GUEST',
}

apps/gateway/api/src/app/user/user.controller.ts 안에 @Roles을 적용한다. 

// user.controller.ts
import { Body, Controller, Delete, Get, Param, Post, Put, UseGuards } from '@nestjs/common';

import { JwtAuthGuard, UserService } from '@rnm/domain';
import { User, UserRole } from '@rnm/model';
import { Roles } from '@rnm/shared';

@Controller('api/gateway/user')
export class UserController {
  constructor(
    private readonly service: UserService
  ) { }

  @UseGuards(JwtAuthGuard)
  @Post()
  @Roles(UserRole.ADMIN, UserRole.MANAGER) // <== 요기
  async create(@Body() data: User): Promise<User> {
    const savedUser = await this.service.create(data);
    if (!savedUser) {
      return;
    }
    return savedUser;
  }
  ....
 }

 

이후 열심히 사용해 보자.

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

 

Release ms-8 · ysyun/rnm-stack

[ms-8] added role guard for authorization

github.com

 

 

<참조>

- NestJS Authorization: https://docs.nestjs.kr/security/authorization

 

네스트JS 한국어 매뉴얼 사이트

네스트JS 한국, 네스트JS Korea 한국어 매뉴얼

docs.nestjs.kr

- JWT Role based authentication: https://github.com/rangle/jwt-role-based-authentication-examples

 

GitHub - rangle/jwt-role-based-authentication-examples: Implement the same backend using graphql, nestjs and deno.

Implement the same backend using graphql, nestjs and deno. - GitHub - rangle/jwt-role-based-authentication-examples: Implement the same backend using graphql, nestjs and deno.

github.com

 

posted by Peter Note
2021. 9. 30. 13:22 React/Architecture

Login Auth Token이 만료되었을 때 Refresh Token을 통하여 다시 Auth Token을 생성토록한다. 

  • Refresh Token을 서버에 저장한다. 다른 기기에서 로그인하면 기존 로그인 기기의 Refresh Token과 비교하여 틀리므로 여러 기기의 로그인을 방지한다. 
  • 서버에 여러개의 Refresh Token을 저장할 수 있다면 기기 제한을 할 수 있겠다. 또한 변조된 refresh token의 사용을 막을 수 있다.

 

User Entity 업데이트

libs/domain/src/lib/entities/user/user.entity.ts 에 refreshToken 컬럼을 추가한다. 

import { Entity, Column, PrimaryGeneratedColumn, CreateDateColumn } from 'typeorm';

@Entity('user_iot')
export class UserEntity {
  @PrimaryGeneratedColumn()
  id!: number;

  @Column({ unique: true, length: 50 })
  username!: string;

  @Column()
  password!: string;

  @Column({ length: 255 })
  email!: string;

  @Column({ name: 'first_name', length: 100 })
  firstName!: string;

  @Column({ name: 'last_name', length: 100 })
  lastName!: string;

  @Column({ default: 'GUEST' })
  role!: string;

  @CreateDateColumn({ name: 'created_at', select: false })
  createdAt?: Date;

  @CreateDateColumn({ name: 'updated_at', select: false })
  updatedAt?: Date;

  // refresh token 저장
  @Column({
    name: 'current_hashed_refresh_token',
    nullable: true
  })
  currentHashedRefreshToken?: string;

}

 

 

User Service에 refreshToken 매칭

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 에 추가한다. 

// auth.service.ts 일부
  getCookieWithJwtAccessToken(payload: TokenPayload, hasAuthorization = false) {
    const token = this.jwtService.sign(payload, {
      secret: config?.AUTH?.SECRET || 'iot_app',
      expiresIn: config?.AUTH?.EXPIRED_ON || '1d'
    });
    if (hasAuthorization) {
      return [`LOGIN_TOKEN=${token}; HttpOnly; Path=/; Max-Age=${config?.AUTH?.EXPIRED_ON}`, `Authorization=${token}; HttpOnly; Path=/; Max-Age=${config?.AUTH?.EXPIRED_ON}`];
    } else {
      return [`LOGIN_TOKEN=${token}; HttpOnly; Path=/; Max-Age=${config?.AUTH?.EXPIRED_ON}`];
    }
  }

  getCookieWithJwtRefreshToken(payload: TokenPayload) {
    const token = this.jwtService.sign(payload, {
      secret: config?.AUTH?.REFRESH_SECRET || 'io_app_refresh',
      expiresIn: config?.AUTH?.REFRESH_EXPIRED_ON || '7d'
    });
    const cookie = `REFRESH_LOGIN_TOKEN=${token}; HttpOnly; Path=/; Max-Age=${config?.AUTH?.REFRESH_EXPIRED_ON}`;
    return {
      cookie,
      token
    }
  }

  getCookiesForLogOut() {
    return [
      'LOGIN_TOKEN=; HttpOnly; Path=/; Max-Age=0',
      'REFRESH_LOGIN_TOKEN=; HttpOnly; Path=/; Max-Age=0'
    ];
  }

로apps/gateway/api/src/app/auth/auth.controller.ts 안에 하기 로직을 추가한다.

  • 로그인 했을 때 해당 Cookie를 등록한다. 
  • 로그아웃할 때 해당 Cookie 내용을 삭제한다. 
  • Auth Token (Forbidden)오류 발생시 RefreshToken을 통해 Auth Token 재생성한다. 
import { Controller, Get, HttpCode, Post, Req, Res, UnauthorizedException, UseGuards } from '@nestjs/common';

import { AuthService, JwtAuthGuard, LocalAuthGuard, JwtRefreshGuard, RequestWithUser, UserService } from '@rnm/domain';
import { TokenPayload } from '@rnm/model';

@Controller('api/auth')
export class AuthController {
  constructor(
    private readonly authService: AuthService,
    private readonly userService: UserService
  ) { }

  @UseGuards(LocalAuthGuard)
  @Post('login')
  async login(@Req() req: RequestWithUser): Promise<any> {
    const { user } = req;
    if (user) {
      const payload: TokenPayload = { username: user.username, sub: user.id, email: user.email, role: user.role };
      const accessTokenCookie = this.authService.getCookieWithJwtAccessToken(payload);
      const {
        cookie: refreshTokenCookie,
        token: refreshToken
      } = this.authService.getCookieWithJwtRefreshToken(payload);
      const loginUsernameCookie = this.authService.getCookieWithLoginUsername(payload);

      await this.userService.setCurrentRefreshToken(refreshToken, user.id);

      // 반드시 req.res로 쿠키를 설정
      req.res.setHeader('Set-Cookie', [...accessTokenCookie, refreshTokenCookie, loginUsernameCookie]);
      return {
        payload,
        accessTokenCookie,
        refreshTokenCookie
      };
    } else {
      throw new UnauthorizedException({
        error: 'User does not exist'
      });
    }
  }

  @UseGuards(JwtAuthGuard)
  @Post('logout')
  @HttpCode(200)
  async logout(@Req() req: RequestWithUser, @Res() res): Promise<any> {
    await this.userService.removeRefreshToken(req.user.id);
    res.setHeader('Set-Cookie', this.authService.getCookiesForLogOut());
  }

  @UseGuards(JwtAuthGuard)
  @Get()
  authenticate(@Req() req: RequestWithUser) {
    const user = req.user;
    return user;
  }

  // Refresh Guard를 적용한다.
  @UseGuards(JwtRefreshGuard)
  @Get('refresh')
  refresh(@Req() request: RequestWithUser) {
    const accessTokenCookie = this.authService.getCookieWithJwtAccessToken(request.user);

    request.res.setHeader('Set-Cookie', accessTokenCookie);
    return request.user;
  }
}

 

 

테스트하기 

Postman으로 테스트를 하면 accessTokenCookie가 나온다. 

로그인 결과값

accessTokenCookie를 복사하여 다른 명령 전송시에 Headers에 Cookie를 등록하여 사용한다. 

복사한 accessTokenCookie 사용

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

 

Release ms-7 · ysyun/rnm-stack

[ms-8] added role guard for authorization

github.com

 

 

<참조>

- Refresh Token 만들기
   소스: https://github.com/mwanago/nestjs-typescript

   문서: https://wanago.io/2020/09/21/api-nestjs-refresh-tokens-jwt/

 

API with NestJS #13. Implementing refresh tokens using JWT

It leaves quite a bit of room for improvement. In this article, we look into refresh tokens.

wanago.io

 

posted by Peter Note
2021. 9. 27. 18:04 React/Architecture

Micro Service의 앞단 Gateway에서 모든 호출에 대한 인증/인가를 처리한다. 먼저 JWT기반 인증에 대한 설정을 한다. 

  • passwort jwt 설정

 

JWT 처리를 위한  패키지 설치

passport를 통해 JWT를 관리한다.

  • userId/password 기반은 passport-local을 사용
  • JWT 체크 passport-jwt 사용
  • 추가적으로 http security를 위해 express middleware인 helmet 적용
$> yarn add @nestjs/jwt @nestjs/passport passport passport-local passport-jwt helmet
$> yarn add -D @types/passport-local @types/passport-jwt @types/express

 

Passport Local 적용

  • libs/domain/src/lib/auth/strategies 폴더 생성
  • local.strategy.ts 파일 생성
// local.strategy.ts
import { Injectable, UnauthorizedException } from '@nestjs/common';
import { PassportStrategy } from '@nestjs/passport';
import { Strategy } from 'passport-local';
import { AuthService } from '../auth.service';

@Injectable()
export class LocalStrategy extends PassportStrategy(Strategy) {
  constructor(private readonly authService: AuthService) {
    super();
  }

  async validate(username: string, password: string): Promise<any> {
    const user = await this.authService.validateUser(username, password);
    if (!user) {
      throw new UnauthorizedException({
        error: 'Incorrect username and password'
      });
    }
    return user;
  }
}

 

Username/Password 체크하기

libs/domain/src/lib/auth/  폴더 생성하고, auth.service.ts 파일을 생성한다. 

  • validateUser: LocalStrategy에서 호출한다. username/password 로그인 유효성을 login 호출 이전에 체크한다.
  • login: validate user인 경우 사용자 정보를 통한 webtoken 생성
// auth.service.ts
import { Injectable, UnauthorizedException } from '@nestjs/common';
import { JwtService } from '@nestjs/jwt';

import { User } from '../entities/user/user.model';
import { UserService } from '../entities/user/user.service';
import { bcryptCompare } from '../utilties/bcrypt.util';

@Injectable()
export class AuthService {
  constructor(
    private readonly userService: UserService,
    private readonly jwtService: JwtService,
  ) { }

  async validateUser(username: string, pass: string): Promise<any> {
    const user: any = await this.userService.findOne(username) || {};
    if (user && pass === user.password) {
      const { password, ...result } = user;
      return result;
    }
    return null;
  }

  async login(loginUser: User): Promise<any> {
    const user = await this.userService.findOne(loginUser.username);
    if (user) {
      const payload = { username: user.username, sub: user.id, email: user.email, role: user.role };
      return {
        access_token: this.jwtService.sign(payload),
      };
    } else {
      throw new UnauthorizedException({
        error: 'There is no user'
      });
    }
  }
}

토큰 생성확인은 https://jwt.io/ 에서 할 수 있다. 

 

libs/domain/src/lib/auth/auth.module.ts 파일을 생성하고, config.json파일에 AUTH 프로퍼티를 추가한다. 

  • JwtModule을 등록한다. 
  • secret은 반드시 별도의 환경설정 파일에서 관리한다. 
  • AuthService도 등록한다.
  • User 정보를 read하기 위해 EntitiesModule도 imports 에 설정한다.
// apps/gateway/api/src/environments/config.json
{
  "HTTP_PORT": 8000,
  "AUTH": {
    "SECRET": "iot_secret_auth",
    "EXPIRED_ON": "1d"
  },
  ...
}

// auth.module.ts
import { Module } from '@nestjs/common';
import { JwtModule } from '@nestjs/jwt';
import { PassportModule } from '@nestjs/passport';

import { GatewayConfiguration, loadConfigJson } from '@rnm/shared';
import { EntitiesModule } from '../entities/entity.module';

import { AuthService } from './auth.service';
import { JwtStrategy } from './strategies/jwt.strategy';
import { LocalStrategy } from './strategies/local.strategy';

const config: GatewayConfiguration = loadConfigJson();

@Module({
  imports: [
    PassportModule,
    JwtModule.register({
      secret: config.AUTH?.SECRET,
      signOptions: { expiresIn: config.AUTH?.EXPIRED_ON },
    }),
    EntitiesModule
  ],
  providers: [AuthService, LocalStrategy, JwtStrategy],
  exports: [AuthService],
})
export class AuthModule { }

 

Passport JWT 적용

로그인이 성공하면 jsonwebtoken 을 생성하고, 이후 request(요청)에 대해 JWT를 체크하는 환경설정을 한다.

  • libs/domain/src/lib/auth/strategies/jwt.strategy.ts 파일  생성
    • request header의 Bearer Token 체크 => Cookie 사용으로 변경 (master branch소스 참조)
    • 확인하는 secret 설정
  • AuthModule에 등록한다.
// jwt.strategy.ts
import { Injectable } from '@nestjs/common';
import { PassportStrategy } from '@nestjs/passport';
import { ExtractJwt, Strategy } from 'passport-jwt';
import { Request } from 'express';

import { loadConfigJson } from '@rnm/shared';
const config: any = loadConfigJson();

@Injectable()
export class JwtStrategy extends PassportStrategy(Strategy) {
  constructor() {
    super({
      // jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
      // Cookie를 사용한다
      jwtFromRequest: ExtractJwt.fromExtractors([(request: Request) => {
        return request?.cookies?.LOGIN_TOKEN;
      }]),
      ignoreExpiration: false,
      secretOrKey: config?.AUTH?.SECRET
    });
  }

  async validate(user: any): Promise<any> {
    // return { id: payload.sub, username: payload.username };
    return user;
  }
}

 

Password 암호화

암호화 모듈 설치

$> yarn add bcrypt
$> yarn add -D @types/bcrypt

암호화 유틸리티를 생성한다. libs/domain/src/lib/utilties/bcrypt.util.ts 파일 생성

  • 사용자 생성시 패스워드
  • 입력 패스워드를 DB의 암호화된 패스워드와 비교한다.
import * as bcrypt from 'bcrypt';

// 사용자의 패스워드 암호화
export const bcryptHash = (plainText: string, saltOrRounds = 10): Promise<string> => {
  return bcrypt.hash(plainText, saltOrRounds);
}

// 입력 패스워드와 DB 패스워드 비교
export const bcryptCompare = (plainText: string, hashedMessage: string): Promise<boolean> => {
  return bcrypt.compare(plainText, hashedMessage);
}

libs/domain/src/lib/auth/auth.service.ts 의 validateUser에서 암호화된 password를 체크토록 수정한다.

// auth.service.ts
import { bcryptCompare } from '../utilties/bcrypt.util';

@Injectable()
export class AuthService {
  constructor(
    private readonly userService: UserService,
    private readonly jwtService: JwtService,
  ) { }

  async validateUser(username: string, pass: string): Promise<any> {
    const user: any = await this.userService.findOne(username) || {};
    // 암호화된 패스워드를 입력 패스워드와 같은지 비교
    const isMatch = await bcryptCompare(pass, user.password);
    if (user && isMatch) {
      const { password, ...result } = user;
      return result;
    }
    return null;
  }
  ...
}

 

 

로그인 하기

  • apps/gateway/api/src/app/auth/auth.controller.ts 파일을 신규 생성.
  • apps/gateway/api/src/app/app.module.ts 설정
    • "auth/login" API에 대해 static server에 exclude 설정
    • AuthModule imports에 설정
    • AuthController 등록
// auth.controller.ts
import { Body, Controller, Post, UseGuards } from '@nestjs/common';

import { AuthService, LocalAuthGuard, User } from '@rnm/domain';

@Controller()
export class AuthController {
  constructor(
    private readonly authService: AuthService
  ) { }

  @UseGuards(LocalAuthGuard)
  @Post('auth/login')
  async login(@Body() user: User): Promise<Response> {
    return this.authService.login(user);
  }
}

// app.module.ts
@Module({
  imports: [
    ServeStaticModule.forRoot({
      rootPath: join(__dirname, 'public'),
      exclude: [
        '/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)
    }),
    EntitiesModule,
    // MicroService
    DashboardModule,
    // Auth
    AuthModule
  ],
  controllers: [
    AppController,
    AuthController,
    UserController
  ],
  providers: [GatewayApiAppService]
})
export class AppModule { }

 

UseGuard에서 username/password는  LocalAuthGuard를 등록한다. 이를 위해 libs/domain/src/lib/auth/guards/local-auth.guard.ts 파일 생성한다.

import { Body, Controller, Delete, Get, Param, Post, Put, UseGuards } from '@nestjs/common';

import { JwtAuthGuard, User, UserService } from '@rnm/domain';

@Controller('api/gateway/user')
export class UserController {
  constructor(
    private readonly service: UserService
  ) { }

  @UseGuards(JwtAuthGuard)
  @Post()
  create(@Body() data: User): Promise<User> {
    return this.service.create(data);
  }

  @UseGuards(JwtAuthGuard)
  @Put(':id')
  updateOne(@Param('id') id: number, @Body() data: User): Promise<any> {
    return this.service.updateOne(id, data);
  }

  @UseGuards(JwtAuthGuard)
  @Get()
  findAll(): Promise<User[]> {
    return this.service.findAll();
  }

  @UseGuards(JwtAuthGuard)
  @Get(':username')
  findOne(@Param('username') username: string): Promise<User | undefined> {
    return this.service.findOne(username);
  }

  @UseGuards(JwtAuthGuard)
  @Delete(':id')
  deleteOne(@Param('id') id: string): Promise<any> {
    return this.service.deleteOne(id);
  }
}
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"로 등록한다. 

import { Body, Controller, Delete, Get, Param, Post, Put, UseGuards } from '@nestjs/common';

import { JwtAuthGuard, User, UserService } from '@rnm/domain';

@Controller('api/gateway/user')
export class UserController {
  constructor(
    private readonly service: UserService
  ) { }

  @UseGuards(JwtAuthGuard)
  @Post()
  create(@Body() data: User): Promise<User> {
    return this.service.create(data);
  }

  @UseGuards(JwtAuthGuard)
  @Put(':id')
  updateOne(@Param('id') id: number, @Body() data: User): Promise<any> {
    return this.service.updateOne(id, data);
  }

  @UseGuards(JwtAuthGuard)
  @Get()
  findAll(): Promise<User[]> {
    return this.service.findAll();
  }

  @UseGuards(JwtAuthGuard)
  @Get(':username')
  findOne(@Param('username') username: string): Promise<User | undefined> {
    return this.service.findOne(username);
  }

  @UseGuards(JwtAuthGuard)
  @Delete(':id')
  deleteOne(@Param('id') id: string): Promise<any> {
    return this.service.deleteOne(id);
  }
}

Postman으로 테스트 한다. 

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

 

Release ms-6 · ysyun/rnm-stack

[ms-6] add typeorm and jwt for auth

github.com

주의: 소스가 계속 업데이트되고 있기에 master branch를 참조해도 된다.

 

 

<참조>

- NestJS에 passport 기반 JWT 적용하기 

https://docs.nestjs.kr/security/authentication

 

네스트JS 한국어 매뉴얼 사이트

네스트JS 한국, 네스트JS Korea 한국어 매뉴얼

docs.nestjs.kr

- passport local 환경설정

http://www.passportjs.org/packages/passport-local/

 

passport-local

Local username and password authentication strategy for Passport.

www.passportjs.org

- passport-jwt 환경설정

https://www.passportjs.org/packages/passport-jwt/

 

passport-jwt

Passport authentication strategy using JSON Web Tokens

www.passportjs.org

- password 암호화

https://wanago.io/2020/05/25/api-nestjs-authenticating-users-bcrypt-passport-jwt-cookies/

 

API with NestJS #3. Authenticating users with bcrypt, Passport, JWT, and cookies

1. API with NestJS #1. Controllers, routing and the module structure2. API with NestJS #2. Setting up a PostgreSQL database with TypeORM3. API with NestJS #3. Authenticating users with bcrypt, Passport, JWT, and cookies4. API with NestJS #4. Error handling

wanago.io

posted by Peter Note
2021. 9. 27. 16:23 React/Architecture

Micro Service들이 멀티 Database를 사용할 경우 또는 Database Schema에 대한 주도권이 없으며 단지 연결하여 사용하는 입장의 Frontend Stack 개발자일 경우 Prisma보다는 TypeORM을 사용하는 것이 좋아보인다. 

 

 

TypeORM 설치 및 환경설정

nestjs 패키지와 typeorm 그리고 postgresql 패키지를 설치한다. 

$> yarn add @nestjs/typeorm typeorm pg

.env를 읽는 방식이 아니라 별도의 configuration json 파일에서 환경설정토록 한다. 

apps/gateway/api/src/environments/ 폴더에 orm-config.json 과 orm-config.prod.json 파일을 생성한다. 

  • synchronized는 반드시 개발시에만 true로 사용한다.
// orm-config.json
{
  "HOST": "localhost",
  "PORT": 5432,
  "USER": "iot",
  "PASSWORD": "1",
  "DATABASE": "rnm-stack",
  "ENTITIES": ["libs/domain/src/lib/entities/**/*.entity.ts"],
  "MODE": "dev",
  "SYNC": true
}

// Production 환경에서 사용
// orm-config.prod.json
{
  "HOST": "localhost",
  "PORT": 5432,
  "USER": "iot",
  "PASSWORD": "1",
  "DATABASE": "rnm-stack",
  "ENTITIES": ["libs/domain/src/lib/entities/**/*.entity.ts"],
  "MODE": "production",
  "SYNC": false
}

dev와 prod간의 config 스위칭을 위하여 apps/gateway/api/project.json 안에 replacement  문구를 추가한다. 

// project.json 일부내역
"fileReplacements": [
{
  "replace": "apps/gateway/api/src/environments/environment.ts",
  "with": "apps/gateway/api/src/environments/environment.prod.ts"
},
{
  "replace": "apps/gateway/api/src/environments/config.ts",
  "with": "apps/gateway/api/src/environments/config.prod.ts"
},
{
  "replace": "apps/gateway/api/src/environments/orm-config.ts",
  "with": "apps/gateway/api/src/environments/orm-config.prod.ts"
}
]

 

libs/shared/src/lib/configuration/ 폴더에 orm-config.service.ts 파일을 생성하고, orm-config.json 파일을 값을 다루도록 한다.

// orm-config.service.ts
import { TypeOrmModuleOptions } from '@nestjs/typeorm';
import { loadOrmConfiguration } from './config.service';

class OrmConfigService {
  constructor(private env: { [k: string]: any }) { }

  ensureValues(keys: string[]) {
    keys.forEach(k => this.getValue(k, true));
    return this;
  }

  getPort() {
    return this.getValue('PORT', true);
  }

  isProduction() {
    const mode = this.getValue('MODE', false);
    return mode !== 'dev';
  }

  getTypeOrmConfig(): TypeOrmModuleOptions {
    const config: TypeOrmModuleOptions = {
      type: 'postgres',
      host: this.getValue('HOST'),
      port: parseInt(this.getValue('PORT')),
      username: this.getValue('USER'),
      password: this.getValue('PASSWORD'),
      database: this.getValue('DATABASE'),
      entities: this.getValue('ENTITIES'),
      synchronize: this.getValue('SYNC'),
    };
    return config;
  }

  private getValue(key: string, throwOnMissing = true): any {
    const value = this.env[key];
    if (!value && throwOnMissing) {
      throw new Error(`config error - missing orm-config.${key}`);
    }
    return value;
  }

}

/**
 * Singleton Config
 */
const ormEnv: any = loadOrmConfiguration();
const ormConfigService = new OrmConfigService(ormEnv)
  .ensureValues([
    'HOST',
    'PORT',
    'USER',
    'PASSWORD',
    'DATABASE'
  ]);

export { ormConfigService };

apps/gateway/api/src/app/app.module.ts 에서 해당 configuration을 설정토록한다. @nestjs/typeorm의 모듈을 사용한다.

// app.module.ts
import { join } from 'path';
import { Module } from '@nestjs/common';
import { ServeStaticModule } from '@nestjs/serve-static';
import { TypeOrmModule } from '@nestjs/typeorm';
import { getMetadataArgsStorage } from 'typeorm';

import { GatewayApiAppService, EntitiesModule } from '@rnm/domain';
import { ormConfigService } from '@rnm/shared';

import { DashboardModule } from './dashboard/microservice/dashboard.module';
import { AppController } from './app.controller';
import { UserController } from './user/user.controller';

@Module({
  imports: [
    ServeStaticModule.forRoot({
      rootPath: join(__dirname, 'public'),
      exclude: [
        '/api/gateway*', '/api/dashboard*', '/api/configuration*', '/api/back-office*',
        '/dashboard*', '/configuration*', '/back-office*'
      ],
    }),
    // ORM 환경을 설정한다. orm-config.json에 설정했던 entities의 내용을 등록한다. 
    TypeOrmModule.forRoot({
      ...ormConfigService.getTypeOrmConfig(),
      entities: getMetadataArgsStorage().tables.map(tbl => tbl.target)
    }),
    EntitiesModule,
    // MicroService
    DashboardModule,
  ],
  controllers: [
    AppController,
    UserController
  ],
  providers: [GatewayApiAppService]
})
export class AppModule { }

 

 

TypeORM사용 패턴

typeorm은 두가지 패턴을 선택적으로 사용할 수 있다.

  • Active Record: BeanEntity를 상속받아 entity내에서 CRUD 하기. (작은 서비스유리)
  • Data Mapper: Model은 별도이고, Respository가 DB와 연결하고, CRUD를 별도 서비스로 만든다. (큰 서비스유리)

Data Mapper 패턴을 사용하기 위해 libs/domain/src/lib/entities/user/ 폴더하위에 user.entity.ts, user.model, user.service.ts 파일을 생성한다. 

  • user.entity.ts: table schema
  • user.model.ts: interface DTO
  • user.service.ts: CRUD 
// user.entity.ts
import { Entity, Column, PrimaryGeneratedColumn } from 'typeorm';

@Entity('user_iot')
export class UserEntity {
  @PrimaryGeneratedColumn()
  id!: number;

  @Column()
  username!: string;

  @Column()
  password!: string;

  @Column()
  email!: string;

  @Column()
  firstName!: string;

  @Column()
  lastName!: string;

  @Column({ default: false })
  isActive!: boolean;

  // USER, ADMIN, SUPER
  @Column({ default: 'USER' })
  role!: string;
}


// user.model.ts
export interface User {
  id: number;
  username: string;
  password: string;
  email?: string;
  firstName: string;
  lastName: string;
  isActive: boolean;
  role: string;
}


// user.service.ts
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';

import { UserEntity } from './user.entity';
import { User } from './user.model';

@Injectable()
export class UserService {
  constructor(
    @InjectRepository(UserEntity) private repository: Repository<UserEntity>
  ) { }

  create(data: User): Promise<User> {
    return this.repository.save(data);
  }

  updateOne(id: number, data: User): Promise<any> {
    return this.repository.update(id, data);
  }

  findAll(): Promise<User[]> {
    return this.repository.find();
  }

  findOne(username: string): Promise<User | undefined> {
    // findOne이 객체라는 것에 주의
    return this.repository.findOne({ username });
  }

  deleteOne(id: string): Promise<any> {
    return this.repository.delete(id);
  }
}

libs/domain/src/lib/entities/  폴더에 entity.module.ts 생성하고, user.entity.ts를 등록한다.  entity.module.ts에는 user.entity외에 계속 추가되는 entity들을 forFeature로 등록한다. 

// entity.module.ts
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { UserService } from './user/user.service';
import { UserEntity } from './user/user.entity';

@Module({
  imports: [
    TypeOrmModule.forFeature([UserEntity])
  ],
  providers: [UserService],
  exports: [UserService]
})
export class EntitiesModule { }

entity.module.ts을 사용하기 위해 apps/gateway/api/src/app/app.module.ts 파일에 등록한다. 

//app.module.ts 
import { TypeOrmModule } from '@nestjs/typeorm';
import { EntitiesModule } from '@rnm/domain';
import { ormConfigService } from '@rnm/shared';

@Module({
  imports: [
    ...
    // ORM
    TypeOrmModule.forRoot(ormConfigService.getTypeOrmConfig()),
    EntitiesModule,
    ...
  ],
  ...
})
export class AppModule { }

 

User CRUD 컨트롤러 작성 및 테스트

사용자 CRUD를 위한 controller를 작성한다. apps/gateway/api/src/app/user/ 폴더를 생성하고, user.controller.ts 파일을 생성한다. 

// user.controller.ts
import { Body, Controller, Delete, Get, Param, Post, Put } from '@nestjs/common';
import { User, UserService } from '@rnm/domain';

@Controller('api/gateway/user')
export class UserController {
  constructor(
    private readonly service: UserService
  ) { }

  @Post()
  create(@Body() data: User): Promise<User> {
    return this.service.create(data);
  }

  @Put(':id')
  updateOne(@Param('id') id: number, @Body() data: User): Promise<any> {
    return this.service.updateOne(id, data);
  }

  @Get()
  findAll(): Promise<User[]> {
    return this.service.findAll();
  }

  @Get(':username')
  findOne(@Param('username') username: string): Promise<User | undefined> {
    return this.service.findOne(username);
  }

  @Delete(':id')
  deleteOne(@Param('id') id: string): Promise<any> {
    return this.service.deleteOne(id);
  }
}

gateway를 start하면 dev모드에서 synchronized: true에서 "user_iot"  테이블이 자동으로 생성한다. 

Postman으로 호출을 해본다. 

  • POST method 선택
  • Body에 request json 입력
  • JSON 형식 선택
  • "Send" 클릭

 

 

<참조>

- TypeORM 사용형태

https://aonee.tistory.com/77

 

TypeORM 개념 및 설치 및 사용방법

👉 Typeorm 공식문서 ORM 개요 Object-relational mapping, 객체-관계 매핑 객체와 관계형 데이터베이스의 데이터를 자동으로 매핑(연결)해준다. 객체 지향 프로그래밍은 클래스를 사용하고, 관계형 데이

aonee.tistory.com

- TypeORM & Observable 로 변경 사용하는 방법

https://www.youtube.com/watch?v=Z6kw_aJHJLU 

- Nx에서 typeorm 로딩시 에러 이슈

https://github.com/nrwl/nx/issues/1393

 

tsconfig target incompatibility with NestJS and TypeORM module · Issue #1393 · nrwl/nx

Prerequisites I am running the latest version I checked the documentation and found no answer I checked to make sure that this issue has not already been filed I'm reporting the issue to the co...

github.com

 

posted by Peter Note
2021. 9. 24. 19:48 React/Architecture

MS-4/5/6 글을 통해 Gateway의 공통 기능 구현을 위한 설정을 적용한다.

  • Login 할때 사용자 정보는 ORM 기반으로 처리한다.
  • 인증을 JWT 기반으로 처리한다.
  • Login 화면을 React 기반 구현한다.

 

NestJS에 Prisma ORM 설정

gateway-api의 공통 기능은 다음과 같고, JWT처리를 위한 사용자 정보 조회를 위해 Prisma ORM을 적용한다. Micro Service도 Prisma를 사용할 것이다.

  • api gateway: TCP 통신으로 micro service의 API를 연결한다. 
  • http server: gateway의 공통 기능중 Login을 서비스한다.
  • reverse proxy: Login이 성공하면 dashboard micro service로 이동한다. (configuration, back-office)
  • auth server: JWT 기반으로 token을 발행하고, 요청에 대한 모든 Authorization(인가, 권한)을 체크한다.

 

Step-1) 설치 및 환경 설정

auth server 기능에서 사용자 정보 데이터처리를 위해 ORM으로 Prisma를 사용을 위해 패키지를 (v3.1.1) 설치한다.

$> yarn add -D prisma
$> yarn add @prisma/client
$> yarn add -D ts-node

전체 애플리케이션을 위한 초기 prisma 환경을 생성한다.

$> 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

또한 DB Schema에 _prisma_migrations 테이블이 자동생성된다. 

rnm-stack 의 public에 migrations 테이블 자동 생성

테이블을 자동 생성하고 싶다. prisma db push 명령을 사용한다. (참조)

$> 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" 일 경우만 수행한다.
  • schema.prisma 변경시 마다 다시 실행해 주어야 한다. (참조)

$> 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해서 사용할 수 있는 상태가 되었다. 만일 테이블 변경이 발생한다면 아래와 같이 수행한다. 

  • schema.prisma 파일 내역 수정
  • "npx prisma migrate" 명령 실행
  • migrations 폴더에 있는 migration sql을 database에 적용한다. 

참조: https://www.prisma.io/blog/prisma-migrate-ga-b5eno5g08d0b

 

 

Step-3) NestJS 서비스 생성

사용자 정보는 공통이므로 libs/domain 에 생성한다. (참조)

  • libs/shared/src/lib밑으로 prisma 폴더를 생성하고, prisma-client.service.ts파일을 생성한다. 
  • index.ts에 export를 추가한다. 

// prisma-client.service.ts 
import { Injectable, OnModuleInit, OnModuleDestroy } from '@nestjs/common'
import { PrismaClient } from '@prisma/client'

@Injectable()
export class PrismaClientService extends PrismaClient implements OnModuleInit, OnModuleDestroy {
  async onModuleInit() {
    await this.$connect()
  }

  async onModuleDestroy() {
    await this.$disconnect()
  }
}

// index.ts
export * from './lib/configuration/config.model';
export * from './lib/configuration/config.service';
export * from './lib/prisma/prisma-client.service';

apps/gateway/api/src/app/app.module.ts에 PrismaClientService를 추가한다.

// /apps/gateway/api/src/app/app.module.ts
import { Module } from '@nestjs/common';
import { ServeStaticModule } from '@nestjs/serve-static';
import { join } from 'path';

import { GatewayApiAppService } from '@rnm/domain';
import { PrismaClientService } from '@rnm/shared'; <== 요기

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, PrismaClientService] <== 요기
})
export class AppModule { }

gateway/api/src/app/app.controller.ts에 테스트 코드로 POST로 user를 생성하는 코드를 작성한다. 

// apps/gateway/api/src/app/app.controller.ts
import { Body, Controller, Get, Post } from '@nestjs/common';

import { GatewayApiAppService } from '@rnm/domain';
import { PrismaClientService } from '@rnm/shared';

import { Role, User as UserModel } from '@prisma/client';

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

  @Get()
  getData() {
    return this.appService.getData();
  }

  @Post('user')
  async createUser(@Body() userData: {
    email: string,
    password: string,
    firstname: string,
    lastname; string,
    role: Role
  }): Promise<UserModel> {
    const { email, password, firstname, lastname, role } = userData;
    return this.dbService.user.create({
      data: {
        email,
        password,
        firstname,
        lastname,
        role: !role ? Role.USER : role
      }
    });
  }
}

VSCode에서 Debug창에서 Gateway를 실행하고, 디버깅을 한다.

28, 29줄에 breakpoint 

 

Postman을 실행하여 User 를 생성해 본다.

  • 새로운 Request를 만들고 POST를 선택
  • Body에 JSON 타입을 선택
  • request 값을 넣고, http://localhost:8000/api/gateway/user 호출

DB툴로 User insert가 되었는지 확인 또는 prisma studio에서 확인

test@test.com 사용자가 insert 성공!

 

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

 

<참조>

- Primsa on NestJS 기반 개발

https://www.prisma.io/nestjs

 

NestJS Database & Prisma | Type-safe ORM for SQL Databases

Prisma is a next-generation ORM for Node.js & TypeScript. It's the easiest way to build NestJS apps with MySQL, PostgreSQL & SQL Server databases.

www.prisma.io

- Prisma Migrate 순서

https://www.prisma.io/blog/prisma-migrate-ga-b5eno5g08d0b

 

Prisma Migrate is Production Ready - Hassle-Free Database Migrations

Prisma Migrate is ready for use in production - Database schema migration tool with declarative data modeling and auto-generated, customizable SQL migrations

www.prisma.io

- Primsa, JWT on NestJS StartKit

https://github.com/fivethree-team/nestjs-prisma-starter

 

GitHub - fivethree-team/nestjs-prisma-starter: Starter template for NestJS 😻 includes GraphQL with Prisma Client, Passport-JW

Starter template for NestJS 😻 includes GraphQL with Prisma Client, Passport-JWT authentication, Swagger Api and Docker - GitHub - fivethree-team/nestjs-prisma-starter: Starter template for NestJS 😻...

github.com

- Prisma의 다양한 DB 예제

https://github.com/prisma/prisma-examples

 

GitHub - prisma/prisma-examples: 🚀 Ready-to-run Prisma example projects

🚀 Ready-to-run Prisma example projects. Contribute to prisma/prisma-examples development by creating an account on GitHub.

github.com

posted by Peter Note
2021. 9. 23. 20:45 React/Architecture

micro service인 dashboard와 gateway간의 테스트 환경을 VS Code에 만들어 본다. 

 

VS Code 디버깅환경 설정

Step-1) package.json에 script 등록

각 애플리케이션의 build 스크립트를 등록한다. 

// package.json 
  "scripts": {
    "start": "nx serve",
    "build": "nx build",
    "test": "nx test",
    "build:gateway-api": "nx build gateway-api",
    "start:gateway-api": "nx serve gateway-api",
    "build:dashboard-api": "nx build dashboard-api",
    "start:dashboard-api": "nx serve dashboard-api",
    "build:configuration-api": "nx build configuration-api",
    "start:configuration-api": "nx serve configuration-api",
    "build:back-office-api": "nx build back-office-api",
    "start:back-office-api": "nx serve back-office-api"
  },

 

Step-2) .vscode 폴더안의 tasks.json 추가

tasks.json은 vscode의 디버깅 실행 환경설정파일인 launch.json에서 사용한다. 

{
  "version": "2.0.0",
  "tasks": [
    {
      "label": "build:dashboard-api",
      "type": "shell",
      "command": "npm run build:dashboard-api"
    },
    {
      "label": "build:configuration-api",
      "type": "shell",
      "command": "npm run build:configuration-api"
    },
    {
      "label": "build:back-office-api",
      "type": "shell",
      "command": "npm run build:back-office-api"
    },
    {
      "label": "build:gateway-api",
      "type": "shell",
      "command": "npm run build:gateway-api"
    }
  ]
}

 

Step-3) .vscode 폴더안의 launch.json 추가

tasks.json의 설정내용은 "preLaunchTask"에 설정한다.

  • task.json의 build 를 먼저 실행한다. 빌드하면 루트폴더의 dist 폴더에 js, map이 생성된다. 
  • 이후 main.ts 소스에 breakpoint를 찍으면 디버깅을 할 수 있다. 
{
  "version": "0.2.0",
  "configurations": [
    {
      "type": "node",
      "request": "launch",
      "name": "Dashboard API",
      "program": "${workspaceFolder}/apps/dashboard/api/src/main.ts",
      "preLaunchTask": "build:dashboard-api",
      "outFiles": ["${workspaceFolder}/dist/apps/dashboard/api/**/*.js"]
    },
    {
      "type": "node",
      "request": "launch",
      "name": "Gateway API",
      "program": "${workspaceFolder}/apps/gateway/api/src/main.ts",
      "preLaunchTask": "build:gateway-api",
      "outFiles": ["${workspaceFolder}/dist/apps/gateway/api/**/*.js"]
    },
    {
      "type": "node",
      "request": "launch",
      "name": "Configuration API",
      "program": "${workspaceFolder}/apps/configuration/api/src/main.ts",
      "preLaunchTask": "build:configuration-api",
      "outFiles": ["${workspaceFolder}/dist/apps/configuration/api/**/*.js"]
    },
    {
      "type": "node",
      "request": "launch",
      "name": "Back-Office API",
      "program": "${workspaceFolder}/apps/back-office/api/src/main.ts",
      "preLaunchTask": "build:back-office-api",
      "outFiles": ["${workspaceFolder}/dist/apps/back-office/api/**/*.js"]
    }
  ]
}

.vscode안의 launch.json과 tasks.json 파일

 

Gateway와 Micro Service간의 TCP 통신 테스트

Step-1) gateway의 dashboard.controller.ts에서 호출

  • dashboard.controller.ts에서 서비스호출
  • dashboard.service.ts 에서 breakpoint
// apps/gateway/api/src/app/dashboard/dashboard.controller.ts 
import { Controller, Get } from '@nestjs/common';

import { Observable } from 'rxjs';
import { DashboardService } from './dashboard.service';

@Controller('api/dashboard')
export class DashboardController {
  constructor(
    private readonly dashboardService: DashboardService
  ) { }

  @Get('sum')
  accumulate(): Observable<{ message: number, duration: number }> {
    return this.dashboardService.sum();
  }
}

// apps/gateway/api/src/app/dashboard/dashboard.service.ts 
import { Injectable, Inject } from "@nestjs/common";
import { ClientProxy } from "@nestjs/microservices/client";
import { Observable } from "rxjs";
import { map } from "rxjs/operators";

@Injectable()
export class DashboardService {
  constructor(@Inject("DASHBOARD") private readonly client: ClientProxy) { }

  sum(): Observable<{ message: number, duration: number }> {
    const startTs = Date.now();
    const pattern = { cmd: 'dashboard-sum' };
    const payload = [1, 2, 3];
    return this.client
      .send<number>(pattern, payload)
      .pipe(
        map((message: number) => ({ message, duration: Date.now() - startTs }))
      );
  }
}

14줄 breakpoint

 

Step-2) Micro Service 인 dashboard에서 요청처리

  • 요청에 대한 sum 처리후 observable 로 반환
  • MessagePattern 데코레이터 적용
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]);
  }
}

20줄 breakpoint

 

Step-3) Dashboard, Gateway 서버 실행

디버깅으로 이동한다.

  • Dashboard API 실행
  • Gateway API 실행

실행결과

 

브라우져에서 호출

  • http://localhost:8000/api/dashboard/sum

디버깅 실행 영상

https://www.youtube.com/watch?v=UDWPnJdQUhI 

소스

https://github.com/ysyun/rnm-stack/releases/tag/ms-3

 

Release ms-3 · ysyun/rnm-stack

[ms-3] add debug config in vscode

github.com

 

posted by Peter Note