NestJS-Web Socket(deep)

jaegeunsong97ยท2024๋…„ 2์›” 18์ผ
0

NestJS

๋ชฉ๋ก ๋ณด๊ธฐ
32/37
post-custom-banner

๐Ÿ–Š๏ธValidation pipe

์ด๋ฒˆ์—๋Š” Pipe๋ฅผ ์‚ฌ์šฉํ•ด๋ณด๋„๋ก ํ•˜๊ฒ ์Šต๋‹ˆ๋‹ค.

REST API์—์„œ์˜ pipe์™€ Gateway์—์„œ์˜ API๋Š” ์„œ๋กœ ๋‹ค๋ฅด์ง€ ์•Š์Šต๋‹ˆ๋‹ค. ๊ณต์‹๋ฌธ์—์„œ๋„ ๋ณ„๋กœ ๋‹ค๋ฅด์ง€ ์•Š๋‹ค๊ณ  ํ•ฉ๋‹ˆ๋‹ค.

ํฌ์ŠคํŠธ๋งจ์œผ๋กœ 1๋ฒˆ ์‚ฌ์šฉ์ž๋ฅผ ์—ฐ๊ฒฐ ํ›„ JSON์œผ๋กœ ์•„๋ฌด๋Ÿฐ ๊ฐ’์„ ์ฃผ์ง€์•Š๊ณ  create_chat์„ ํ•ด๋ณด๋„๋ก ํ•˜๊ฒ ์Šต๋‹ˆ๋‹ค.

๋จผ์ € ๋ฆฌ์Šค๋‹์œผ๋กœ exception๊ณผ receive_message๋ฅผ ํ™œ์„ฑํ™” ํ•ด์ค๋‹ˆ๋‹ค.

์ด๋ ‡๊ฒŒ ๋‚˜์˜ค๋ฉด ์•ˆ๋ฉ๋‹ˆ๋‹ค. ์™œ๋ƒํ•˜๋ฉด ์–ด๋– ํ•œ ๊ฐ’์ด ๋ญ๊ฐ€ ์ž˜๋ชป ๋˜์—ˆ๋‹ค๊ณ  ๋‚˜์™€์•ผํ•ฉ๋‹ˆ๋‹ค. ๊ทธ๋Ÿฐ๋ฐ InternalServerError๊ฐ€ ๋‚˜์˜ค๊ฒŒ ๋ฉ๋‹ˆ๋‹ค.

์šฐ๋ฆฌ๋Š” ์ด๋ฏธ main.ts์—์„œ Validation pipe๋ฅผ ์ „์—ญ์ ์œผ๋กœ ์ž‘์šฉํ•˜๋„๋ก ๋งŒ๋“ค์—ˆ์Šต๋‹ˆ๋‹ค. ํ•˜์ง€๋งŒ ์ž‘๋™์„ ํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค.

  • main.ts
app.useGlobalPipes(new ValidationPipe({
    transform: true, // ๋ณ€ํ™”๋Š” ํ•ด๋„ ๋œ๋‹ค.
    transformOptions: {
      	enableImplicitConversion: true // ์ž„์˜๋กœ ๋ณ€ํ™˜ํ•˜๋Š” ๊ฒƒ์„ ํ—ˆ๊ฐ€ํ•œ๋‹ค.
    },
    whitelist: true,
    forbidNonWhitelisted: true,
}));

์•„์‰ฝ๊ฒŒ๋„ ๊ธ€๋กœ๋ฒŒ ํŒŒ์ดํ”„๋ฅผ ์ „์—ญ์ ์œผ๋กœ ์ž‘์šฉํ•˜๋Š” ๊ฒƒ์€ ์˜ค์ง REST API ์ปจํŠธ๋กค๋Ÿฌ์—๋งŒ ์ ์šฉ์ด ๋ฉ๋‹ˆ๋‹ค. ๊ทธ๋ž˜์„œ ์šฐ๋ฆฌ๊ฐ€ Gateway๋ฅผ ์‚ฌ์šฉํ•  ๋•Œ๋Š” ๋”ฐ๋กœ Validation์„ gateway์— ์ถ”๊ฐ€ํ•ด์ค˜์•ผ ํ•ฉ๋‹ˆ๋‹ค.

๋”ฐ๋ผ์„œ @SubscribeMessage๋ณ„๋กœ @UsePipes๋ฅผ ์ ์šฉํ•ด์•ผํ•ฉ๋‹ˆ๋‹ค.

  • chats.gateway.ts
@UsePipes(new ValidationPipe({
    transform: true, // ๋ณ€ํ™”๋Š” ํ•ด๋„ ๋œ๋‹ค.
    transformOptions: {
      	enableImplicitConversion: true // ์ž„์˜๋กœ ๋ณ€ํ™˜ํ•˜๋Š” ๊ฒƒ์„ ํ—ˆ๊ฐ€ํ•œ๋‹ค.
    },
    whitelist: true,
    forbidNonWhitelisted: true,
}))
@SubscribeMessage('create_chat')
async createChat(
    @MessageBody() data: CreateChatDto,
    @ConnectedSocket() socket: Socket,
) {
    const chat = await this.chatsService.createChat(
      	data, 
    );
}

