인프런 커뮤니티 질문&답변

뱅준님의 프로필 이미지
뱅준

작성한 질문수

Slack 클론 코딩[백엔드 with NestJS + TypeORM]

socketio 질문 있습니다.

작성

·

366

·

수정됨

0

안녕하세요. 마이크로 서비스를 만들어 보고있습니다. 서버끼리는 gRPC 통신을 사용하고, 클라이언트는 게이트웨이를 통해서 HTTP통신을 하는 형태로 만들고있습니다.

요구사항은 이제 어느정도 다 구현이 된 상태인데요. 데이터베이스의 데이터를 모두 보여주는 화면에서 새로운 데이터가 입력됐을때 실시간으로 보여주기 위해서 웹 소켓을 사용하려고 합니다.

그래서 게이트웨이에 웹소켓 게이트웨이를 추가했습니다.

 

import { Inject, OnModuleInit } from '@nestjs/common';
import {
  MessageBody,
  SubscribeMessage,
  WebSocketGateway,
  WebSocketServer,
} from '@nestjs/websockets';
import { Server } from 'socket.io';
import { CounselServiceClient } from './../../../proto/src/counsel';
import { ClientGrpc } from '@nestjs/microservices';
import { firstValueFrom } from 'rxjs';

@WebSocketGateway({ cors: { origin: '*' } })
export class EventsGateWay implements OnModuleInit {
  counselService: CounselServiceClient;

  constructor(@Inject('COUNSEL_PACKAGE') private clientCounsel: ClientGrpc) {}

  onModuleInit() {
    this.counselService =
      this.clientCounsel.getService<CounselServiceClient>('CounselService');
  }

  @WebSocketServer()
  server: Server;

  @SubscribeMessage('getAllCounselAdmin')
  async getAllCounselAdmin() {
    const result = await firstValueFrom(
      this.counselService.getAllCounselAdmin({})
    );
    this.server.emit('allCounselAdminData', result);
  }

  @SubscribeMessage('getCounselAdmin')
  async getCounselAdmin(@MessageBody() data: any) {
    const counselId = data.counselId;
    const result = await firstValueFrom(
      this.counselService.getCounselAdmin({ counselId })
    );

    this.server.emit('counselData', result);
  }
}

이게 지금 웹소켓 게이트웨이의 코드입니다. 클라이언트와 연결하기 위해서

'use client';

import { useEffect, useState } from 'react';
import { CounselProto } from './../../../proto/src/counsel';
import axios from 'axios';
import Long from 'long';
import Link from 'next/link';
import { io } from 'socket.io-client';

export const statusInfoMap: { [key: number]: string } = {
  1: '',
  2: '',
  3: '',
  4: '',
  5: '',
};

export default function GetAllCounselByAdmin() {
  const [counselData, setCounselData] = useState<CounselProto[]>([]);
  const [isLoading, setIsLoading] = useState(false);

  useEffect(() => {
    const socket = io('http://localhost:3000');
    socket.on('connect', () => {
      console.log('Connected to the server');
    });

    socket.on('allCounselAdminData', (data) => {
      console.log('Received data: ', data);
      setCounselData(data);
    });

    socket.on('disconnect', () => {
      console.log('Disconnected from the server');
    });

    return () => {
      socket.disconnect();
    };
  }, []);

  return (
    <div>
      {isLoading ? (
        <p>Loading...</p>
      ) : (
        <>
          <h1>모든 상담 내역 조회</h1>
          <table>
            <thead>
              <tr>
                <th>[상담 기록 번호]</th>
                <th>[상담 접수 내용]</th>
                <th>[상담 진행 상황]</th>
                <th>[상담 접수 날짜]</th>
                <th>[고객 고유 번호]</th>
                <th>[고객 성함]</th>
                <th>[고객 연락처]</th>
              </tr>
            </thead>
            <tbody>
              {counselData?.map((item, i) => (
                <tr key={i}>
                  <td>
                    <Link href={`/admin/counsel/${item.counselId}`}>
                      {item.counselId}
                    </Link>
                  </td>
                  <td>{item.content}</td>
                  <td>{statusInfoMap[item.statusInfo]}</td>
                  <td>{item.createdAt.toLocaleString()}</td>
                  <td>{item.counselUserId}</td>
                  <td>{item.name}</td>
                  <td>{item.phone}</td>
                </tr>
              ))}
            </tbody>
          </table>
        </>
      )}
    </div>
  );
}

이렇게 적고 서버를 실행해서 해당 페이지에 접속을 하니까 콘솔에 Connected to the server라고는 뜨는데, 데이터가 꽂히지 않고있습니다.

서버로 부터 데이터를 못 받아오고있는것같은데요. 어떤 부분을 손대야할지 모르겠습니다. 해당 소켓 게이트웨이는 공식문서를 참고해서 적었습니다..

답변 2

0

뱅준님의 프로필 이미지
뱅준
질문자

삭제된 글입니다

제로초(조현영)님의 프로필 이미지
제로초(조현영)
지식공유자

