Skip to main content

3 posts tagged with "nestjs"

View All Tags

Multi-tenancy with Nestjs

· 10 min read

Nest.js를 처음 사용하는 경우 탄력적인 웹 애플리케이션을 구축하기 위한 탁월한 프레임워크의 공식문서를 살펴보는 것이 좋습니다 .

다중 테넌트에 대해 이미 잘 알고 있고 Nest.js를 사용한 구현에 대해 코드로 바로 알고 싶다면 GitHub 저장소 에서 코드를 직접 검토해 보세요.

준비

먼저 Nest.js CLI 프로그램을 아직 설치하지 않았다면 설치하세요.

Nestjs cli 설치: https://docs.nestjs.com/cli/overview

nest new multitenant 프로젝트를 발판으로 실행합니다 . 저는 패키지 관리자로 pnpm을 선택했습니다. 또는 pnpm을 사용하지 않으려는 경우 npm 또는 Yarn을 선택할 수 있습니다.

멀티 테넌시(Multi-tenancy)가 무엇인지 알아봅시다.

멀티 테넌시는 SaaS(Software as a Service) 애플리케이션에 사용되는 널리 사용되는 아키텍처 접근 방식으로, 일반적으로 테넌트라고 하는 고객 간의 리소스 공유를 촉진합니다.

위키피디아 정의: https://en.wikipedia.org/wiki/Multitenancy

Multitenancy에서 가장 중요한 개념은 Host vs Tenant 입니다.

호스트는 SaaS 애플리케이션 시스템의 관리를 소유하고 감독할 책임이 있습니다.

테넌트는 서비스를 활용하는 SaaS 애플리케이션의 유료 고객을 의미합니다.

코드를 봅시다.

IService먼저, 나중에 애플리케이션 서비스에서 사용할 일반 인터페이스를 만들어야 합니다 .

폴더 app.interface.ts안에 파일을 만듭니다 .src

 export interface IService<T, C, U> {
get: (uuid: string, tenantId?: string) => T;
create: (data: C, tenantId?: string) => void;
update: (uuid: string, data: U, tenantId?: string) => void;
delete: (uuid: string, tenantId?: string) => void;
getAll: (tenantId?: string) => T[];
}

애플리케이션에서 T는 TodoModel 로 예시된 모델 엔터티를 나타냅니다 . C 와 U는 각각 CreateDto 와 UpdateDto를 나타냅니다 . 또한, 곧 명백해질 이유 때문에 중요한 의미를 지닌 선택적 테넌트 ID 속성이 있습니다 .

다음으로, 기능에 따라 애플리케이션의 폴더를 구성하기 위해 src 디렉터리 내에 테넌트 와 todo라는 두 개의 폴더를 생성해 보겠습니다. 테넌트 기능  우리의 주요 초점으로 작용하고, todo는 테넌트(고객)가 접근할 수 있는 서비스 역할을 하며 호스트도 이를 활용할 수 있습니다.

기본 설정이 완료되면 테넌트 기능을 완료해 보겠습니다. 테넌트 폴더 에는 모델, 컨트롤러, 서비스, 미들웨어 등을 포함한 해당 논리가 포함됩니다.

image

테넌트 논리 설정

테넌트 기능에 대한 DTO, 모델 및 서비스를 만드는 것부터 시작해 보겠습니다. 모델 폴더 내에서 TenantModel.ts 라는 파일을 시작합니다 .

export class TenantModel {
id: string;
name: string;
subdomain?: string; // https://store.mysassapp.com
constructor(id: string, name: string, subdomain?: string) {
this.id = id;
this.name = name;
this.subdomain = subdomain || 'https://mysassapp.com';
}
}

테넌트 모델은 id 및 name 과 같은 필수 속성으로 구성되며 둘 다 필수입니다. 또한 도메인 또는 하위 도메인을 포함하여 고객이 도메인을 맞춤설정할 수 있는 옵션을 제공할 수 있습니다.

참고: 게시물에서 도메인/하위 도메인 부분을 다루지는 않겠습니다.

이제 dtos와 서비스를 만들어 보겠습니다.

src/tenant/dtos/CreateTenantDto.ts

export class CreateTenantDto {
name: string;
subdomain?: string;
constructor(name: string, subdomain?: string) {
this.name = name;
this.subdomain = subdomain;
}
}

