NestJS는 높은 평가를 받고 있는 프레임워크로, Node.js의 기능을 활용하여 강력한 서버 측 애플리케이션을 개발할 수 있게 해줍니다. 이 프레임워크는 웹소켓을 지원하므로 실시간 채팅 애플리케이션을 만드는 데 이상적인 선택입니다.

웹소켓은 단일 연결을 통해 클라이언트와 서버 간의 양방향 통신을 허용하는 프로토콜입니다. 실시간 데이터 전송이 가능하므로 채팅 앱과 같이 짧은 지연 시간이 중요한 애플리케이션에 사용하기에 이상적입니다. NestJS를 사용하여 실시간 채팅 애플리케이션을 만들려면 일반적으로 Expresso 라이브러리를 사용하여 웹소켓 연결을 처리하고 JavaScript를 활용하여 애플리케이션의 클라이언트 및 서버 측에서 필요한 로직을 구현합니다.

웹소켓이란 무엇인가요?

웹소켓은 클라이언트와 서버 간의 지속적이고 연속적인 양방향 상호 작용을 위한 매체로, 지속성, 실시간 특성 및 양방향 통신 기능이 특징입니다.

클라이언트와 서버 간의 요청-응답 주기가 완료되면 연결이 종료되는 HTTP와 달리, 웹소켓 연결은 단일 요청-응답 교환 범위를 넘어 지속됩니다.

웹소켓을 사용하는 서버와 클라이언트 간의 통신 메커니즘을 설명하기 위해 그래픽 표현이 제공되었습니다.

서버가 프로토콜을 HTTP에서 WebSocket으로 전환하고 연결을 유지하도록 지시하는 업그레이드 헤더. 자바스크립트를 사용하여 웹소켓을 공부하면 이 기술에 대한 더 많은 통찰력을 얻을 수 있습니다.

NestJS 웹소켓 모듈을 사용하여 실시간 채팅 API 구축하기

Node.js는 두 가지 주요 웹소켓 구현을 제공합니다. 첫 번째는 ws 로 베어 웹소켓을 구현합니다. 그리고 두 번째는 socket.io 으로, 더 높은 수준의 기능을 제공합니다.

NestJS에는 socket.io ws 에 대한 모듈이 모두 있습니다. 이 문서에서는 샘플 애플리케이션의 웹소켓 기능에 대해 socket.io 모듈을 사용합니다.

아래에서 주어진 명령문의 보다 정제된 대체 버전을 찾아보시기 바랍니다: “디렉토리 구조와 이 프로젝트의 다양한 구성 요소 간의 상호 연결에 대한 포괄적인 이해를 얻으려면 제공된 코드의 로컬 사본을 GitHub 리포지토리에서 다운로드하는 것이 좋습니다.

프로젝트 설정 및 설치

터미널 초기화를 시작하고 그 안에서 nest new 명령을 실행하여 원하는 애플리케이션 이름(예: nest new chat-app)을 지정하면 필요한 모든 프로젝트 파일이 포함된 새 디렉터리가 만들어집니다.이 시점에서 개발 프로세스의 시작을 진행할 수 있습니다.

MongoDB 연결 설정

영구 채팅 시스템에는 데이터베이스가 필요하며, 이 문서에서는 NestJS 애플리케이션에 MongoDB 형태로 활용합니다. 쉽게 작동을 시작하려면 클라우드 내에 MongoDB 클러스터를 설정하고 해당 URL을 얻을 수 있습니다. 그 후, 해당 URL을 .env 파일 내에 MONGO\_URI 변수로 저장하면 필요한 연결 정보를 효율적으로 보존할 수 있습니다.

이 글도 확인해 보세요:  내부에서 REST API 호출을 수행하는 방법 VS 코드

몽고DB 데이터베이스에 대한 쿼리를 실행하려면 결국 몽구스를 사용해야 합니다. 이 도구를 얻으려면 터미널에서 “npm install mongoose” 명령을 실행하세요.

소스 폴더에 `mongo.config.ts`라는 새 파일을 생성하고, 그 안에 후속 코딩 내용을 복사하여 이 문서에 붙여넣을 수 있습니다.

 import { registerAs } from '@nestjs/config';

/**
* Mongo database connection config
*/

export default registerAs('mongodb', () => {
  const { MONGO_URI } = process.env; // from .env file
  return {
    uri:`${MONGO_URI}`,
  };
});

프로젝트의 기본 파일은 ‘main.ts’로 표시되며 다음 구조를 따라야 합니다.

 import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import * as cookieParser from 'cookie-parser'
import helmet from 'helmet'
import { Logger, ValidationPipe } from '@nestjs/common';
import { setupSwagger } from './utils/swagger';
import { HttpExceptionFilter } from './filters/http-exception.filter';

