웹개발

[Next.js] socket.io 를 사용하여 실시간 양방향 통신하기

Heeyeon Choi 2025. 1. 2. 17:56
728x90

1. socket.io 설치하기

npm install socket.io socket.io-client

 

2. Next.js API Route에 Socket.io 서버 설정

: pages/api/socket.ts를 생성하여 Socket.io 서버를 설정합니다.

import { Server as IOServer, Socket } from "socket.io";
import { NextApiRequest, NextApiResponse } from "next";
import { Server as HTTPServer } from "http";
import { connectDB } from "@/util/database";
import { getServerSession } from "next-auth";
import { authOptions } from "../api/auth/[...nextauth]";
import { ObjectId } from "mongodb";

type NextApiResponseWithSocket = NextApiResponse & {
  socket: {
    server: HTTPServer & {
      io?: IOServer;
    };
  };
};

interface ChatMessage {
  chatRoomId: string;
  sender: string;
  text: string;
}

let io: IOServer | undefined;

export default async function handler(req: NextApiRequest, res: NextApiResponseWithSocket) {
  if (!res.socket.server.io) {
    console.log("Socket.io server starting...");
    io = new IOServer(res.socket.server, {
      cors: {
        origin: "http://localhost:3000", // 클라이언트 URL
        methods: ["GET", "POST"],
      },
    });
    res.socket.server.io = io;

    io.on("connection", async (socket: Socket) => {
      console.log("A user connected:", socket.id);

      // 세션에서 사용자 정보 가져오기
      const session = await getServerSession(req, res, authOptions);

      if (!session) {
        console.log("Unauthorized user tried to connect");
        socket.disconnect(); // 세션 없는 사용자는 연결 차단
        return;
      }

      const client = await connectDB;
      const db = client.db("StellarLink");

      const requesterEmail = session.user?.email;
      const requester = await db.collection("user_cred").findOne({ email: requesterEmail });

      if (!requester) {
        console.log("User not found in database");
        socket.disconnect();
        return;
      }

      const requesterId = requester._id.toString(); // 사용자 ID를 문자열로 저장
      console.log(`User ${requesterId} (${requesterEmail}) connected`);

      // Join a room
      socket.on("join_room", (room: string) => {
        socket.join(room);
        console.log(`User ${requesterId} joined room ${room}`);
      });

      // Handle incoming messages
      socket.on("send_message", (data: ChatMessage) => {
        const message = {
          ...data,
          sender: requesterId, // 실제 사용자 ID를 추가
        };
        console.log("Message received:", message);
        io?.to(data.chatRoomId).emit("receive_message", message);
      });

      socket.on("disconnect", () => {
        console.log(`User ${requesterId} disconnected`);
      });
    });
  } else {
    console.log("Socket.io server already running.");
  }

  res.end();
}

- 소켓이 이미 열려있다면 재생성하지 않도록 합니다.

 

3. Socket.io 클라이언트 설정

'use client';
import { usePathname,useSearchParams } from 'next/navigation';
import Image from 'next/image';
import { useSession } from 'next-auth/react';
import { useEffect,useState } from 'react';
import { io, Socket } from "socket.io-client";

export default function Detail() {
  const { data: session, status } = useSession();
  const searchParams = useSearchParams();
  const pathname = usePathname();
  const [chatRoomId, setChatRoomId] = useState<string | null>(null);
  const [socket, setSocket] = useState<Socket | null>(null);
  const [messages, setMessages] = useState<{ sender: string; text: string }[]>([]);
  const [input, setInput] = useState<string>("");

  // URL 파라미터에서 chatRoomId 가져오기
  useEffect(() => {
    const id = searchParams?.get("chatRoomId");
    console.log(`
      id
      `,id)
    if (id) {
      setChatRoomId(id);
    }
  }, [searchParams]);

  useEffect(() => {
    if (chatRoomId && !socket) {
      const newSocket = io("http://localhost:3000", {
        path: "/api/socket", // Next.js API Route와 연결
      });
      setSocket(newSocket);
  
      console.log("Socket initialized:", newSocket);
  
      newSocket.on("connect", () => {
        console.log("Connected to Socket.io server");
        newSocket.emit("join_room", chatRoomId);
      });
  
      newSocket.on("receive_message", (message) => {
        console.log("New message received:", message);
        setMessages((prev) => [...prev, message]);
      });
  
      return () => {
        newSocket.disconnect();
      };
    }
  }, [chatRoomId, socket]);

  const handleSendMessage = () => {
    if (socket && input.trim() && chatRoomId) {
      const message = {
        chatRoomId,
        text: input,
      };
  
      // 서버로 메시지 전송 (sender는 서버에서 처리)
      socket.emit("send_message", message);
      setMessages((prev) => [...prev, { sender: session?.user?.name|| "Me", text: input }]); // UI 업데이트
      setInput(""); // 입력 초기화
    }
  };

  return (
    <div className="w-full h-full flex items-center justify-center">
      
      {chatRoomId ?(
        <div className='text-black'>
          <div>
            <h1>Chat Room {chatRoomId}</h1>
            <div>
              {messages.map((msg, index) => (
                <div key={index}>
                  <strong>{msg.sender}</strong>: {msg.text || "No message"}
                </div>
              ))}
            </div>
            <input
              type="text"
              value={input}
              onChange={(e) => setInput(e.target.value)}
              onKeyDown={(e) => e.key === "Enter" && handleSendMessage()}
            />
            <button onClick={handleSendMessage}>Send</button>
          </div>
        </div>
      ):(
        <Image
        src="/SVG/bigLogo.svg"
        alt="select"
        width={339}
        height={199}
        priority
        className=""
      />
      )}
      
    </div>
  );
}

- 우선, 채팅방 아이디가 존재하는지 검사합니다.

- 채팅방 아이디 혹은 소켓이 변경되면 소켓을 연결하고 메세지를 보낼 수 있도록 합니다.

- 이때,

const newSocket = io("http://localhost:3000", {
path: "/api/socket", // Next.js API Route와 연결
});

 

를 꼭 해줘야합니다. 

저희가 정의한 소켓 서버가 /api/socket 

에 존재하기 때문에, 꼭 path 설정을 해주세요!!! 그래야 소켓 연결가능합니다. 

(필자는 이 부분에서 한 시간 넘게 헤맸습니다 ㅜㅜㅜㅜㅜ 꼭 부탁드립니다)

 

4. 테스트하기

728x90