src/tenant/dtos/UpdateTenantDto.ts`

export class UpdateTenantDto {
id: string;
name: string;
subdomain?: string;
constructor(id: string, name: string, subdomain?: string) {
this.id = id;
this.name = name;
this.subdomain = subdomain;
}
}

src/tenant/services/TenantService.ts

import { randomUUID } from 'crypto';
import { Injectable, NotFoundException } from '@nestjs/common';
import { IService } from '../../app.interface';
import { TenantModel } from '../models/TenantModel';
import { CreateTenantDto } from '../dtos/CreateTenantDto';
import { UpdateTenantDto } from '../dtos/UpdateTenantDto';

@Injectable()
export class TenantService
implements IService<TenantModel, CreateTenantDto, UpdateTenantDto>
{
private readonly tenants: TenantModel[] = []; // Temp local database..

create(data: CreateTenantDto): void {
const uuid = randomUUID();
this.tenants.push(new TenantModel(uuid, data.name, data.subdomain));
}

delete(uuid: string): void {
const index = this.tenants.findIndex((tenant) => tenant.id === uuid);
if (index === -1) throw new NotFoundException('Tenant not found');
this.tenants.splice(index, 1);
}

get(uuid: string): TenantModel {
const todo = this.tenants.find((tenant) => tenant.id === uuid);
if (!todo) throw new NotFoundException('Tenant not found');
return todo;
}

update(uuid: string, data: UpdateTenantDto): void {
const tenant = this.tenants.find((tenant) => tenant.id === uuid);
if (!tenant) throw new NotFoundException('Tenant not found');
tenant.name = data.name;
tenant.subdomain = data.subdomain;
}

getAll(): TenantModel[] {
return this.tenants;
}
}

테넌트 서비스에서는 현재 테넌트 정보를 저장하기 위해 임시 배열을 활용하고 있습니다. 프로덕션 환경에서는 데이터 지속성을 위해 실제 데이터베이스를 통합하는 것이 필수적입니다. 또한 개별 테넌트에 대해 서로 다른 데이터베이스를 구성할 수 있는 옵션도 있습니다. (이 기능을 구현하는 방법을 배우고 싶다면 댓글로 문의해 주세요.)

앞으로 테넌트 컨트롤러를 만들어 보겠습니다.

src/tenant/controllers/tenant.controller.ts

import {
Body,
Controller,
Delete,
Get,
HttpStatus,
Param,
Post,
Put,
Req,
} from '@nestjs/common';
import { CreateTenantDto } from '../dtos/CreateTenantDto';
import { UpdateTenantDto } from '../dtos/UpdateTenantDto';
import { TenantService } from '../services/TenantService';

@Controller()
export class TenantController {
constructor(private readonly tenantService: TenantService) {}

@Get('/tenants')
getAll() {
return this.tenantService.getAll();
}

@Post('/tenants')
createTodo(@Req() req: Request, @Body() data: CreateTenantDto) {
this.tenantService.create(data);
return HttpStatus.CREATED;
}

@Get('/tenants/:uuid')
getTenant(@Req() req: Request, @Param('uuid') uuid: string) {
return this.tenantService.get(uuid);
}

@Put('/tenants/:uuid')
updateTenant(
@Req() req: Request,
@Param('uuid') uuid: string,
@Body() data: UpdateTenantDto,
) {
this.tenantService.update(uuid, data);
return HttpStatus.NO_CONTENT;
}

@Delete('/tenants/:uuid')
deleteTodo(@Req() req: Request, @Param('uuid') uuid: string) {
this.tenantService.delete(uuid);
return HttpStatus.ACCEPTED;
}
}

좋습니다. 테넌트 논리 구현을 완료했습니다. 다음으로app.module.ts에 컨트롤러와 서비스를 등록합니다. 그런 다음 Postman과 같은 선호하는 REST 클라이언트를 실행하고 엔드포인트를 테스트하세요.

image

image

좋습니다. 테넌트 API가 예상대로 작동합니다. 할 일 논리를 만들어 보겠습니다.

Todo 로직 설정

먼저 todo의 dto, 모델 및 서비스를 계속 생성해 보겠습니다. 모델 폴더 내에 할 일 모델을 생성합니다.TodoModel.ts

export class TodoModel {
uuid: string;
title: string;
done: boolean;
tenantId?: string;

constructor(uuid: string, title: string, done: boolean) {
this.uuid = uuid;
this.title = title;
this.done = done;
}

setTenantId(tenantId: string) {
this.tenantId = tenantId;
}

우리의 할일 모델에서 tenantId 속성은 선택 사항입니다. 이는 각 할일 항목의 소유권 관리를 용이하게 하기 위해 설계되었습니다. 예를 들어, 호스트는 호스트의 고객(테넌트)에게 보이지 않는 상태로 유지되는 할 일 항목을 가질 수 있습니다.

참고: 실제 프로덕션 코드에서는 다대일 관계를 설정하는 todo 모델 엔터티를 소유해야 합니다.

나머지 todo 로직을 계속 진행해 보겠습니다.

src/todo/dtos/CreateTodoDto.ts

export class CreateTodoDto {
title: string;
done: boolean;
constructor(title: string, done: boolean) {
this.title = title;
this.done = done;
}
}

src/todo/dtos/UpdateTodoDto.ts

export class UpdateTodoDto {
id: string;
title: string;
done: boolean;
constructor(id: string, title: string, done: boolean) {
this.title = title;
this.done = done;
}
}

src/todo/services/TodoService.ts

import { randomUUID } from 'crypto';
import { Injectable, NotFoundException } from '@nestjs/common';
import { IService } from '../../app.interface';
import { TodoModel } from '../models/TodoModel';
import { CreateTodoDto } from '../dtos/CreateTodoDto';
import { UpdateTodoDto } from '../dtos/UpdateTodoDto';

@Injectable()
export class TodoService
implements IService<TodoModel, CreateTodoDto, UpdateTodoDto>
{
private readonly todos: TodoModel[] = []; // temp local databse to store all our todo items

create(data: CreateTodoDto, tenantId?: string): void {
const uuid = randomUUID();
const newTodo = new TodoModel(uuid, data.title, data.done);
if (tenantId) newTodo.setTenantId(tenantId);
this.todos.push(newTodo);
}

delete(uuid: string, tenantId?: string) {
const index = this.todos.findIndex((todo) => todo.uuid === uuid);
if (index === -1) throw new NotFoundException('Todo not found');
if (tenantId && this.todos[index].tenantId !== tenantId)
throw new NotFoundException('Todo not found');
this.todos.splice(index, 1);
}

get(uuid: string, tenantId?: string): TodoModel {
const todo = this.todos.find((todo) => todo.uuid === uuid);
if (!todo) throw new NotFoundException('Todo not found');
if (tenantId && todo.tenantId !== tenantId)
throw new NotFoundException('Todo not found');
return todo;
}

update(uuid: string, data: UpdateTodoDto, tenantId?: string): TodoModel {
const todo = this.todos.find((todo) => todo.uuid === uuid);
if (!todo) throw new NotFoundException('Todo not found');
if (tenantId && todo.tenantId !== tenantId)
throw new NotFoundException('Todo not found');
todo.title = data.title;
todo.done = data.done;
return todo;
}

getAll(tenantId?: string): TodoModel[] {
if (tenantId)
return this.todos.filter((todo) => todo.tenantId === tenantId);
return this.todos.filter((todo) => !todo.tenantId);
}
}

src/todo/controllers/todo.controller.ts`