async function bootstrap() {
  const app = await NestFactory.create(AppModule, { cors: true });
  app.enableCors({
    origin: '*',
    credentials: true
  })
  app.use(cookieParser())
  app.useGlobalPipes(
    new ValidationPipe({
      whitelist: true
    })
  )
  const logger = new Logger('Main')

  app.setGlobalPrefix('api/v1')
  app.useGlobalFilters(new HttpExceptionFilter());

  setupSwagger(app)
  app.use(helmet())

  await app.listen(AppModule.port)

  // log docs
  const baseUrl = AppModule.getBaseUrl(app)
  const url = `http://${baseUrl}:${AppModule.port}`
  logger.log(`API Documentation available at ${url}/docs`);
}
bootstrap();

채팅 모듈 구축

라이브 채팅 기능을 시작하려면 터미널 내에서 실행되는 명령을 통해 필요한 NestJS 웹 소켓 패키지를 설치하는 것이 첫 번째 작업입니다.

 npm install @nestjs/websockets @nestjs/platform-socket.io @types/socket.io 

필요한 패키지가 설치되면 챗봇 모듈을 생성하기 위해 일련의 명령을 실행해야 합니다.

 nest g module chats
nest g controller chats
nest g service chats

다음 단계는 NestJS 내에서 웹소켓 연결을 설정하는 것입니다. 클라이언트와 서버 간의 통신을 원활하게 하려면 메시지 송수신을 위한 게이트웨이를 구현해야 합니다. 이 게이트웨이는 chat.gateway.ts라는 이름의 새 파일 형태로 구현되며, 이 파일은 “chats” 디렉토리에 위치합니다.

