작성
·
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
아, 오해가 있었던 것 같습니다. 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
제가 지금 생각을 해봤는데, 데이터를 받아오는 부분이 없어서, 소켓 연결을 해도 데이터를 받지 못했던 것 같습니다.
// 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하는 코드 입니다
클라이언트에서 게이트웨이 네임스페이스를 연결하면 되고요. 서버쪽에서 클라이언트로 emit해주어야 합니다. 게이트웨이에서 구독하는 건 클라이언트에서 emit한 이벤트를 구독하는 거라 아무 상관이 없습니다.
근데 아직도 왜 @Get('/admin/counsel')이 필요한지 전혀 이해가 안 됩니다. getAllCounselAdmin은 또 왜 필요한건가요? 분명 "새 데이터 입력 시 실시간으로 보여준다"라고 하셨는데 지금 저 라우터는 데이터 입력이 아니라 데이터 전체 조회잖아요.
클라이언트에서 네임스페이스를 연결하신 걸 보여주시고, 백엔드에서는 데이터를 입력하는 메서드를 보여주셔야죠. 데이터 입력하는 메서드에서 데이터 입력 후 그 데이터를 emit해주어야 실시간으로 전송이 되죠.