import {
Body,
Controller,
Delete,
Get,
HttpStatus,
Param,
Post,
Put,
Req,
} from '@nestjs/common';
import { CreateTodoDto } from '../dtos/CreateTodoDto';
import { UpdateTodoDto } from '../dtos/UpdateTodoDto';
import { TodoService } from '../services/TodoService';

@Controller()
export class TodoController {
constructor(private readonly todoService: TodoService) {}

@Get('/todos')
getTodos(@Req() req: Request) {
return this.todoService.getAll(req['tenantId']);
}

@Post('/todos')
createTodo(@Req() req: Request, @Body() data: CreateTodoDto) {
this.todoService.create(data, req['tenantId']);
return HttpStatus.CREATED;
}

@Get('/todos/:uuid')
getTodo(@Req() req: Request, @Param('uuid') uuid: string) {
return this.todoService.get(uuid, req['tenantId']);
}

@Put('/todos/:uuid')
updateTodo(
@Req() req: Request,
@Param('uuid') uuid: string,
@Body() data: UpdateTodoDto,
) {
this.todoService.update(uuid, data, req['tenantId']);
return HttpStatus.NO_CONTENT;
}

@Delete('/todos/:uuid')
deleteTodo(@Req() req: Request, @Param('uuid') uuid: string) {
this.todoService.delete(uuid, req['tenantId']);
return HttpStatus.ACCEPTED;
}
}