ํฌ์ŠคํŠธ๋งจ์œผ๋กœ ๋‹ค์‹œ ๋ณด๋‚ด๋„ ์—๋Ÿฌ๊ฐ€ ๋ฐœ์ƒํ•ฉ๋‹ˆ๋‹ค.

ํ˜„์žฌ ์—๋Ÿฌ์˜ ์œ„์น˜๋ฅผ ๋ณด๋ฉด validation.pipe๋กœ ๋‚˜์˜ต๋‹ˆ๋‹ค. ์ฆ‰, dto์—์„œ๋Š” ํ†ต๊ณผ๊ฐ€ ๋˜์—ˆ๋‹ค๋Š” ๊ฒƒ์ž…๋‹ˆ๋‹ค. ํ•˜์ง€๋งŒ Bad Request Exception์ด ๋ฉ”์„ธ์ง€๋กœ ์ „๋‹ฌ์ด ๋˜์ง€์•Š๊ณ  ํ„ฐ์ ธ๋ฒ„๋ ธ์Šต๋‹ˆ๋‹ค.

์™œ๋ƒํ•˜๋ฉด ์šฐ๋ฆฌ๋Š” ์—๋Ÿฌ๋ฅผ ๋˜์งˆ ๋•Œ WsException์œผ๋กœ ๋˜์ ธ์•ผํ•ฉ๋‹ˆ๋‹ค. ํ•˜์ง€๋งŒ class-validator๋Š” ๊ธฐ๋ณธ์ ์œผ๋กœ REST API๋ฅผ ์œ„ํ•ด์„œ ์„ค๊ณ„๊ฐ€ ๋œ๊ฒƒ์ด๊ธฐ ๋•Œ๋ฌธ์— ๋ชจ๋“  Expection๋“ค์ด HTTP Excetpion์„ extendsํ•˜๊ณ  ์žˆ์Šต๋‹ˆ๋‹ค.

ํ•˜์ง€๋งŒ WsException์€ HTTP Exception์„ extendsํ•˜๊ณ  ์žˆ์ง€ ์•Š์Šต๋‹ˆ๋‹ค. ๊ทธ๋ž˜์„œ ๋‹ค์Œ๊ณผ ๊ฐ™์€ ์—๋Ÿฌ๊ฐ€ ๋ฐœ์ƒํ•œ ๊ฒƒ์ž…๋‹ˆ๋‹ค.

๊ทธ๋Ÿฌ๋ฉด ์šฐ๋ฆฌ๋Š” HTTP Exception๋“ค์ด ๋ฐœ์ƒํ–ˆ์„ ๋•Œ, WsException์œผ๋กœ ๋ณ€ํ™˜ํ•ด์ฃผ๋Š” ์ฝ”๋“œ๋ฅผ ์ž‘์„ฑํ•˜๋ฉด ๋˜๋Š” ๊ฒƒ์ž…๋‹ˆ๋‹ค.


๐Ÿ–Š๏ธException Filter ์ ์šฉ

์ด์–ด์„œ HTTP Exception๋“ค์ด ๋ฐœ์ƒํ–ˆ์„ ๋•Œ, WsException์œผ๋กœ ๋ณ€ํ™˜ํ•ด์ฃผ๋Š” ์ฝ”๋“œ๋ฅผ ์ž‘์„ฑํ•˜๊ฒ ์Šต๋‹ˆ๋‹ค.

Exception Filter์—์„œ HTTP Exception์„ ๋ชจ๋‘ ์žก์•„์„œ WsException์œผ๋กœ ๋ฐ”๊ฟ”์ฃผ๋ฉด ์—๋Ÿฌ๋“ค์„ ์ „๋ถ€ ์žก์„ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

common ํด๋”์—์„œ ์ž‘์—…์„ ์ง„ํ–‰ํ•˜๊ฒ ์Šต๋‹ˆ๋‹ค.

  • common/exception-filter/socket-catch-http.exception-filter.ts
@Catch(HttpException)
export class SocketCatchHttpExceptionFilter extends BaseWsExceptionFilter<HttpException> {
    // BaseWsExceptionFilter๋ฅผ ์ƒ์†ํ•˜๋ฉด Ws๊ด€๋ จ Exception์„ ๋งŒ๋“ค ์ˆ˜ ์žˆ๋‹ค.

    catch(exception: HttpException, host: ArgumentsHost): void {
        super.catch(exception, host);

        const socket = host.switchToWs().getClient(); // ์†Œ์ผ“ ๊ฐ€์ ธ์˜ค๊ธฐ
        socket.emit( // ํ˜„์žฌ ์†Œ์ผ“์—๋‹ค๊ฐ€๋งŒ emitํ•˜๊ธฐ
            'exception', // ์ด๋ฒคํŠธ ์ด๋ฆ„
            {	
              	// ์‹ค์ œ ์‘๋‹ต์—์„œ ๋ฐ›๋Š” {๋ฉ”์„ธ์ง€ ํ˜•ํƒœ๋“ค}
              	data: exception.getResponse(),
            }
        )
    }
}

