🎉 berenickt 블로그에 온 걸 환영합니다. 🎉
Back
NestJs
32-socketIO-심화

1. Validation Pipe

chats.gateway.ts
1
// chats.gateway.ts 생략
2
@UsePipes(
3
new ValidationPipe({
4
transform: true,
5
transformOptions: {
6
// 임의로 변환을 허가
7
enableImplicitConversion: true,
8
},
9
whitelist: true,
10
forbidNonWhitelisted: true,
11
}),
12
)
13
@SubscribeMessage('create_chat')
14
async createChat(@MessageBody() data: CreateChatDto) {
15
const 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
1
import { ArgumentsHost, Catch, HttpException } from '@nestjs/common'
2
import { BaseWsExceptionFilter } from '@nestjs/websockets'
3
4
// 모든 HTTP Exception을 잡기
5
@Catch(HttpException)
6
export class SocketCatchHttpExceptionFilter extends BaseWsExceptionFilter<HttpException> {
7
catch(exception: HttpException, host: ArgumentsHost) {
8
const socket = host.switchToWs().getClient()
9
socket.emit('exception', {
10
status: 'Exception',
11
message: 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) // 추가 및 import
6
@SubscribeMessage('create_chat')
7
async createChat(@MessageBody() data: CreateChatDto) {
8
const 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
1
import { CanActivate, ExecutionContext, Injectable } from '@nestjs/common'
2
import { WsException } from '@nestjs/websockets'
3
import { AuthService } from 'src/auth/auth.service'
4
import { UsersService } from 'src/users/users.service'
5
6
@Injectable()
7
export class SocketBearerTokenGuard implements CanActivate {
8
constructor(
9
private readonly authService: AuthService,
10
private readonly usersService: UsersService,
11
) {}
12
13
async canActivate(context: ExecutionContext): Promise<boolean> {
14
const socket = context.switchToWs().getClient()
15
16
// rawToken : Bearer xxxx 형태
17
const rawToken = socket.handshake.headers['authorization']
18
19
if (!rawToken) {
20
throw new WsException('토큰이 없습니다!')
21
}
22
23
try {
24
const token = this.authService.extractTokenFromHeader(rawToken, true)
25
const payload = this.authService.verifyToken(token)
26
const user = await this.usersService.getUserByEmail(payload.email)
27
28
socket.user = user
29
socket.token = token
30
socket.tokenType = payload.type
31
32
return true
33
} catch (e) {
34
throw new WsException('토큰이 유효하지 않습니다!')
35
}
36
}
37
}

위 기능을 사용하기 위해, chats 모듈에 auth, users 모듈을 추가한다.

chats.module.ts
1
@Module({
2
imports: [
3
TypeOrmModule.forFeature([ChatsModel, MessagesModel]), //
4
CommonModule,
5
AuthModule,
6
UsersModule,
7
],
8
// 생략
9
})
10
export class ChatsModule {}

chats.gateway.ts에서 Guard를 추가한다.

chats.gateway.ts
1
// chats.gateway.ts 생략
2
@UseGuards(SocketBearerTokenGuard)
3
@SubscribeMessage('create_chat')
4
async createChat(
5
@MessageBody() data: CreateChatDto, //
6
@ConnectedSocket() socket: Socket & { user: UsersModel },
7
) {
8
const chat = await this.chatsService.createChat(data)
9
}

4. 데코레이터 기반으로 로직 변경

create-messages.dto.ts 파일에서 authorId 컬럼을 삭제한다.

create-messages.dto.ts
1
import { PickType } from '@nestjs/mapped-types'
2
import { IsNumber } from 'class-validator'
3
import { MessagesModel } from '../entitiy/messages.entity'
4
5
export class CreateMessagesDto extends PickType(MessagesModel, ['message']) {
6
@IsNumber()
7
chatId: number
8
}

messages.service.ts 파일에서 authorId를 파라미터로 받는다.

messages.service.ts
1
// messages.service.ts 생략
2
async createMessage(dto: CreateMessagesDto, authorId: number) {
3
const message = await this.messagesRepository.save({
4
chat: {
5
id: dto.chatId,
6
},
7
author: {
8
id: authorId,
9
},
10
message: dto.message,
11
})
12
13
// 생략
14
}

chats.gateway.ts 파일을 다음과 같이 수정한다.

chats.gateway.ts
1
// chats.gateway.ts 생략
2
@SubscribeMessage('enter_chat')
3
@UsePipes(
4
new ValidationPipe({
5
transform: true,
6
transformOptions: {
7
enableImplicitConversion: true,
8
},
9
whitelist: true,
10
forbidNonWhitelisted: true,
11
}),
12
)
13
@UseFilters(SocketCatchHttpExceptionFilter)
14
@UseGuards(SocketBearerTokenGuard)
15
16
@SubscribeMessage('send_message')
17
@UsePipes(
18
new ValidationPipe({
19
transform: true,
20
transformOptions: {
21
enableImplicitConversion: true,
22
},
23
whitelist: true,
24
forbidNonWhitelisted: true,
25
}),
26
)
27
@UseFilters(SocketCatchHttpExceptionFilter)
28
@UseGuards(SocketBearerTokenGuard)
29
async sendMessage(
30
@MessageBody() dto: CreateMessagesDto, //
31
@ConnectedSocket() socket: Socket & { user: UsersModel },
32
) {
33
// 생략
34
35
const message = await this.messageService.createMessage(dto, socket.user.id)
36
socket.to(message.chat.id.toString()).emit('receive_message', message.message)
37
}

5. AccessToken을 매번 검증할 떄의 문제

생략


6. Socket에 사용자 정보 저장

chats.gateway.ts
1
// 생략
2
3
@WebSocketGateway({
4
// ws://localhost:3000/chats
5
namespace: 'chats',
6
})
7
export class ChatsGateway implements OnGatewayConnection {
8
constructor(
9
// 생략
10
private readonly authService: AuthService,
11
private readonly usersService: UsersService,
12
) {}
13
14
// 생략
15
16
async handleConnection(socket: Socket & { user: UsersModel }) {
17
console.log(`On connect called... ${socket.id}`)
18
const rawToken = socket.handshake.headers['authorization']
19
if (!rawToken) socket.disconnect()
20
21
try {
22
const token = this.authService.extractTokenFromHeader(rawToken, true)
23
const payload = this.authService.verifyToken(token)
24
socket.user = await this.usersService.getUserByEmail(payload.email)
25
26
return true
27
} catch (e) {
28
socket.disconnect()
29
}
30
}
31
32
// 생략 - guard 삭제
33
async createChat(
34
@MessageBody() data: CreateChatDto, //
35
@ConnectedSocket() socket: Socket & { user: UsersModel },
36
) {
37
const chat = await this.chatsService.createChat(data)
38
}
39
40
// 생략 - guard 삭제
41
async enterChat(
42
@MessageBody() data: EnterChatDto, //
43
@ConnectedSocket() socket: Socket & { user: UsersModel },
44
) {
45
// 생략
46
}
47
48
// 생략 - guard 삭제
49
async 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 생략
2
3
// ws://localhost:3000/chats
4
@WebSocketGateway({ namespace: 'chats' })
5
export class ChatsGateway implements OnGatewayConnection, OnGatewayInit, OnGatewayDisconnect {
6
// 생략
7
8
@WebSocketServer()
9
server: Server
10
11
// Gateway가 시작됐을 때, 특정 함수를 실행하거나 로직을 실행하고 싶을 떄 사용
12
afterInit(server: Server): any {
13
console.log(`After gateway init...`)
14
}
15
16
// Gateway의 연결이 끊어졌을 때, 특정 함수를 실행하거나 로직을 실행하고 싶을 떄 사용
17
handleDisconnect(socket: Socket): any {
18
console.log(`On disconnect called... ${socket.id}`)
19
}
20
21
// 생략
22
}