app.module.ts 에 todo 컨트롤러와 todo 서비스를 등록하여 진행해 보겠습니다 . 그런 다음 todo API를 테스트할 수 있습니다. 할 일 항목을 생성할 수 있어야 하지만 아직 테넌트 ID를 구성하지 않았다는 점에 유의하는 것이 중요합니다.

할 일 서비스에 대한 테넌트 ID를 구성해 보겠습니다.

미들웨어

Nestjs는 expressjs 위에 구축되었으므로 미들웨어를 사용할 수 있습니다. 들어오는 HTTP 요청과 나가는 응답을 가로챌 수 있는 기능입니다.

미들웨어를 사용하여 테넌트 ID가 포함된 HTTP 수신 요청 헤더(예: x-tenant-id: f4d6f363-e4cf-4bda-af19-f0dc2feada81) 를 읽습니다.

테넌트 ID가 로컬 DB에 존재하는지 확인한 다음 요청 객체에 테넌트 ID를 속성으로 추가합니다. 그래서 우리는 그것을 todo 컨트롤러에서 사용할 수 있습니다.

그렇지 않으면 테넌트 ID가 null이 됩니다. 이는 할 일 항목이 호스트에 속함을 의미합니다.

마지막 단계는 todoService가 될 서비스에 미들웨어를 구현하는 것입니다. 엔드포인트를 호출하여 할일 서비스에 액세스할 수 있습니다 /todos.

미들웨어를 만들어 보겠습니다.

` 안에 폴더를 만들고 ` src/tenant/middlewares파일을 만듭니다 .TenantMiddleware.ts

import {
HttpException,
HttpStatus,
Injectable,
NestMiddleware,
Logger,
} from '@nestjs/common';
import { NextFunction } from 'express';
import { TenantService } from '../services/TenantService';

@Injectable()
export class TenantMiddleware implements NestMiddleware {

private readonly logger = new Logger(TenantMiddleware.name);
constructor(private readonly tenantService: TenantService) {}

async use(req: Request, res: Response, next: NextFunction) {
const { headers } = req;

const tenantId = headers['X-TENANT-ID'] || headers['x-tenant-id'];

if (!tenantId) {
this.logger.warn('`X-TENANT-ID` not provided');
req['tenantId'] = null;
return next();
}
const tenant = this.tenantService.get(tenantId);
req['tenantId'] = tenant.id;
next();
}
}

app.module.ts 에 미들웨어를 등록 하고 NestModule 인터페이스를 구현하여 구성해 보겠습니다.

@Module({
imports: [],
controllers: [AppController, TodoController, TenantController],
providers: [TodoService, TenantService, TenantMiddleware],
})
export class AppModule implements NestModule {
configure(consumer: MiddlewareConsumer) {
consumer.apply(TenantMiddleware).forRoutes('/todos');
}
}

curl을 사용하여 2개의 테넌트를 생성해 보겠습니다.

curl -X POST --location "http://localhost:3000/api/tenants" \ -H "Content-Type: application/json" \ -d '{ "name": "Tenant 1" }'
curl -X POST --location "http://localhost:3000/api/tenants" \ -H "Content-Type: application/json" \ -d '{ "name": "Tenant 2" }'

모든 임차인을 가져옵니다

curl -X GET --location "http://localhost:3000/api/tenants" \ -H "Accept: application/json"
[
{
"id": "f4d6f363-e4cf-4bda-af19-f0dc2feada81",
"name": "Tenant 1",
"subdomain": "https://mysassapp.com"
},
{
"id": "df19344b-1063-4699-809a-e596138b2194",
"name": "Tenant 2",
"subdomain": "https://mysassapp.com"
},
]

이제 호스트, 테넌트 1, 테넌트 2에 대한 할 일 항목을 만들어 보겠습니다.