Exception Filter๋ฅผ ์ ์šฉํ•˜๊ฒ ์Šต๋‹ˆ๋‹ค.

  • chats.gateway.ts
@UsePipes(new ValidationPipe({
    transform: true,
    transformOptions: {
      	enableImplicitConversion: true
    },
    whitelist: true,
    forbidNonWhitelisted: true,
}))
@UseFilters(SocketCatchHttpExceptionFilter) // ์ ์šฉ
@SubscribeMessage('create_chat')
async createChat(
    @MessageBody() data: CreateChatDto,
    @ConnectedSocket() socket: Socket,
) {
    const chat = await this.chatsService.createChat(
      	data, 
    );
}

ํฌ์ŠคํŠธ๋งจ์œผ๋กœ ํ…Œ์ŠคํŠธ๋ฅผ ํ•˜๊ฒ ์Šต๋‹ˆ๋‹ค.

User 1๊ณผ User 2๋ชจ๋‘ ๋ฆฌ์Šค๋„ˆ๋ฅผ exception, receive_message๋ฅผ ์—ด๊ฒ ์Šต๋‹ˆ๋‹ค. ๊ทธ๋ฆฌ๊ณ  create_chat์„ ์‹คํ–‰ํ•˜๋ฉด ์—๋Ÿฌ๊ฐ€ 2๊ฐœ๊ฐ€ ๋‚˜์˜ต๋‹ˆ๋‹ค.

์ด์œ ๋Š” super.catch() ๋•Œ๋ฌธ์ž…๋‹ˆ๋‹ค. ๋”ฐ๋ผ์„œ ํ•ด๋‹น ์ฝ”๋“œ๋ฅผ ์‚ญ์ œํ•ฉ๋‹ˆ๋‹ค. ์ดํ›„์— ๋‹ค์‹œ ๋˜‘๊ฐ™์€ ๋ฐฉ๋ฒ•์œผ๋กœ ํ…Œ์ŠคํŠธ๋ฅผ ํ•˜๊ฒŒ๋˜๋ฉด

๋‹ค์Œ ์—๋Ÿฌ ๋ฉ”์„ธ์ง€๋Š” User 1๋ฒˆ์—๊ฒŒ๋งŒ ๊ฐ€๊ฒŒ ๋ฉ๋‹ˆ๋‹ค. User 2๋ฒˆ์€ ์–ด๋– ํ•œ ์—๋Ÿฌ ๋ฉ”์„ธ์ง€๋ฅผ ๋ฐ›์ง€ ๋ชปํ•ฉ๋‹ˆ๋‹ค. ์™œ๋ƒํ•˜๋ฉด ์—ฐ๊ฒฐ๋œ ์‚ฌ์šฉ์žํ•œํ…Œ๋งŒ ๊ฐ€๋Š” emit ๋•Œ๋ฌธ์ž…๋‹ˆ๋‹ค.

์ฆ‰, Gateway์—์„œ๋Š” Pipe๋ฅผ ์ ์šฉํ•˜๊ธฐ ์œ„ํ•ด์„œ๋Š” ๊ฐ๊ฐ์˜ ๋ฉ”์†Œ๋“œ ์œ„์—๋‹ค๊ฐ€ ์ ์šฉ์„ ํ•ด์ค˜์•ผํ•ฉ๋‹ˆ๋‹ค.

๐Ÿ–Š๏ธGuard ์ ์šฉ

์ด๋ฒˆ์—๋Š” Guard๋ฅผ ์ ์šฉํ•ด๋ณด๊ฒ ์Šต๋‹ˆ๋‹ค. REST API์—์„œ ์ ์šฉํ•œ ๋ฐฉ๋ฒ•๊ณผ ๋™์ผํ•ฉ๋‹ˆ๋‹ค.

  • auth/guard/socket/socket-bearer-token.guard.ts
@Injectable()
export class SocketBearerTokenGuard implements CanActivate {

    constructor(
    	private readonly authService: AuthService,
     	private readonly userService: UsersService,
    ) {}

    async canActivate(context: ExecutionContext): Promise<boolean> {
        // ์ง€๊ธˆ ์—ฐ๊ฒฐํ•ด์„œ ์‚ฌ์šฉํ•˜๊ณ  ์žˆ๋Š” ์†Œ์ผ“
        const socket = context.switchToWs().getClient();

        // ํ—ค๋” ๊ฐ€์ ธ์˜ค๊ธฐ
        const headers = socket.handshake.headers;

        // Bearer xxx
        const rawToken = headers['authorization'];
        if (!rawToken) throw new WsException('ํ† ํฐ์ด ์—†์Šต๋‹ˆ๋‹ค. ');

        const token = this.authService.extractTokenFromHeader(
            rawToken,
            true
        );

        const payload = this.authService.verifyToken(token);
        const user = await this.userService.getUserByEmail(payload.email);

        socket.user = user;
        socket.token = token;
        socket.tokeType = payload.tokenType;
    }
}