‘./chat.component.html’, styleUrls: [‘./chat.component.css’] }) }) 내보내기 클래스 ChatComponent 구현 OnInit { private messages = []; private input = ”; constructor() {} ngOnInit(): void {} this.loadMessage(); } loadMessages() { // 서버 또는 로컬 저장소에서 채팅 메시지를 검색하고 표시합니다. this.messages = environment.getChatMessages(); } addMessage(message: 문자열

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

@WebSocketGateway()
export class ChatGateway {
    @WebSocketServer()
    server: Server;
    // listen for send_message events
    @SubscribeMessage('send_message')
    listenForMessages(@MessageBody() message: string) {
      this.server.sockets.emit('receive_message', message);
    }
}

연결된 사용자 인증하기

웹 애플리케이션에 액세스하는 사용자의 신원을 확인하는 프로세스를 인증이라고 합니다. 이 관행은 채팅 애플리케이션을 개발할 때도 똑같이 중요합니다.’chats.service.ts’ 파일에는 클라이언트에서 소켓

 @Injectable()
export class ChatsService {
    constructor(private authService: AuthService) {}

    async getUserFromSocket(socket: Socket) {
        let auth_token = socket.handshake.headers.authorization;
        // get the token itself without "Bearer"
        auth_token = auth_token.split(' ')[1];

        const user = this.authService.getUserFromAuthenticationToken(
            auth_token
        );

        if (!user) {
            throw new WsException('Invalid credentials.');
        }
        return user;
    }
}

로 들어오는 연결을 인증하는 기능 모듈이 있습니다. 앞서 언급한 프로세스에는 베어러 심볼 추출을 통해 JWT 토큰에서 현재 사용자를 검색하는 `getUserFromAuthenticationToken` 함수를 사용하는 것이 수반됩니다. 이 작업은 아래에 표시된 것처럼 `auth.service.ts` 파일에 포함되어 있습니다:

 public async getUserFromAuthenticationToken(token: string) {
        const payload: JwtPayload = this.jwtService.verify(token, {
          secret: this.configService.get('JWT_ACCESS_TOKEN_SECRET'),
        });

        const userId = payload.sub

        if (userId) {
            return this.usersService.findById(userId);
        }
      }

OnGatewayConnection 인터페이스를 구현하는 ChatGateway 클래스의 handleConnection 메서드가 새 소켓 연결을 받으면 연결된 사용자에 대한 세부 정보를 얻을 수 있도록 소켓을 getUserFromSocket 함수에 매개 변수로 전달합니다.

이 글도 확인해 보세요:  JES에서 사운드를 임포트하고 재생하는 방법

다음 코드 조각은 앞서 언급한 점을 보여주는 예시입니다.

 // chat.gateway.ts
@WebSocketGateway()
export class ChatGateway implements OnGatewayConnection {
    @WebSocketServer()
    server: Server;

    constructor(private chatsService: ChatsService) {}

    async handleConnection(socket: Socket) {
        await this.chatsService.getUserFromSocket(socket)
    }

    @SubscribeMessage('send_message')
    async listenForMessages(@MessageBody() message: string, @ConnectedSocket() socket: Socket) {

        const user = await this.chatsService.getUserFromSocket(socket)
        this.server.sockets.emit('receive_message', {
            message,
            user
        });
    }
}

필요한 모든 코드 스니펫과 임포트 문이 포함된 인증 시스템 구현에 대한 자세한 정보와 이해를 높이려면 앞서 언급한 GitHub 리포지토리를 참조하세요.

데이터베이스에 채팅 지속

주어진 텍스트를 다듬는 데 도움을 줄 수 있나요?

 import { User } from './../users/schemas/user.schema';
import { Prop, Schema, SchemaFactory } from "@nestjs/mongoose";
import mongoose, { Document } from "mongoose";

export type MessageDocument = Message & Document;

@Schema({
    toJSON: {
        getters: true,
        virtuals: true,
    },
    timestamps: true,
})
export class Message {
    @Prop({ required: true, unique: true })
    message: string

    @Prop({ type: mongoose.Schema.Types.ObjectId, ref: 'User' })
    user: User
}

const MessageSchema = SchemaFactory.createForClass(Message)

export { MessageSchema };

제공된 코드 조각은 새 채팅 메시지를 만드는 기능과 채팅 서비스에서 모든 채팅 메시지를 검색하는 두 가지 기능의 구현을 보여 줍니다. 이러한 기능은 `chats.service.ts` 파일 내에 캡슐화되어 있습니다.

 import { Message, MessageDocument } from './message.schema'; 
import { Socket } from 'socket.io';
import { AuthService } from './../auth/auth.service';
import { Injectable } from '@nestjs/common';
import { WsException } from '@nestjs/websockets';
import { InjectModel } from '@nestjs/mongoose';
import { Model } from 'mongoose';
import { MessageDto } from './dto/message.dto';

@Injectable()
 export class ChatsService {
    constructor(private authService: AuthService, @InjectModel(Message.name) private messageModel: Model<MessageDocument>) {}
    ....
    async createMessage(message: MessageDto, userId: string) {
        const newMessage = new this.messageModel({...message, userId})
        await newMessage.save
       return newMessage
    }
    async getAllMessages() {
       return this.messageModel.find().populate('user')
    }
}

‘chats’라는 이름의 디렉토리 구조에서 ‘dto’ 폴더 내에 ‘message.dto.ts’라는 파일이 있으며, 이 파일은 MessageDto의 구현을 포함합니다. 또한 이 파일은 리포지토리를 통해 액세스할 수 있습니다.

메시지 모델 및 스키마를 chats.module.ts 내의 가져오기 문 목록에 통합하세요.

 import { Message, MessageSchema } from './message.schema';
import { Module } from '@nestjs/common';
import { ChatGateway } from './chats.gateway';
import { ChatsService } from './chats.service';
import { MongooseModule } from '@nestjs/mongoose';

@Module({
  imports: [MongooseModule.forFeature([
    { name: Message.name, schema: MessageSchema }
  ])],
  controllers: [],
  providers: [ChatsService, ChatGateway]
})
export class ChatsModule {}

앞서 언급한 모든 메시지를 검색하는 기능이 다음 코드 스니펫에서 알 수 있듯이 ChatGateway 클래스의 프레임워크 내에 통합되었습니다:

 // imports...

@WebSocketGateway()
export class ChatGateway implements OnGatewayConnection {
    ....

    @SubscribeMessage('get_all_messages')
    async getAllMessages(@ConnectedSocket() socket: Socket) {

        await this.chatsService.getUserFromSocket(socket)
        const messages = await this.chatsService.getAllMessages()

        this.server.sockets.emit('receive_message', messages);

        return messages
    }
}

연결된 사용자로부터 get\_all\_messages 이벤트를 수신하면 모든 메시지를 검색할 수 있으며, 해당 사용자가 메시지를 보내면 데이터베이스 내에 메시지가 생성 및 저장된 후 연결된 다른 모든 클라이언트에 배포됩니다.

이 글도 확인해 보세요:  녹 매크로: 매크로를 사용하여 코드를 개선하는 방법

이전 조치가 완료되면 “npm run start:dev”를 실행하여 애플리케이션을 시작하고 Postman과 같은 웹소켓 클라이언트를 사용하여 애플리케이션의 유효성을 검사할 수 있습니다.

NestJS로 실시간 애플리케이션 구축하기

웹소켓은 다양한 시나리오에서 간단하게 구현할 수 있고 채팅 애플리케이션 매체로서 뛰어난 성능을 발휘하기 때문에 실시간 프레임워크를 구축하는 데 매우 인기 있는 기술로 부상했습니다.

실시간 애플리케이션의 다른 예로는 비디오 스트리밍이나 통화 서비스, 실시간 날씨 업데이트 등이 있으며, NestJS는 이러한 애플리케이션을 개발하기 위한 강력한 도구를 제공합니다.

By 최은지

윈도우(Windows)와 웹 서비스에 대한 전문 지식을 갖춘 노련한 UX 디자이너인 최은지님은 효율적이고 매력적인 디지털 경험을 개발하는 데 탁월한 능력을 발휘합니다. 사용자의 입장에서 생각하며 누구나 쉽게 접근하고 즐길 수 있는 콘텐츠를 개발하는 데 주력하고 있습니다. 사용자 경험을 향상시키기 위해 연구를 거듭하는 은지님은 All Things N 팀의 핵심 구성원으로 활약하고 있습니다.