curl -X POST --location "http://localhost:3000/api/todos" \ -H "Content-Type: application/json" \ -d '{ "title": "Belongs to host", "done": false }'
curl -X POST --location "http://localhost:3000/api/todos" \ -H "Content-Type: application/json" \ -H "X-Tenant-ID: f4d6f363-e4cf-4bda-af19-f0dc2feada81" \ -d '{ "title": "Belongs to tenant 1", "done": false }'
curl -X POST --location "http://localhost:3000/api/todos" \ -H "Content-Type: application/json" \ -H "X-Tenant-ID: df19344b-1063-4699-809a-e596138b2194" \ -d '{ "title": "Belongs to tenant 2", "done": false }'

이제 요청 헤더에tenantId를 전달하여 할일 항목을 가져옵니다.

curl -X GET --location "http://localhost:3000/api/todos" \ -H "Accept: application/json" \ -H "X-Tenant-ID: f4d6f363-e4cf-4bda-af19-f0dc2feada81"
[
{
"uuid": "0438dcc5-3061-4bf6-824d-7857885501ca",
"title": "Belongs to tenant 1",
"done": false,
"tenantId": "f4d6f363-e4cf-4bda-af19-f0dc2feada81"
}
]

요청 헤더에 테넌트 ID를 전달하지 않으면 호스트에 속한 할 일 항목을 가져옵니다.

curl -X GET --location "http://localhost:3000/api/todos" \ -H "Accept: application/json"
[
{
"uuid": "12922bd3-2d2b-4dff-9130-32adbaee0432",
"title": "Belongs to host",
"done": false
}
]

결론

우리는 Nest.js를 사용하여 다중 테넌트의 기능적 예를 성공적으로 만들었습니다. 그럼에도 불구하고 실제 시나리오를 위한 다중 테넌트 애플리케이션을 개발할 때는 다음과 같은 몇 가지 요소를 고려하는 것이 중요합니다.

1. Redis와 같은 캐싱 메커니즘으로 보완된 MongoDB 또는 PostgreSQL과 같은 강력한 다중 클러스터 데이터베이스를 활용합니다.
2. 각 테넌트에 맞게 여러 데이터베이스 연결을 구성합니다.

NestJS 실시간 채팅 앱 구현

· 5 min read

image

서버와 클라이언트 간 통신이 실시간으로 발생해야 하는 경우가 종종 있습니다.

이런 기능은 대표적으로 채팅 앱에 많이 사용되는데요.

NestJS에서 WebSocket을 활용해 실시간 채팅 앱을 만들어 원리를 확인해 봅니다.

실시간 API란 무엇일까요?

실시간 API는 실시간으로 데이터를 교환하는 클라이언트와 서버 간의 통신입니다.

일반적으로 WebSocket을 활용해 구현하는데 WebSocket은 클라이언트와 서버 간 실시간 통신을 가능하게 하는 일종의 프로토콜이라고 보면 됩니다.

WebSocket을 구현하기 위해 프레임워크를 사용할 수 있습니다.(socket.io, ws)

프로젝트 구성

프로젝트 구조를 이야기 하자면, 아래와 같습니다.

- 클라이언트: ReactJS Wep App

- 서버: NestJS Realtime App

우선 프로젝트 폴더부터 생성하고,

mkdir nest-chat-app

프로젝트 폴더에 진입 후 NestJS로 서버 폴더를 설치합니다.

nest new server

서버 폴더에 진입 후 NestJS에서 WebSocket 작업을 시작하기 위해 패키지를 설치합니다.

npm i --save @nestjs/websockets @nestjs/platform-socket.io

서버는 얼추 설치가 마무리 됐고, 다시 프로젝트 폴더로 나온 뒤 이제 React 프로젝트를 생성합니다.

npx create-react-app client

클라이언트 폴더로 진입 후 React에서도 필요한 패키지를 설치합니다.

npm i --save socket.io-client

이렇게 프로젝트 기본 구성을 마쳤습니다!

이제 본격적으로 서버를 만들어 볼까요🔥

서버 구성

NestJS에서 WebSocket을 관리하려면 Gateway를 사용해야 하고 이것을 이용하면 간단히 @WebSocketGateway() 데코레이터를 이용해 간단히 설정이 가능해집니다.