์ด์ œ try catch๋กœ ๋ฌถ๊ฒ ์Šต๋‹ˆ๋‹ค. ์™œ๋ƒํ•˜๋ฉด ๊ธฐ๋ณธ์ ์œผ๋กœ HTTP Exception์„ ์ฃผ๊ธฐ ๋•Œ๋ฌธ์— WsException์œผ๋กœ ๋ณ€๊ฒฝ์„ ํ•ด์ค˜์•ผ ํ•ฉ๋‹ˆ๋‹ค.

@Injectable()
export class SocketBearerTokenGuard implements CanActivate {

    constructor(
    	private readonly authService: AuthService,
     	private readonly userService: UsersService,
    ) {}

    async canActivate(context: ExecutionContext): Promise<boolean> {
        const socket = context.switchToWs().getClient();
        const headers = socket.handshake.headers;
        const rawToken = headers['authorization'];
        if (!rawToken) throw new WsException('ํ† ํฐ์ด ์—†์Šต๋‹ˆ๋‹ค. ');

        try {
            const token = this.authService.extractTokenFromHeader(
                rawToken,
                true
            );

            const payload = this.authService.verifyToken(token);
            const user = await this.userService.getUserByEmail(payload.email);

            socket.user = user;
            socket.token = token;
            socket.tokeType = payload.tokenType;
            return true;
        } catch (error) {
          	throw new WsException('ํ† ํฐ์ด ์œ ํšจํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค.')
        }
    }
}

chats.module.ts์—์„œ provider๋กœ ๋“ฑ๋ก์„ ํ•˜๊ฒ ์Šต๋‹ˆ๋‹ค.

  • chats.module.ts
@Module({
    imports: [
        TypeOrmModule.forFeature([
          	ChatsModel,
          	MessagesModel,
        ]),
        CommonModule,
        AuthModule,
      	UsersModule,
    ],
    controllers: [
        ChatsController,
        MessagesController,
    ],
    providers: [
        ChatsGateway,
        ChatsService,
        ChatsMessagesService,
    ],
})
export class ChatsModule {}

์ด์ œ chats.gateway.ts์— ์ ์šฉ์„ ํ•˜๊ฒ ์Šต๋‹ˆ๋‹ค.

  • chats.gateway.ts
@UsePipes(new ValidationPipe({
    transform: true,
    transformOptions: {
      	enableImplicitConversion: true
    },
    whitelist: true,
    forbidNonWhitelisted: true,
}))
@UseFilters(SocketCatchHttpExceptionFilter)
@UseGuards(SocketBearerTokenGuard) // ์ถ”๊ฐ€
@SubscribeMessage('create_chat')
async createChat(
    @MessageBody() data: CreateChatDto,
  	// ์ธํ„ฐ์„น์…˜: user๊ฐ€ UsersModel์ด๋ผ๊ณ  ์กด์žฌํ•œ๋‹ค.
  	// ํ† ํฐ์ด ํ†ต๊ณผ๋˜๋ฉด user๊ฐ€ ์žˆ๋‹ค๋Š” ๊ฒƒ์„ ์•Œ ์ˆ˜ ์žˆ๋‹ค.
    @ConnectedSocket() socket: Socket & {user: UsersModel}, 
) {
    const chat = await this.chatsService.createChat(
      	data, 
    );
}

ํฌ์ŠคํŠธ๋งจ์œผ๋กœ ํ…Œ์ŠคํŠธ๋ฅผ ํ•˜๊ฒ ์Šต๋‹ˆ๋‹ค. ๋กœ๊ทธ์ธ์„ ํ•˜์ง€ ์•Š๊ณ  create_chat์„ ๋ˆ„๋ฅด๋ฉด ์˜ˆ์ƒํ•œ๋Œ€๋กœ ๋‚˜์˜ต๋‹ˆ๋‹ค.

๋กœ๊ทธ์ธ์„ ํ•˜๊ณ  ์‘๋‹ต๋ฐ›์€ accessToken์„ Headers์— ๋„ฃ์–ด์ฃผ๊ฒ ์Šต๋‹ˆ๋‹ค.

๋”ฐ๋ผ์„œ REST API์—์„œ ์ ์šฉํ•œ ๊ฒƒ์ฒ˜๋Ÿผ ๋˜‘๊ฐ™์ด Gateway์—์„œ๋„ Guard๋ฅผ ์ ์šฉํ•  ์ˆ˜ ์žˆ๋Š” ๊ฒƒ์„ ์•Œ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.


๐Ÿ–Š๏ธdecorator ๊ธฐ๋ฐ˜ ๋กœ์ง ๋ณ€๊ฒฝ

๋‚˜๋จธ์ง€ ๊ธฐ๋Šฅ์„ ์™„์„ฑํ•˜๊ฒ ์Šต๋‹ˆ๋‹ค. enter_chat์—๋„ Guard, Filter, Pipe๋ฅผ ์ ์šฉํ•˜๊ฒ ์Šต๋‹ˆ๋‹ค. ๋กœ๊ทธ์ธ์„ ํ•ด์•ผ๋งŒ ์ ‘๊ทผ์ด ๊ฐ€๋Šฅํ•˜๋„๋ก ๋งŒ๋“ค๊ฒ ์Šต๋‹ˆ๋‹ค.

