본문 바로가기
Next.js

[Next.js, socket.io] Next.js에서 socket.io 공식 사용하기

by goodchuck 2024. 6. 7.

목차

     

     

     Next.js에서 socket.io를 어떻게 활용할까?

    Next.js를 개발하던 도중 소켓 통신을 어떻게 할 수 있을까에 대한 생각을 했었다. 역시 웹개발이기 때문에 socket.io에서 공식으로 Next.js와 연결하는방법이 적혀있었다.

    https://socket.io/how-to/use-with-nextjs

     

    How to use with Next.js | Socket.IO

    This guide shows how to use Socket.IO within a Next.js application.

    socket.io

    그래서 해당 링크를 기반으로 socket.io통신을 하려고한다.

     

     세팅방법

     

    Server.js파일 생성

    공식문서에 따르면 그냥 프로젝트 루트에 server.js파일을 만들어주면 된다고 한다.그래서 프로젝트 최상단에 server.js파일을 만들어주었다. 코드는 공식문서의 Next.js활용법에 적혀있지만 io.on("connection")부분은 테스트를위해 코드를 더 작성하였다.

    import { createServer } from "node:http";
    import next from "next";
    import { Server } from "socket.io";
    
    const dev = process.env.NODE_ENV !== "production";
    const hostname = "localhost";
    const port = 3000;
    
    const app = next({ dev, hostname, port });
    const handler = app.getRequestHandler();
    
    app.prepare().then(() => {
      const httpServer = createServer(handler);
    
      const io = new Server(httpServer);
      let users = {};
    
      function getUsersArray() {
        return Object.keys(users).map(id => ({ id, nickname: users[id] }))
      }
    
      io.on("connection", (socket) => {
        // ...
        console.log("a user connected");
        /**
         * msg는 {type:string, nickName:string}
         */
        socket.on('joinAndLeave', (msg) => {
          console.log("joinAndLeave", msg.type, msg.nickName);
          if (msg.type === 'join' && !users[socket.id]) {
            io.emit('chat message', `${msg.nickName}님이 입장하셨습니다.`)
            users[socket.id] = msg.nickName;
          }
          // else if (msg.type === 'leave') {
          //   io.emit('chat message', `${msg.nickName}님이 퇴장하셨습니다.`)
          //   users = users.filter((user) => user !== msg.nickName);
    
          // }
    
          io.emit('users', getUsersArray());
        })
    
        socket.on('chat message', (msg) => {
          console.log('message: ' + msg);
          io.emit('chat message', msg);
        })
    
        socket.on('disconnect', () => {
          console.log('user disconnected')
          const nickName = users[socket.id];
          if (nickName) {
            io.emit('chat message', `${nickName}님이 퇴장하셨습니다.`)
            delete users[socket.id];
            io.emit('users', getUsersArray());
          }
        })
    
      });
    
      httpServer
        .once("error", (err) => {
          console.error(err);
          process.exit(1);
        })
        .listen(port, () => {
          console.log(`> Ready on http://${hostname}:${port}`)
        })
    })

     

    socket.js 파일 생성

    /src 폴더에 클라이언트가 사용할 socket.js파일을 작성해준다. 아래 코드처럼 단 3줄이면 된다.

    "use client";
    
    import { io } from "socket.io-client";
    
    export const socket = io();

     

    Socket.tsx  및 Socket.styles.ts 파일 생성

     

     Socket.tsx

    /src/containers/Socket/Socket.tsx파일을 생성해준다.

    'use client';
    
    import { MainLayout } from '@/layout/MainLayout';
    import { v4 as uuidv4 } from 'uuid';
    import { useEffect, useState } from 'react';
    import { faker } from '@faker-js/faker';
    import StyledSocket from './Socket.styles';
    import { socket } from '../../socket.js';
    
    interface MessageLog {
      room: string;
      message: string;
      nickName: string;
    }
    
    export default function SocketPage() {
      const [nickName, setNickName] = useState(faker.internet.userName());
      const [isConnected, setIsConnected] = useState(false);
      const [transport, setTransport] = useState('N/A');
      const [logs, setLogs] = useState<string[]>([]);
      const [message, setMessage] = useState('');
      const [users, setUsers] = useState<string[]>([]);
      useEffect(() => {
        function onConnect() {
          setIsConnected(true);
          setTransport(socket.io.engine.transport.name);
    
          socket.io.engine.on('upgrade', (transport) => {
            setTransport(transport.name);
          });
        }
    
        if (socket.connected) {
          onConnect();
          socket.emit('joinAndLeave', { type: 'join', nickName });
        }
    
        function onDisconnect() {
          setIsConnected(false);
          setTransport('N/A');
        }
    
        socket.on('connect', onConnect);
        socket.on('disconnect', onDisconnect);
    
        function onSetLog(msg: MessageLog | string) {
          if (typeof msg === 'string') {
            setLogs((prevLogs) => [...prevLogs, msg]);
          } else {
            setLogs((prevLogs) => [
              ...prevLogs,
              `${msg.nickName}의 말: ${msg.message}`,
            ]);
          }
        }
    
        socket.on('chat message', onSetLog);
    
        function onUsers(inputUsers: { id: string; nickname: string }[]) {
          console.log({ inputUsers });
          setUsers(inputUsers.map((inputUser) => inputUser.nickname));
        }
        socket.on('users', onUsers);
    
        return () => {
          socket.off('connect', onConnect);
          socket.off('disconnect', onDisconnect);
          socket.off('chat message', onSetLog);
          socket.off('users', onUsers);
        };
      }, [nickName]);
    
      const onClickSubmitBtn = () => {
        socket.emit('chat message', { room: 'room1', message, nickName });
        setMessage('');
      };
      return (
        <MainLayout title="Socket 페이지!" tags={['next.js', 'socket']}>
          <StyledSocket>
            <p>Status: {isConnected ? 'connected' : 'disconnected'}</p>
            <p>Transport : {transport}</p>
            <div className="socket-nickname-container">
              <h3>닉네임</h3>
              <input
                type="text"
                placeholder="닉네임을 적어주세요"
                value={nickName}
                onChange={(e) => setNickName(e.target.value)}
                disabled
              />
            </div>
    
            <div className="socket-message-container">
              <h3>메시지</h3>
              <input value={message} onChange={(e) => setMessage(e.target.value)} />
              <button type="submit" onClick={onClickSubmitBtn}>
                전송
              </button>
            </div>
    
            <div className="socket-logs-container">
              <div className="socket-logs-container__left">
                <h3>채팅 로그</h3>
                {logs && logs.map((log) => <p key={uuidv4()}>{log}</p>)}
              </div>
    
              <div className="socket-logs-container__right">
                <h3>접속 유저</h3>
                {users && users.map((user) => <p key={uuidv4()}>{user}</p>)}
              </div>
            </div>
          </StyledSocket>
        </MainLayout>
      );
    }

     

     Socket.styles.ts

    /src/containers/Socket/Socket.styles.ts파일을 작성해준다.

    import styled from 'styled-components';
    
    const StyledSocket = styled.div`
      display: flex;
      flex-direction: column;
      gap: 10px;
    
      .socket-nickname-container {
        display: flex;
        gap: 10px;
      }
    
      .socket-message-container {
        display: flex;
        gap: 10px;
    
        input {
          width: 80%;
        }
      }
    
      .socket-logs-container {
        display: flex;
        gap: 5px;
        background-color: white;
        .socket-logs-container__left {
          width: 70%;
          border: 1px solid gray;
          padding: 10px;
        }
    
        .socket-logs-container__right {
          width: 30%;
          border: 1px solid gray;
          padding: 10px;
        }
      }
    `;
    
    export default StyledSocket;

     

    socket라우트에 적용

     

    import SocketPage from '@/containers/Socket/Socket';
    
    export default function Page() {
      return <SocketPage />;
    }

    이제 Next.js socket 라우터에 아까만든 Socket container를 적용해주면 된다.

     

    package.json변경

    {
      "scripts": {
    -   "dev": "next dev",
    +   "dev": "node server.js",
        "build": "next build",
    -   "start": "next start",
    +   "start": "NODE_ENV=production node server.js",
        "lint": "next lint"
      }
    }

    server.js파일을 추가하였기 때문에 실행은 이제 그냥 next dev가 아닌 node server.js로 실행하게 해야 한다. 이부분을 주의해야한다.

     

     구현한 기능

    • 입장 및 퇴장
      • 클라이언트가 입장 또는 퇴장시 모두에게 알림
    • 채팅
      • 접속한 유저들에게 채팅이 보여줌
      • 채팅 작성 가능
    • 접속유저
      • 접속한 유저들의 리스트를 알려줌

     

     작동영상