그럼 이제 ChatGateway를 생성해 볼까요!

nest generate gateway chat

이렇게 하면 자동으로 chat 폴더에 chat.gateway.ts, chat.gateway.spec.ts 파일들를 자동으로 생성해 줍니다.

그리고 아래와 같이 ChatGateway 클래스에 @WebSocketGateway() 데코레이터가 추가되고 handleMessage 기능을 통해 메시지를 구독할 수 있게 됩니다. 이렇게 되면 클라이언트로부터 데이터를 전달하는 동안 들어오는 메시지를 처리할 수 있게 됩니다.

import { SubscribeMessage, WebSocketGateway } from '@nestjs/websockets';

@WebSocketGateway()
export class ChatGateway {
@SubscribeMessage('message') // subscribe to message events
handleMessage(client: any, payload: any): string {
return 'Hello world!'; // return the text
}
}

handleMessage 기능은 2개의 파라미터가 있는데, client: 플랫폼 별 소켓 인스턴스, payload: 클라이언트로부터 수신된 데이터 이렇게 2개가 있습니다.

클라이언트 측에서 메시지를 보내려면 어떻게 해야 하나요? socket.io-client 패키지를 활용해 이것이 가능합니다. 또한 아래 두번째 줄과 같이 서버에서 보낸 결과 데이터를 다룰 수도 있습니다.

socket.emit('events', { name: 'Nest' });
socket.emit('events', { name: 'Nest' }, (data) => console.log(data));

단순히 서버에서 클라이언트로 데이터를 반환하는 예를 보았는데, 그것이 아닌 메시지를 subscribe 중인 모든 고객에게 메시지를 broadcast 해 보겠습니다.

import {
SubscribeMessage,
WebSocketGateway,
WebSocketServer,
MessageBody,
} from '@nestjs/websockets';
import { Logger } from '@nestjs/common';
import { Server, Socket } from 'socket.io';

import { AddMessageDto } from './dto/add-message.dto';

@WebSocketGateway({ cors: { origin: '*' } })
export class ChatGateway {
@WebSocketServer()
server: Server;

private logger = new Logger('ChatGateway');

@SubscribeMessage('chat') // subscribe to chat event messages
handleMessage(@MessageBody() payload: AddMessageDto): AddMessageDto {
this.logger.log(`Message received: ${payload.author} - ${payload.body}`);
this.server.emit('chat', payload); // broadbast a message to all clients
return payload; // return the same payload data
}
}

위와 같이 데코레이터 @WebSocketServer()를 이용해 broadcast를 실행합니다.(chat을 구독중인 모두에게)

그리고 NestJS에서 제공하는 2개의 수명주기를 사용해 편리하게 소켓 연결 시기를 알 수 있습니다.

@WebSocketGateway({ cors: { origin: '*' } })
export class ChatGateway implements OnGatewayConnection, OnGatewayDisconnect {
...
...

// it will be handled when a client connects to the server
handleConnection(socket: Socket) {
this.logger.log(`Socket connected: ${socket.id}`);
}

// it will be handled when a client disconnects from the server
handleDisconnect(socket: Socket) {
this.logger.log(`Socket disconnected: ${socket.id}`);
}
}

위와 같이 OnGatewayConnection, OnGatewayDisconnect 2개에서 제공하는 handleConnection, handleDisconnect를 활용해 연결 시와 연결 끊김 시의 기능을 구현할 수 있습니다. 그리고 원래는 신뢰할 수 있는 주소에만 적용해야 하지만 테스트 중이니 cors를 모두 통과하도록 안전하지 않은 구성을 진행했습니다.

그럼 서버는 이제 마무리 했고 웹을 구현해 볼 차례입니다.

웹 구성

클라이언트에서 서버와 실시간으로 메시지를 보내고 받는 방법을 구현하기 위해 chat.tsx 파일에 만들 예정입니다.

import { useState, useEffect } from "react";
import { io } from "socket.io-client";

const SystemMessage = {
id: 1,
body: "Welcome to the Nest Chat app",
author: "Bot",
};

// create a new socket instance with localhost URL
const socket = io('http://localhost:4000', { autoConnect: false });

