1. Validation Pipe
chats.gateway.ts
1// chats.gateway.ts 생략2@UsePipes(3new ValidationPipe({4transform: true,5transformOptions: {6// 임의로 변환을 허가7enableImplicitConversion: true,8},9whitelist: true,10forbidNonWhitelisted: true,11}),12)13@SubscribeMessage('create_chat')14async createChat(@MessageBody() data: CreateChatDto) {15const chat = await this.chatsService.createChat(data)16}
- main.ts에 글로벌 파이프를 적용했는데, ValidationPipe가 동작하지 않는다.
- 글로벌 파이프를 적용하면,
- DTO에 class validator를 적용하는데는 REST-API 컨트롤러에만 적용된다.
- 그래서 gateway를 쓸 때는 따로 validation을 또 추가해야 한다.
2. Exception Filter 적용
common/exception-filter/socket-exception.filter.ts 파일을 만든다.
common/exception-filter/socket-exception.filter.ts
1import { ArgumentsHost, Catch, HttpException } from '@nestjs/common'2import { BaseWsExceptionFilter } from '@nestjs/websockets'34// 모든 HTTP Exception을 잡기5@Catch(HttpException)6export class SocketCatchHttpExceptionFilter extends BaseWsExceptionFilter<HttpException> {7catch(exception: HttpException, host: ArgumentsHost) {8const socket = host.switchToWs().getClient()9socket.emit('exception', {10status: 'Exception',11message: exception.getResponse(), // 응답에서 받는 값을 받을 수 있음12})13}14}
chats/chats.gateway.ts 파일에서 위에서 작성한 예외필터를 적용한다.
chats/chats.gateway.ts
1// chats/chats.gateway.ts 생략2@UsePipes(3// 생략4)5@UseFilters(SocketCatchHttpExceptionFilter) // 추가 및 import6@SubscribeMessage('create_chat')7async createChat(@MessageBody() data: CreateChatDto) {8const chat = await this.chatsService.createChat(data)9}
3. Guard 적용
auth/guard/socket/socket-bearer-token.guard.ts 파일을 만든다.
auth/guard/socket/socket-bearer-token.guard.ts
1import { CanActivate, ExecutionContext, Injectable } from '@nestjs/common'2import { WsException } from '@nestjs/websockets'3import { AuthService } from 'src/auth/auth.service'4import { UsersService } from 'src/users/users.service'56@Injectable()7export class SocketBearerTokenGuard implements CanActivate {8constructor(9private readonly authService: AuthService,10private readonly usersService: UsersService,11) {}1213async canActivate(context: ExecutionContext): Promise<boolean> {14const socket = context.switchToWs().getClient()1516// rawToken : Bearer xxxx 형태17const rawToken = socket.handshake.headers['authorization']1819if (!rawToken) {20throw new WsException('토큰이 없습니다!')21}2223try {24const token = this.authService.extractTokenFromHeader(rawToken, true)25const payload = this.authService.verifyToken(token)26const user = await this.usersService.getUserByEmail(payload.email)2728socket.user = user29socket.token = token30socket.tokenType = payload.type3132return true33} catch (e) {34throw new WsException('토큰이 유효하지 않습니다!')35}36}37}
위 기능을 사용하기 위해, chats 모듈에 auth, users 모듈을 추가한다.
chats.module.ts
1@Module({2imports: [3TypeOrmModule.forFeature([ChatsModel, MessagesModel]), //4CommonModule,5AuthModule,6UsersModule,7],8// 생략9})10export class ChatsModule {}
chats.gateway.ts에서 Guard를 추가한다.
chats.gateway.ts
1// chats.gateway.ts 생략2@UseGuards(SocketBearerTokenGuard)3@SubscribeMessage('create_chat')4async createChat(5@MessageBody() data: CreateChatDto, //6@ConnectedSocket() socket: Socket & { user: UsersModel },7) {8const chat = await this.chatsService.createChat(data)9}
4. 데코레이터 기반으로 로직 변경
create-messages.dto.ts 파일에서 authorId 컬럼을 삭제한다.
create-messages.dto.ts
1import { PickType } from '@nestjs/mapped-types'2import { IsNumber } from 'class-validator'3import { MessagesModel } from '../entitiy/messages.entity'45export class CreateMessagesDto extends PickType(MessagesModel, ['message']) {6@IsNumber()7chatId: number8}
messages.service.ts 파일에서 authorId를 파라미터로 받는다.
messages.service.ts
1// messages.service.ts 생략2async createMessage(dto: CreateMessagesDto, authorId: number) {3const message = await this.messagesRepository.save({4chat: {5id: dto.chatId,6},7author: {8id: authorId,9},10message: dto.message,11})1213// 생략14}
chats.gateway.ts 파일을 다음과 같이 수정한다.
chats.gateway.ts
1// chats.gateway.ts 생략2@SubscribeMessage('enter_chat')3@UsePipes(4new ValidationPipe({5transform: true,6transformOptions: {7enableImplicitConversion: true,8},9whitelist: true,10forbidNonWhitelisted: true,11}),12)13@UseFilters(SocketCatchHttpExceptionFilter)14@UseGuards(SocketBearerTokenGuard)1516@SubscribeMessage('send_message')17@UsePipes(18new ValidationPipe({19transform: true,20transformOptions: {21enableImplicitConversion: true,22},23whitelist: true,24forbidNonWhitelisted: true,25}),26)27@UseFilters(SocketCatchHttpExceptionFilter)28@UseGuards(SocketBearerTokenGuard)29async sendMessage(30@MessageBody() dto: CreateMessagesDto, //31@ConnectedSocket() socket: Socket & { user: UsersModel },32) {33// 생략3435const message = await this.messageService.createMessage(dto, socket.user.id)36socket.to(message.chat.id.toString()).emit('receive_message', message.message)37}
5. AccessToken을 매번 검증할 떄의 문제
생략
6. Socket에 사용자 정보 저장
chats.gateway.ts
1// 생략23@WebSocketGateway({4// ws://localhost:3000/chats5namespace: 'chats',6})7export class ChatsGateway implements OnGatewayConnection {8constructor(9// 생략10private readonly authService: AuthService,11private readonly usersService: UsersService,12) {}1314// 생략1516async handleConnection(socket: Socket & { user: UsersModel }) {17console.log(`On connect called... ${socket.id}`)18const rawToken = socket.handshake.headers['authorization']19if (!rawToken) socket.disconnect()2021try {22const token = this.authService.extractTokenFromHeader(rawToken, true)23const payload = this.authService.verifyToken(token)24socket.user = await this.usersService.getUserByEmail(payload.email)2526return true27} catch (e) {28socket.disconnect()29}30}3132// 생략 - guard 삭제33async createChat(34@MessageBody() data: CreateChatDto, //35@ConnectedSocket() socket: Socket & { user: UsersModel },36) {37const chat = await this.chatsService.createChat(data)38}3940// 생략 - guard 삭제41async enterChat(42@MessageBody() data: EnterChatDto, //43@ConnectedSocket() socket: Socket & { user: UsersModel },44) {45// 생략46}4748// 생략 - guard 삭제49async sendMessage(50@MessageBody() dto: CreateMessagesDto, //51@ConnectedSocket() socket: Socket & { user: UsersModel },52) {53// 생략54}55}
7. Gateway Lifecycle Hooks
OnGatewayInit: Gateway가 시작됐을 때, 특정 함수를 실행하거나 로직을 실행하고 싶을 떄 사용OnGatewayDisconnect: Gateway의 연결이 끊어졌을 때, 특정 함수를 실행하거나 로직을 실행하고 싶을 떄 사용
chats.gateway.ts
1// chats.gateway.ts 생략23// ws://localhost:3000/chats4@WebSocketGateway({ namespace: 'chats' })5export class ChatsGateway implements OnGatewayConnection, OnGatewayInit, OnGatewayDisconnect {6// 생략78@WebSocketServer()9server: Server1011// Gateway가 시작됐을 때, 특정 함수를 실행하거나 로직을 실행하고 싶을 떄 사용12afterInit(server: Server): any {13console.log(`After gateway init...`)14}1516// Gateway의 연결이 끊어졌을 때, 특정 함수를 실행하거나 로직을 실행하고 싶을 떄 사용17handleDisconnect(socket: Socket): any {18console.log(`On disconnect called... ${socket.id}`)19}2021// 생략22}