클라이언트에서 게이트웨이 네임스페이스를 연결하면 되고요. 서버쪽에서 클라이언트로 emit해주어야 합니다. 게이트웨이에서 구독하는 건 클라이언트에서 emit한 이벤트를 구독하는 거라 아무 상관이 없습니다.

근데 아직도 왜 @Get('/admin/counsel')이 필요한지 전혀 이해가 안 됩니다. getAllCounselAdmin은 또 왜 필요한건가요? 분명 "새 데이터 입력 시 실시간으로 보여준다"라고 하셨는데 지금 저 라우터는 데이터 입력이 아니라 데이터 전체 조회잖아요.

클라이언트에서 네임스페이스를 연결하신 걸 보여주시고, 백엔드에서는 데이터를 입력하는 메서드를 보여주셔야죠. 데이터 입력하는 메서드에서 데이터 입력 후 그 데이터를 emit해주어야 실시간으로 전송이 되죠.

뱅준님의 프로필 이미지
뱅준
질문자

아, 오해가 있었던 것 같습니다. getAllCounselAdmin은 전체 데이터를 조회하는 메서드 입니다.

@Get('/admin/counsel') 경로로 접속했을때, 대시보드에 현재 데이터베이스에 있는 데이터가 가공되어서 클라이언트에 보여지는데요.

새로운 데이터가 입력 됐을 때, 새로 고침 없이도 데이터 베이스에 들어온 새로운 데이터가 포함되어 전체 조회 되는 것이 제 의도였습니다.

그래서, 실시간으로 조회를 해야 한다는 생각에 데이터를 전체 조회하는 곳에서 socket을 사용하려고 했던 것 입니다.

선생님 말씀으로는, 데이터가 입력될 때 emit되어야 @Get('/admin/counsel')이 경로에서도 실시간으로 추가된 데이터까지 조회가 가능하다는 말씀이시죠?

 

// 클라이언트
export default function SocketGetAllCounselByAdmin() {
  const [counselData, setCounselData] = useState<CounselProto[]>([]);
  const [isLoading, setIsLoading] = useState(false);

  const socket = io('http://localhost:3001', {
    transports: ['websocket'],
  });
  useEffect(() => {
    socket.on('connect', () => {
      console.log('client: 서버 연결 완료');
    });

    socket.on('allCounselAdminData', ({ data }) => { // 네임스페이스와 연결
      console.log('data: ', data);
      setCounselData(data);
    });

    socket.on('disconnect', () => {
      console.log('client: 연결 해제 완료');
    });

    return () => {
      socket.disconnect();
    };
  }, [socket]);

  return (
    <div>
      {isLoading ? (
        <p>Loading...</p>
      ) : (
        <>
          <h1>모든 상담 내역 조회</h1>
          <table>
            <thead>
              <tr>
                <th>[상담 기록 번호]</th>
                <th>[상담 접수 내용]</th>
                <th>[상담 진행 상황]</th>
                <th>[상담 접수 날짜]</th>
                <th>[고객 고유 번호]</th>
                <th>[고객 성함]</th>
                <th>[고객 연락처]</th>
              </tr>
            </thead>
            <tbody>
              {counselData?.map((item, i) => (
                <tr key={i}>
                  <td>
                    <Link href={`/admin/counsel/${item.counselId}`}>
                      {item.counselId}
                    </Link>
                  </td>
                  <td>{item.content}</td>
                  <td>{statusInfoMap[item.statusInfo]}</td>
                  <td>{item.createdAt.toLocaleString()}</td>
                  <td>{item.counselUserId}</td>
                  <td>{item.name}</td>
                  <td>{item.phone}</td>
                </tr>
              ))}
            </tbody>
          </table>
        </>
      )}
    </div>
  );
}

새로운 데이터를 입력하는 서버는 이렇게 되어있습니다.

  // 1. 클라이언트에서 요청을 받아서 바디로 받은 데이터를 넘김
  @UseGuards(JwtAuthGuard)
  @Post('/counsel')
  async takeCounsel(@Req() req, @Body() { content }) {
    const user = req.user;
    const ip = req.ip;
    return this.counselService.userTakeCounsel({ user, ip, content });
  }

// 2. 요청받은대로 데이터를 저장함
 @GrpcMethod()
  async userTakeCounsel(data: requestUser): Promise<CounselUserWithContent> {
    const { user, ip, content } = data;
    const { userId, name, phone } = user;

    if (!content) {
      return { status: 'FAIL_3' };
    }

    if (ip === '::1') {
      const checkPhone = await this.counselRepository.findOne({
        where: { phone },
      });

      if (checkPhone) {
        return { status: 'FAIL_1' };
      }

      const takeCounsel = await this.counselRepository.save({
        counselUserId: userId,
        name,
        phone,
        ip,
        content,
      });

      await this.historyRepository.save({
        counsel: takeCounsel,
        historyCounselId: takeCounsel.counselId,
        statusInfo: takeCounsel.statusInfo,
        counselUserId: userId,
        name: takeCounsel.name,
        phone: takeCounsel.phone,
      });

      webhook();

      return { name, ip, phone, status: 'success' };
    } else if (ip !== '::1') {
      if (!content) {
        return { status: 'FAIL_3' };
      }

      const checkIp = await this.counselRepository.findOne({ where: { ip } });

      if (checkIp) {
        return { status: 'FAIL_2' };
      }

      const checkPhone = await this.counselRepository.findOne({
        where: { phone },
      });

      if (checkPhone) {
        return { status: 'FAIL_1' };
      }

      const takeCounsel = await this.counselRepository.save({
        counselUserId: userId,
        name,
        phone,
        ip,
        content,
      });

      await this.historyRepository.save({
        counsel: takeCounsel,
        historyCounselId: takeCounsel.counselId,
        statusInfo: takeCounsel.statusInfo,
        counselUserId: userId,
        name: takeCounsel.name,
        phone: takeCounsel.phone,
      });

      webhook();
      return { name, ip, phone, status: 'success' };
    }
  }