export function Chat({ currentUser, onLogout }) {
const [inputValue, setInputValue] = useState("");
const [messages, setMessages] = useState([SystemMessage]);

useEffect(() => {
socket.connect(); // connect to socket

socket.on("connect", () => { // fire when we have connection
console.log("Socket connected");
});

socket.on("disconnect", () => { // fire when socked is disconnected
console.log("Socket disconnected");
});

// listen chat event messages
socket.on("chat", (newMessage) => {
console.log("New message added", newMessage);
setMessages((previousMessages) => [...previousMessages, newMessage]);
});

// remove all event listeners
return () => {
socket.off("connect");
socket.off("disconnect");
socket.off("chat");
};
}, []);

const handleSendMessage = (e) => {
if (e.key !== "Enter" || inputValue.trim().length === 0) return;

// send a message to the server
socket.emit("chat", { author: currentUser, body: inputValue.trim() });
setInputValue("");
};

const handleLogout = () => {
socket.disconnect(); // disconnect when we do logout
onLogout();
};

return (
<div className="chat">
<div className="chat-header">
<span>Nest Chat App</span>
<button className="button" onClick={handleLogout}>
Logout
</button>
</div>
<div className="chat-message-list">
{messages.map((message, idx) => (
<div
key={idx}
className={`chat-message ${
currentUser === message.author ? "outgoing" : ""
}`}
>
<div className="chat-message-wrapper">
<span className="chat-message-author">{message.author}</span>
<div className="chat-message-bubble">
<span className="chat-message-body">{message.body}</span>
</div>
</div>
</div>
))}
</div>
<div className="chat-composer">
<input
className="chat-composer-input"
placeholder="Type message here"
value={inputValue}
onChange={(e) => setInputValue(e.target.value)}
onKeyDown={handleSendMessage}
/>
</div>
</div>
);
}

이제 웹 클라이언트를 모두 만들었으니 실제 화면을 공유해 봅니다.

NestJS - ReactJS의 데모 앱 시연

git

결론

모든 코드를 확인하려면 여기에서 확인하세요!

도움이 되었기를 바랍니다!

NestJS를 Python으로 만든다면!?

· 4 min read

image

개발자들은 확장 가능하고 유지 관리하기 좋은 API에 대한 수요가 증가하면서 강력한 솔루션을 제공하는 프레임워크를 찾고 있다. Node.js 애플리케이션 구축을 위한 프레임워크인 NestJS는 종속성 주입, 데코레이터, 모듈식 아키텍처를 포함한 강력한 기능으로 인정을 받았다.

동시에 Generative AI는 대규모 언어 모델의 힘을 보여주는 GPT4와 같은 모델을 통해 놀라운 발전을 이루면서, 생성적 AI 애플리케이션에 대한 수요로 인해 다양한 API와 상호 작용해야 하는 확장 가능한 마이크로서비스를 구축하는 데 중점을 두게 됩니다. 라이브러리와 프레임워크로 구성된 광범위한 생태계를 갖춘 Python은 생성 AI 모델을 구현하는 데 탁월한 언어로 부상했고 다재다능함과 유연성 덕분에 이러한 애플리케이션의 요구 사항을 충족하는 확장 가능한 솔루션을 개발하기에 이상적이다. Python을 사용하면 개발자는 생성 AI의 발전을 활용하고 효율적이고 강력한 애플리케이션을 구축할 수 있다.

NestJS에서 영감을 받은 Python 프레임워크 PyNest!

PyNestNestJS의 모듈식 아키텍처FastAPI를 기반으로구축된 Python 프레임워크다. Python 개발자, 데이터 과학자, ML 엔지니어 및 데이터 엔지니어에게 확장 가능하고 유지 관리 가능한 API를 구축하기 위한 직관적이고 강력한 프레임워크를 제공하는 것을 목표로 한다.

핵심은 깨끗하고 체계적인 방식으로 API를 구성하는 문제를 해결한다. NestJS의 모듈식 아키텍처를 채택함으로써 PyNest를 사용하면 개발자가 문제를 분리하고 코드를 모듈, 컨트롤러, 서비스 및 공급자로 구성할 수 있습니다. 이 모듈식 접근 방식은 코드 재사용성, 테스트 가능성 및 유지 관리성을 향상하여 애플리케이션이 발전함에 따라 확장을 촉진하게 됩니다.

image

