[Next & Node] Socket.io로 채팅 구현하기

    728x90
    반응형

    [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 여기서 확인 할 수 있습니다.

     

     

     

    728x90
    반응형

    댓글