그럼 여기에 emit이 들어가야 하는 건가요?

0

제로초(조현영)님의 프로필 이미지
제로초(조현영)
지식공유자

새로운 데이터가 입력됐을때 실시간으로 보여주기 위한 코드는 어디에 있는거죠?

뱅준님의 프로필 이미지
뱅준
질문자

api/admin/counsel 경로로 접속하면 서버에서 배열로 보내주고있습니다

제로초(조현영)님의 프로필 이미지
제로초(조현영)
지식공유자

잘 이해가 안 되는 것이, 실시간으로 받아온다 하셨는데 왜 링크에 접속하시는건가요..?

뱅준님의 프로필 이미지
뱅준
질문자

이런식으로 하는게 아닌가보네요 ㅠㅠ 더 찾아보고 오겠습니다

제로초(조현영)님의 프로필 이미지
제로초(조현영)
지식공유자

받는 입장에서는 소켓 연결만 하면 따로 뭘 하지 않아도 알아서 서버에서 데이터가 와야합니다. 서버에서 데이터 넣자마자 그 아래줄에 소켓 emit하시면 돼요

뱅준님의 프로필 이미지
뱅준
질문자

제가 지금 생각을 해봤는데, 데이터를 받아오는 부분이 없어서, 소켓 연결을 해도 데이터를 받지 못했던 것 같습니다.

 

// 1. 클라이언트에서 요청을 받아서 넘김 
@Get('/admin/counsel')
  getAllCounselAdmin() {
    return this.counselService.getAllCounselAdmin({});
  }

// 2.받은 요청에 따라 데이터를 가공해서 넘김

  @GrpcMethod()
  async getAllCounselAdmin(): Promise<RepeatedCounselProtoAll> {
    const findCounsel = await this.counselRepository.find();

    const counsels = findCounsel.map((counsel) => ({
      counselId: counsel.counselId,
      statusInfo: counsel.statusInfo,
      name: counsel.name,
      phone: counsel.phone,
      content: counsel.content,
      createdAt: counsel.createdAt.getTime(),
      updatedAt: counsel.updatedAt.getTime(),
      counselUserId: counsel.counselUserId,
    }));

    return { counsels };
  }

서버에서 보내주는 데이터는 이렇게 처리하고 있습니다.

소켓 emit을 하는 부분은 2번의 return {counsels} 전에 해주면 되는걸까요?

웹 소켓을 이용하기 위해서는 게이트웨이에서 네임스페이스를 만들고, 구독을 하고있어야 실시간으로 데이터가 가는것이라고 이해 했었습니다 ㅜㅜ

그래서 이렇게 작성했습니다...

@WebSocketGateway(3001, { namespace: 'AdminCounsel', cors: { origin: '*' } })
export class EventsGateWay
  implements
    OnModuleInit,
    OnGatewayInit,
    OnGatewayConnection,
    OnGatewayDisconnect
{
  counselService: CounselServiceClient;

  constructor(@Inject('COUNSEL_PACKAGE') private clientCounsel: ClientGrpc) {}

  onModuleInit() {
    this.counselService =
      this.clientCounsel.getService<CounselServiceClient>('CounselService');
  }

  @WebSocketServer()
  server: Server;
  private logger: Logger = new Logger('EventsGateway');

  @SubscribeMessage('allCounselAdminData')
  async getAllCounselAdmin() {
    const result = await firstValueFrom(
      this.counselService.getAllCounselAdmin({})
    );
    console.log('Emitting allCounselAdminData:', result);
    this.server.emit('allCounselAdminData', result);
  }

  afterInit(server: Server) {
    this.logger.log('server: 웹 소켓 서버 초기화');
  }

  handleDisconnect(client: Socket) {
    this.logger.log(`server: Client Disconnected : ${client.id}`);
  }

  handleConnection(client: Socket, ...args: any[]) {
    this.logger.log(`server: Client Connected : ${client.id}`);
  }
}

getAllCounselAdmin을 호출해서 리턴받은 데이터를 emit하는 코드 입니다

제로초(조현영)님의 프로필 이미지
제로초(조현영)
지식공유자

삭제하신 글에 답변 참조하세요.

뱅준님의 프로필 이미지
뱅준

작성한 질문수

질문하기