PyNest로 문제 해결

PyNest는 확장 가능하고 유지 관리 가능한 Python API 구축과 관련된 문제를 해결하는 데 기여하는 몇 가지 주요 기능을 제공한다.

1. Modular Architecture

NestJS에서 영감을 받은 PyNest의 모듈식 아키텍처는 코드 구성에 대한 구조화된 접근 방식을 제공하는데, module, controller, service 및 provider는 문제를 논리적으로 분리하여 코드 중복을 줄이고 유지 관리성을 향상시킨다.

image

2. Dependency Injection

PyNest는 종속성 관리를 단순화하고 테스트 가능성을 높이는 기술인 종속성 주입을 지원해서 개발자는 service와 provider를 controller에 쉽게 주입하여 더욱 깔끔하고 모듈화된 코드를 구현할 수 있다.

3. Decorators

Decorator는 PyNest에서 중요한 역할을 하며 개발자가 경로, 미들웨어 및 기타 애플리케이션 구성 요소를 간결하게 정의할 수 있도록 해준다. 코드 가독성을 향상시키고 개발자가 API의 핵심 기능에 집중할 수 있도록 해다.

4. Type Annotations

PyNest는 FastAPI 유형 주석 메커니즘을 활용하여 더 나은 도구 및 오류 방지 기능을 제공한다. controller, service 및 provider에 유형을 추가하여 개발자는 코드의 견고성과 유지 관리성을 향상시킬 수 있다.

5. Code Generation

PyNest에는 module, controller 및 기타 구성 요소에 대한 상용구 코드 생성을 자동화하는 코드 생성 도구가 포함되어 있어서 개발자의 시간이 절약되고 코드베이스의 필수 부분을 작성하는 데 집중할 수 있다.

6. Database Support

PyNest는 PostgreSQL, MySQL, SQLite와 같은 여러 데이터베이스에서 즉시 사용 가능한 지원을 제공한다.

Getting Started with PyNest

1. 새 프로젝트 생성 및 가상 환경 활성화

python -m venv venv && source venv/bin/activate

2. pip를 사용하여 PyNest를 설치한다.

pip install pynest-api

3. CLI를 사용하여 새 PyNest 프로젝트를 만든다.

pynest create-nest-app -n my_app_name
  • 참고 — 기본적으로 pynest는 SQLite 데이터베이스를 기본으로 하고 PostgreSQL 및 MySQL도 지원한다. 금방 MongoDB 및 기타 데이터베이스도 지원할 예정이다.
  • PostgreSQL로 앱을 생성하려면 아래와 같이 명령하면 된다.
pynest create-nest-app -n my_app_name -db postgresql

4. project directory로 이동한다.

cd my_app_name

5. uvicorn을 사용하여 서버를 실행합니다.

uvicorn "app:app" - host "0.0.0.0" - port "80" - reload

image

PyNest는 애플리케이션 내의 특정 기능을 위한 모듈 생성을 권장한다. 그러니 CLI를 사용해 간단히 모듈을 생성하면 좋다.

pynest generate-module -n module_name

결론

PyNest는 NestJS의 모듈식 아키텍처에서 영감을 받은 강력한 Python 프레임워크다. 이를 통해 Python 개발자는 Python의 강점과 NestJS 확장 가능하고 유지 관리 가능한 API를 구축할 수 있다. PyNest는 종속성 주입, 데코레이터 및 코드 생성을 위한 직관적인 구조와 지원을 통해 확장 가능한 Python API를 개발할 때 직면하는 과제를 해결한다.

생성적 AI에 대한 수요가 계속 증가함에 따라 Python은 생성적 AI 작업에 맞춤화된 마이크로서비스를 구현하기 위한 선택 언어로 부상했는데, PyNest의 모듈식 아키텍처와 확장 가능한 마이크로서비스 지원은 생성 AI 분야의 개발자에게 유용한 도구로 사용하기 좋다.

PyNest를 활용하여 API를 효과적으로 구성하고, pip를 통해 설치하고, Python 프로젝트에서 PyNest의 강력한 기능을 활용해 확장 가능하고 유지 관리가 가능하며 효율적인 API를 구축하길 바란다.

아래는 PyNest 문서를 읽어볼 수 있는 링크이다.

https://pythonnest.github.io/PyNest/