[Next & Node] Socket.io로 채팅 구현하기
안녕하세요
next.js / node.js / socket.io를 가지고 채팅 구현한 내용을 블로그에 담으려고 합니다.
Socket.io?
Socket.IO는 실시간, 양방향 및 이벤트 기반 통신을 위한 라이브러리입니다.
클라이언트와 서버 간의 통신을 쉽게 구현할 수 있게 해주는 툴입니다.
Socket.IO는 주로 실시간 채팅 애플리케이션, 알림 시스템, 실시간 대시보드, 온라인 게임 등에서 사용됩니다.
주요 개념
웹소켓 (WebSocket)
- 웹소켓은 클라이언트와 서버 간의 실시간 양방향 통신을 가능하게 하는 프로토콜입니다.
- HTTP와는 다르게, 웹소켓은 연결을 맺은 후 클라이언트와 서버가 자유롭게 데이터를 주고받을 수 있습니다.
- 웹소켓을 통해 브라우저에서 서버와의 실시간 통신이 가능해졌으며, 이를 활용하여 다양한 실시간 애플리케이션을 개발할 수 있습니다.
폴링(Polling)
- 폴링은 클라이언트가 일정 간격으로 서버에 요청을 보내서 데이터를 받아오는 방식입니다.
- 폴링은 초기 단계의 실시간 통신 방법으로, 서버의 부하를 증가시킬 수 있습니다.
- Socket.IO는 폴링을 사용하여 초기 연결을 맺고, 이후 웹소켓으로 업그레이드하여 최적의 성능을 제공합니다.
이벤트(Event)
- Socket.IO는 이벤트 기반 라이브러리입니다.
- 클라이언트와 서버 간에 메시지를 주고받을 때 이벤트를 사용합니다.
- 예를 들어, 클라이언트가 서버에 메시지를 보낼 때 emit 이벤트를 사용하고, 서버는 on 이벤트로 메시지를 수신합니다.
기본 사용법
설치
- 서버와 클라이언트 각각 Socket.IO, Socket.IO-Client를 설치해야 합니다.
// 서버
npm install socket.io
// 클라이언트
npm install socket.io-client
서버 적용
const express = require('express');
const http = require('http');
const { Server } = require('socket.io');
const app = express();
const server = http.createServer(app);
const io = new Server(server, {
cors: { // cors 에러 위함
origin: 'http://your-client-url', // 클라이언트의 URL
credentials: true, // 쿠키 공유 위함
},
});
io.on('connection', (socket) => {
console.log('User connected:', socket.id);
socket.on('disconnect', () => {
console.log('User disconnected');
});
socket.on('message', (msg) => {
console.log('Message from client:', msg);
io.emit('message', msg); // 모든 클라이언트에게 메시지 전송
});
});
server.listen(3000, () => {
console.log('Server listening on port 3000');
});
클라이언트 적용
import { io } from 'socket.io-client';
const socket = io('http://your-server-url', { // 서버 url
withCredentials: true, // 쿠키 공유 위함
});
socket.on('connect', () => {
console.log('Connected to server');
});
socket.on('message', (msg) => {
console.log('Message from server:', msg);
});
// 메시지를 서버로 전송
socket.emit('message', 'Hello, Server!');
채팅 만들기
socket.io에 대해서 알았으니 이제 채팅을 만들어 보려고 합니다.
소켓을 사용하는 코드가 길어지기 때문에 파일을 따로 만들고 app.js로부터 필요한 데이터를 받아서 사용하려고 합니다.
서버
/app.js
const express = require('express');
const cors = require('cors');
const { Server } = require('socket.io');
const { createServer } = require('http');
const db = require('./models');
const app = express();
const httpServer = new createServer(app);
const io = new Server(httpServer, { // io 서버 시작
cors: {
origin: 'https://zzimzzim.com',
credentials: true,
},
});
require('./utils/io)(io, db); // 필요한 데이터 넣어주기
/utils/io.js
module.exports = function (io, db) {
const userSockets = {};
const nsp = io.of('/messages');
nsp.on('connection', (socket) => {
console.log('soket connected');
socket.on('disconnect', () => {
console.log('-----------------user disconnected');
});
socket.on('login', (user) => {
userSockets[user.id] = socket.id;
console.log(`User ${user.id} connected with socket id ${socket.id} --------- 유저입장`);
})
}
}
app.js에서 받은 io를 connect하게 되면 소켓 정보를 받게 된다.
받은 socket을 가지고 필요한 이벤트를 적용할 수 있다.
채팅을 하게되면 메세지를 누구에게서 받았고, 누구에게 보낼지가 중요한데 그것은 socketId로 구분해야한다.
유저마다 다른 id부여하고 기억하기 위해서 userSockets 객체를 만들고, 로그인 후, 정보를 받으면 저장하게 만들었다.
socket.on('sendMessage', async (data) => {
try {
const roomId = data.room.split('-').sort().join('-');
// 데이터를 유저에게 보내주기 전에 db에 저장이 필요하다. 차후에 조회 위해서
const receiverSocketId = userSockets[data.receiverId];
if (receiverSocketId) {
// 유저가 로그인하고 socket이 있다면 해당 유저에게 내용을 보내준다.
socket.to(receiverSocketId).emit('receiveMessage', savedMessage);
socket.to(receiverSocketId).emit('receiveRoom', room);
} else {
console.log(`User ${data.receiverId} is not connected`);
}
} catch (error) {
console.error('Error saving message:', error);
}
});
이후 유저가 메세지를 보낼때 마다 .on('sendMessage' 이벤트로 받게 되는데
여기서 메세지들을 차후에 볼수있고, 채팅 목록들을 구분해서 볼 수 있게 하기위해서
각각 db에 저장하고, 메세지를 해당 유저에게 보내준다.
그리고 차후에 저장한 채팅 목록과 채팅 내용들을 조회할 수 있게 api를 만들어줘야 합니다!
클라이언트
클라이언트에서는 소켓을 실행하고 성공하면 그때부터 소켓 이벤트로 데이터를 보내고 받을 수 있다.
그치만 조심해야할 것이 소켓 실행은 한번만 해야한다.
여러개를 실행하게 되면 같이 공유하는 것이 아니라 더 추가되기 때문에 부하를 줄 수 있기 때문이다.
그래서 나는 useContext를 이용해서 메세지 페이지에 들어갔을때 한번 실행하고, 그것을 공유해서 쓸수 있도록 했다.
'use client'
export default function SocketProvider({ children }: Props) {
const [socket, setSocket] = useState<any | null>(null);
const [goDown, setGoDown] = useState(false);
const disconnect = useCallback(() => {
socket?.disconnect();
setSocket(null);
}, [socket]);
const value = useMemo(() => {
return { socket, disconnect, goDown, setGoDown }
}, [socket, isConnected, disconnect, goDown]);
useEffect(() => {
if (!socket) {
const socketInstance = io(`${process.env.NEXT_PUBLIC_BASE_URL}/messages`, {
withCredentials: true,
});
socketInstance.on('connect', async () => {
console.log("소켓연결 성공!!!", socketInstance.id);
})
// 로그인 되었으면 로그인 정보랑 소켓정보 보내줘!
setSocket(socketInstance);
}
}, [socket]);
return (
<SocketContext.Provider value={value}>
{children}
</SocketContext.Provider>
)
}
소켓을 연결 후에, 로그인 정보와 소켓정보를 서버에 보내줘야하는데
이를 위해서 로직을 추가해줘야 한다.
다음부터는 input에서 메세지를 보내고 받는 이벤트를 적용하면 된다.
// 예시
socket?.emit('sendMessage', {
senderId: session?.user?.id,
receiverId: id,
content: data.content,
});
여기에 추가로 리액트 쿼리나 등등에서 보낸 메세지를 화면에서 볼 수있도록 적용해줘야한다!
그와동시에 채팅을 받고, 채팅 리스트를 조회할 수 있게 하기위해서 필요한 api를 호출해서 뿌려줘야한다.
나는 react-query를 통해 이를 작업했다.
서버 컴포넌트 + 인피닛 스크롤이기 때문에 맞춰서 데이터를 prefetch해주었고 이를 하이드레이션 해주기위해 데이터를 넣어줬다.
export default async function Page() {
const queryClient = new QueryClient();
queryClient.prefetchInfiniteQuery({ queryKey: [키], queryFn: 함수명, initialPageParam: 0 });
const dehydrateState = dehydrate(queryClient);
const session = await auth();
return (
<HydrationBoundary state={dehydrateState}>
<MainTitle>채팅</MainTitle>
<Suspense fallback={<Loading />}>
<RoomSection session={session} />
</Suspense>
</HydrationBoundary>
)
}
이후에 필요에따라
useInfiniteQuery로 데이터를 받아서 사용하면 됩니다.
const {
data: messages,
isFetching,
isLoading,
isPending,
hasPreviousPage,
fetchPreviousPage,
} = useInfiniteQuery({
queryKey: [키],
queryFn: 함수명,
initialPageParam: 0,
getPreviousPageParam: (firstPage) => firstPage.length < 10 ? undefined : firstPage.at(0)?.id,
getNextPageParam: (lastPage) => lastPage.length < 10 ? undefined : lastPage.at(-1)?.id,
enabled: !!(session?.user?.email && id),
})
결과
결과 내용은 https://y-chyachya.tistory.com/136 여기서 확인 할 수 있습니다.
'next.js' 카테고리의 다른 글
[Next & Node] 프론트와 백엔드간의 쿠키 공유하기 (Cookie) (0) | 2024.06.27 |
---|---|
[Next] 유저정보 수정 후 바로 서버 세션 업데이트하기 (update server session) (0) | 2024.06.18 |
[Next] <Image>으로 레이아웃 시프트 예방 (feat. plaiceholder, skeloton) (0) | 2024.05.26 |
[Next] UseSelectedLayoutSegment / UsePathname Hook (0) | 2024.05.13 |
[Next] App Router의 폴더들 (0) | 2024.05.13 |
댓글