๋˜ํ•œ send_message์—๋„ ๋™์ผํ•˜๊ฒŒ ์ ์šฉ์„ ํ•˜๊ฒ ์Šต๋‹ˆ๋‹ค.

  • chats.gateway.ts
@UsePipes(new ValidationPipe({
    transform: true,
    transformOptions: {
      	enableImplicitConversion: true
    },
    whitelist: true,
    forbidNonWhitelisted: true,
}))
@UseFilters(SocketCatchHttpExceptionFilter)
@UseGuards(SocketBearerTokenGuard)
@SubscribeMessage('send_message')
async sendMessage(
.
.
@UsePipes(new ValidationPipe({
    transform: true,
    transformOptions: {
    enableImplicitConversion: true
    },
    whitelist: true,
    forbidNonWhitelisted: true,
}))
@UseFilters(SocketCatchHttpExceptionFilter)
@UseGuards(SocketBearerTokenGuard)
@SubscribeMessage('enter_chat')
async enterChat(

์ถ”๊ฐ€์ ์œผ๋กœ CreateMessageDto์—์„œ authorId๋ฅผ number๋กœ ๋ฐ›๊ณ  ์žˆ์—ˆ์Šต๋‹ˆ๋‹ค. ์ด ๋˜ํ•œ ์ œ๊ฑฐ๋ฅผ ํ•˜๊ณ  ์ง์ ‘ ๋ฐ›๋„๋ก ํ•˜๊ฒ ์Šต๋‹ˆ๋‹ค.

  • chats/messages/dto/create-message.dto.ts
import { PickType } from "@nestjs/mapped-types";
import { MessagesModel } from "../entities/messages.entity";
import { IsNumber } from "class-validator";

export class CreateMessagesDto extends PickType(MessagesModel, [
  	'message',
]) {
    @IsNumber()
    chatId: number;
  
  	// ์ œ๊ฑฐ
}
  • messages.service.ts
async createMessage(
    dto: CreateMessagesDto,
    authorId: number
) {
    const message = await this.messagesRepository.save({
        chat: {
          	id: dto.chatId,
        },
        author: {
          	id: authorId, // ๋ณ€๊ฒฝ
        },
        message: dto.message,
    });
    return this.messagesRepository.findOne({
        where: {
          	id: message.id,
        },
        relations: {
          	chat: true,
        }
    });
}
  • chats.gateway.ts
@UsePipes(new ValidationPipe({
    transform: true, // ๋ณ€ํ™”๋Š” ํ•ด๋„ ๋œ๋‹ค.
    transformOptions: {
      	enableImplicitConversion: true // ์ž„์˜๋กœ ๋ณ€ํ™˜ํ•˜๋Š” ๊ฒƒ์„ ํ—ˆ๊ฐ€ํ•œ๋‹ค.
    },
    whitelist: true,
    forbidNonWhitelisted: true,
}))
@UseFilters(SocketCatchHttpExceptionFilter)
@UseGuards(SocketBearerTokenGuard)
@SubscribeMessage('send_message')
async sendMessage(
    @MessageBody() dto: CreateMessagesDto,
    @ConnectedSocket() socket: Socket & {user: UsersModel}, // ๋ณ€๊ฒฝ
) {
    const chatExists = await this.chatsService.checkIfChatExists(
      	dto.chatId,
    );

    if (!chatExists) {
        throw new WsException(
          	`์กด์žฌํ•˜์ง€ ์•Š๋Š” ์ฑ„ํŒ…๋ฐฉ์ž…๋‹ˆ๋‹ค. Chat ID : ${dto.chatId}`,
        );
    }

    const message = await this.messagesService.createMessage(
        dto,
        socket.user.id
    );
    socket.to(message.chat.id.toString()).emit('receive_message', message.message);
}

๋‹ค์Œ๊ณผ ๊ฐ™์ด ๋ฐ”๊พธ๋Š” ์ด์œ ๋Š” ์—‘์„ธ์Šค ํ† ํฐ์œผ๋กœ ๋ถ€ํ„ฐ ์‚ฌ์šฉ์ž ์ •๋ณด๋ฅผ ๋ฐ›์•„์˜ค๊ธฐ ์œ„ํ•ด์„œ ์ž…๋‹ˆ๋‹ค.


๐Ÿ–Š๏ธAccessToken์„ ๋งค๋ฒˆ ๊ฒ€์ฆํ• ๋•Œ์˜ ๋ฌธ์ œ

์ง€๊ธˆ๋ถ€ํ„ฐ๋Š” accessToken์„ ์ ์šฉํ•˜๋Š” ๋ฌธ์ œ์— ๋Œ€ํ•ด์„œ ์•Œ์•„๋ณด๊ฒ ์Šต๋‹ˆ๋‹ค.

๋กœ๊ทธ์ธ์„ ํ•˜๊ฒŒ ๋˜๋ฉด accessToken๊ณผ refreshToken์„ ์ œ๊ณตํ•ฉ๋‹ˆ๋‹ค. accessToken์˜ ๊ฒฝ์šฐ 5๋ถ„์ž…๋‹ˆ๋‹ค. ๊ทธ ์ดํ›„์—๋Š” ๋งŒ๋ฃŒ๊ฐ€ ๋˜์–ด์„œ ๋‹ค์‹œ refreshToken์œผ๋กœ ์žฌ๋ฐœ๊ธ‰์„ ๋ฐ›์•„์•ผํ•ฉ๋‹ˆ๋‹ค.

ํฌ์ŠคํŠธ๋งจ์˜ ๊ฒฝ์šฐ connection์„ ํ•˜๊ฒŒ๋˜๋ฉด ๋ฐ”๊ฟ€ ์ˆ˜ ์—†๊ฒŒ ๋˜์–ด๋ฒ„๋ฆฝ๋‹ˆ๋‹ค.

๊ทธ๋Ÿผ 5๋ถ„ ์•ˆ์œผ๋กœ๋Š” ์†Œํ†ต์ด ๊ฐ€๋Šฅํ•˜์ง€๋งŒ, ๊ทธ ์ดํ›„ ๋งŒ๋ฃŒ๊ฐ€ ๋˜๋ฉด ํ† ํฐ์ด ์œ ํšจํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค. ์—๋Ÿฌ๋ฅผ ๋˜์ง€๊ฒŒ ๋ฉ๋‹ˆ๋‹ค. ๊ทธ๋ž˜์„œ ๋งค๋ฒˆ ๋ฌด์—‡์ธ๊ฐ€๋ฅผ ๊ฒ€์ฆ์„ ํ•œ๋‹ค๋ฉด Guard๋ฅผ ์‚ฌ์šฉํ•˜๋Š” ๊ฒƒ์ด ๋งž์Šต๋‹ˆ๋‹ค.

ํ•˜์ง€๋งŒ Bearer Token ๊ฒ€์ฆ์„ ํ†ตํ•ด ์‚ฌ์šฉ์ž์™€์˜ socket๊ณผ ์—ฐ๊ฒฐ์—๋Š” ๋งค๋ฒˆ Guard๋ฅผ ์ด์šฉํ•œ ๊ฒ€์ฆ์„ ํ•  ํ•„์š”๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค.

์ด์ œ๋ถ€ํ„ฐ ์‚ฌ์šฉ์ž๋ฅผ ์–ด๋–ป๊ฒŒ ๊ฐ๊ฐ์˜ ์†Œ์ผ“๊ณผ ์—ฐ๊ฒฐํ•˜๋Š”์ง€๋ฅผ ์•Œ์•„๋ณด๊ฒ ์Šต๋‹ˆ๋‹ค.


๐Ÿ–Š๏ธSocket์—์„œ ์‚ฌ์šฉ์ž ์ •๋ณด ์ €์žฅ

๋จผ์ € chats.gateway.ts์— ์žˆ๋Š” Guard๋ฅผ ๋ชจ๋‘ ์ง€์›Œ์ค๋‹ˆ๋‹ค.

  • chats.gateway.ts
@WebSocketGateway({
    // ws:localhost:3000/chats
    namespace: '/chats',
})
export class ChatsGateway implements OnGatewayConnection{

    constructor(
    	private readonly chatsService: ChatsService,
     	private readonly messagesService: ChatsMessagesService
    ){}

    @WebSocketServer()
    server: Server;

    handleConnection(socket: Socket) {
      	console.log(`on connect called : ${socket.id}`);
    }

    @UsePipes(new ValidationPipe({
        transform: true,
        transformOptions: {
          	enableImplicitConversion: true
        },
        whitelist: true,
        forbidNonWhitelisted: true,
    }))
    @UseFilters(SocketCatchHttpExceptionFilter)
    @SubscribeMessage('enter_chat')
    async enterChat(
    	@MessageBody() data: EnterChatDto,
     	@ConnectedSocket() socket: Socket & {user: UsersModel},
    ) {  
        for (const chatId of data.chatIds) {
            const exists = await this.chatsService.checkIfChatExists(
              	chatId,
            );

            if (!exists) {
                throw new WsException({
                    code: 100,
                    message: `์กด์žฌํ•˜์ง€ ์•Š๋Š” chat ์ž…๋‹ˆ๋‹ค. chatId: ${chatId}`,
                });
            }
        }
        socket.join(data.chatIds.map((x) => x.toString()));
    }

    @UsePipes(new ValidationPipe({
        transform: true,
        transformOptions: {
          	enableImplicitConversion: true
        },
        whitelist: true,
        forbidNonWhitelisted: true,
    }))
    @UseFilters(SocketCatchHttpExceptionFilter)
    @SubscribeMessage('create_chat')
    async createChat(
    	@MessageBody() data: CreateChatDto,
     	@ConnectedSocket() socket: Socket & {user: UsersModel},
    ) {
        const chat = await this.chatsService.createChat(
          	data, 
        );
    }

    @UsePipes(new ValidationPipe({
        transform: true,
        transformOptions: {
          	enableImplicitConversion: true
        },
        whitelist: true,
        forbidNonWhitelisted: true,
    }))
    @UseFilters(SocketCatchHttpExceptionFilter)
    @SubscribeMessage('send_message')
    async sendMessage(
      	@MessageBody() dto: CreateMessagesDto,
       	@ConnectedSocket() socket: Socket & {user: UsersModel},
    ) {
        const chatExists = await this.chatsService.checkIfChatExists(
          	dto.chatId,
        );

        if (!chatExists) {
            throw new WsException(
             	 `์กด์žฌํ•˜์ง€ ์•Š๋Š” ์ฑ„ํŒ…๋ฐฉ์ž…๋‹ˆ๋‹ค. Chat ID : ${dto.chatId}`,
            );
        }
        const message = await this.messagesService.createMessage(
          	dto,
          	socket.user.id
        );
        socket.to(message.chat.id.toString()).emit('receive_message', message.message);
    }
}
Guard๋ฅผ ์ง€์šฐ๊ณ  ์–ด๋–ป๊ฒŒ ์‚ฌ์šฉ์ž ์ •๋ณด๋ฅผ ๊ฐ€์ง€๊ณ  ์˜ฌ ์ˆ˜ ์žˆ์„๊นŒ? 

Socket์ด๋ผ๋Š” ๊ฒƒ์€ ์—ฐ๊ฒฐ์ด ๋˜๋ฉด ์„œ๋กœ Pipe๊ฐ™์€ ๊ฒƒ์ด ์ƒ๊น๋‹ˆ๋‹ค. ๊ทธ๋ž˜์„œ ํ•œ๋ฒˆ ์—ฐ๊ฒฐ๋˜์–ด ์žˆ์œผ๋ฉด ๊ทธ ์—ฐ๊ฒฐ์€ ์ง€์†์ด ๋ฉ๋‹ˆ๋‹ค.

async handleConnection(socket: Socket & {user: UsersModel}) {
    console.log(`on connect called : ${socket.id}`); // ์–ด๋–ค id ์†Œ์ผ“์ด ์—ฐ๊ฒฐ๋จ?
  
  	// ์ž„์‹œ ํ…Œ์ŠคํŠธ
    const user = await this.usersService.getUserByEmail('codefactory@codefactory.ai');
    socket.user = user; // ์†Œ์ผ“์— ์‚ฌ์šฉ์ž ์ •๋ณด ๋„ฃ๊ธฐ
}

๋”ฐ๋ผ์„œ 1๋ฒˆ ์—ฐ๊ฒฐ๋˜๊ณ  ๋‚˜๋ฉด socket์—๋Š” ๋ฉ”์„ธ์ง€๋ฅผ ๋ณด๋‚ผ ๋•Œ ๊ณ„์†ํ•ด์„œ ์œ ์ง€๊ฐ€ ๋ฉ๋‹ˆ๋‹ค. ์ด๊ฒƒ์€ ๋””๋ฒ„๊น…์„ ํ†ตํ•ด์„œ socket์— ๋“ค์–ด์žˆ๋Š” ๊ฐ’์„ ํ™•์ธํ•ด๋ณด๋ฉด ์•Œ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

๋”ฐ๋ผ์„œ REST API์ฒ˜๋Ÿผ ๋ชจ๋“  ๊ณณ์— Guard๋ฅผ ๋ถ™์ผ ํ•„์š” ์—†์ด, handleConnection์—์„œ ํ•ธ๋“ค๋ง๋งŒ ํ•ด์ฃผ๊ณ  socket.user = user;๋กœ ๋„ฃ์–ด์ฃผ๋ฉด, ๋‚˜๋จธ์ง€ ๋ฉ”์„ธ์ง€๋ฅผ ๋ณด๋‚ผ ๋•Œ ์†Œ์ผ“์— ๋“ค์–ด์žˆ๋Š” ๊ฐ’์„ ์‹ ๋ขฐํ•  ์ˆ˜ ์žˆ๊ฒŒ ๋ฉ๋‹ˆ๋‹ค.

๋˜ํ•œ ์—ฌ๊ธฐ์„œ๋Š” ์ค‘๊ฐ„์— ์—๋Ÿฌ๊ฐ€ ๋ฐœ์ƒํ•˜๋ฉด ์—๋Ÿฌ๋ฅผ throw ํ•˜๋Š” ๊ฒƒ๋ณด๋‹ค๋Š” ์—ฐ๊ฒฐ์„ ๋Š์–ด์ฃผ๊ฒ ์Šต๋‹ˆ๋‹ค.

async handleConnection(socket: Socket & {user: UsersModel}) {
    console.log(`on connect called : ${socket.id}`); // ์–ด๋–ค id ์†Œ์ผ“์ด ์—ฐ๊ฒฐ๋จ?
    const headers = socket.handshake.headers; // ํ—ค๋” ๊ฐ€์ ธ์˜ค๊ธฐ
    const rawToken = headers['authorization']; // Bearer xxx
    if (!rawToken) socket.disconnect(); // ํ† ํฐ์ด ์—†์œผ๋ฉด ์—ฐ๊ฒฐ ๋Š๊ธฐ

    try {
        const token = this.authService.extractTokenFromHeader(
            rawToken,
            true
        );

        const payload = this.authService.verifyToken(token);
        const user = await this.usersService.getUserByEmail(payload.email);
        socket.user = user;
        return true;
    } catch (error) {
      	socket.disconnect(); // ์—๋Ÿฌ๊ฐ€ ๋‚˜๋ฉด ์—ฐ๊ฒฐ ๋Š๊ธฐ
    }
}

ํฌ์ŠคํŠธ๋งจ์œผ๋กœ ๋กœ๊ทธ์ธ์„ ํ•˜๊ณ  ํ† ํฐ์—†์ด connect๋ฅผ ํ•˜๋ฉด ๋ฐ”๋กœ ์—ฐ๊ฒฐ์ด ๋Š๊ธฐ๋Š” ๊ฒƒ์„ ์•Œ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.


๐Ÿ–Š๏ธGateway Lifecycle Hooks

์—ฐ๊ฒฐ๋˜์—ˆ์„ ๋•Œ handleconnection์„ ํ†ตํ•ด์„œ ํŠน์ • ๊ธฐ๋Šฅ์ด ๋ฐœ์ƒํ•˜๋„๋ก ๋งŒ๋“ค์—ˆ์Šต๋‹ˆ๋‹ค. ์ด๊ฒƒ์„ Lifecycle Hooks์ด๋ผ๊ณ  ํ•ฉ๋‹ˆ๋‹ค. Lifecycle Hooks์€ 2๊ฐœ๊ฐ€ ๋” ์žˆ์Šต๋‹ˆ๋‹ค.

OnGatewayInit ์ž…๋‹ˆ๋‹ค. OnGatewayInit๋Š” afterInit ํ•จ์ˆ˜๋ฅผ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๊ฒŒ ๋งŒ๋“ค์–ด ์ค๋‹ˆ๋‹ค. ์ด ํ•จ์ˆ˜๋Š” ์‹ค์ œ ์„œ๋ฒ„๋ฅผ inject ๋ฐ›์„ ๋•Œ ์‚ฌ์šฉ๋˜๋Š” ํ•จ์ˆ˜์ด๊ธฐ๋„ ํ•˜๊ณ , ๊ทธ๋ฆฌ๊ณ  Gateway๊ฐ€ ์ดˆ๊ธฐํ™”๋˜์—ˆ์„ ๋•Œ ์‹คํ–‰ํ•  ์ˆ˜ ์žˆ๋Š” ํ•จ์ˆ˜ ์ž…๋‹ˆ๋‹ค.

  • chats.gateway.ts
export class ChatsGateway implements OnGatewayConnection, OnGatewayInit{ // ์ถ”๊ฐ€

    constructor(
    	private readonly chatsService: ChatsService,
     	private readonly messagesService: ChatsMessagesService,
     	private readonly usersService: UsersService,
     	private readonly authService: AuthService,
    ){}

    @WebSocketServer()
    server: Server;

    afterInit(server: any) { // server: Server; ์ด๊ฒƒ๊ณผ ๋™์ผํ•œ ๊ฐ’์„ ๋ฐ›๋Š”๋‹ค.
      	console.log(`after gateway init`); // ๋กœ๊ทธ ํ™•์ธ
    }

after gateway init ์ดํ›„๋กœ ์†Œ์ผ“ ๋ฉ”์„ธ์ง€๋“ค์ด ๊ตฌ๋…๋œ๊ฒƒ์„ ์•Œ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ๊ทธ๋ž˜์„œ Gateway๊ฐ€ ์‹œ์ž‘ํ–ˆ์„ ๋•Œ ํŠน์ • ํ•จ์ˆ˜ ๋˜๋Š” ๋กœ์ง์„ ์‹คํ–‰ํ•˜๊ณ  ์‹ถ์œผ๋ฉด afterInit์ด๋ผ๋Š” Lifecycle Hooks์„ ์‚ฌ์šฉํ•˜๋ฉด ๋ฉ๋‹ˆ๋‹ค.

๋‹ค์Œ์€ ์—ฐ๊ฒฐ์ด ๋Š๊ธด ์ดํ›„๋ฅผ ๊ด€๋ฆฌํ•˜๋Š” OnGatewayDisconnect์ž…๋‹ˆ๋‹ค.

export class ChatsGateway implements OnGatewayConnection, OnGatewayInit, OnGatewayDisconnect{
.
.
handleDisconnect(socket: Socket) {
  	console.log(`on disconnect called : ${socket.id}`);
}

ํฌ์ŠคํŠธ๋งจ์œผ๋กœ ์—ฐ๊ฒฐ ํ›„ disconnect๋ฅผ ํ•˜๊ฒ ์Šต๋‹ˆ๋‹ค.

profile
๋ธ”๋กœ๊ทธ ์ด์ „ : https://medium.com/@jaegeunsong97
post-custom-banner

0๊ฐœ์˜ ๋Œ“๊ธ€