socket.io, stomp, sockJS 는 실시간 통신을 구현할 수 있는 라이브러리 이다.
STOMP는 주로 메시지 브로커를 통한 메시징 패턴에 적합하며, Socket.IO는 서버와 클라이언트 간의 직접적인 실시간 통신에 최적화 되어 있다.
socket.io 를 사용하기 위해서는 socket 서버와 클라이언트가 필요하다.
NextJS 에서는 node.js 처럼 api 서버를 띄울 수 있기에 socket 서버를 만들어 client와 연결을 해보려고 한다.
Next.JS socket.io 서버 구동
Next.JS app routes 에서는 Socket.io 구동x
Next.JS Pages Routes 에서 Socket.io 구동o
// pages/api/socket/io.ts
import { ServerToClientEvents } from "@/types/socket";
import { Server as NetServer } from "http";
import { Socket } from "net";
import { NextApiRequest, NextApiResponse } from "next";
import { Server as ServerIO } from "socket.io";
export type NextApiResponseServerIO = NextApiResponse & {
socket: Socket & {
server: NetServer & {
io: ServerIO<ServerToClientEvents>;
};
};
};
const ioHandler = (req: NextApiRequest, res: NextApiResponseServerIO) => {
if (!res.socket.server.io) {
const httpServer = res.socket.server as NetServer;
const io = new ServerIO(httpServer, {
path: "/api/socket/io",
addTrailingSlash: false,
cors: {
origin: "*",
methods: ["GET", "POST"],
},
});
res.socket.server.io = io;
}
res.end();
};
export default ioHandler;
위와 같이 socket 서버를 띄운 다음 client 에서 socket connect 하면 된다.
채팅을 위한 api 또한 필요하기에 chat.ts 를 만들어 요청 받은 메시지를 처리하는 handler 를 만들었다.
import { NextApiRequest } from "next";
export default function handler(req: NextApiRequest, res: any) {
if (req.method === "POST") {
// 메시지 얻기
const message = req.body;
// on('message')가 메시지를 받음
res?.socket?.server?.io?.emit("message", JSON.parse(message));
res.status(201).json(message);
}
}
Next.JS 클라이언트 코드
아래 코드에서는 SocketProvider를 만들어 props 로 socket을 내리지 않고
useSocket() 커스텀 훅을 만들어 provider 안에서 socket을 가져와 사용했다.
"use client";
import { createContext, useContext, useEffect, useState } from "react";
import { io, Socket } from "socket.io-client";
const SocketContext = createContext<{
socket: Socket | null;
isConnected: boolean;
} | null>(null);
export const useSocket = () => {
const context = useContext(SocketContext);
if (context === null) {
throw new Error("useSocket은 SocketProvider내에서 사용해야 합니다.");
}
return context;
};
const SocketProvider = ({ children }: { children: React.ReactNode }) => {
const [socket, setSocket] = useState<Socket | null>(null);
const [isConnected, setIsConnected] = useState(false);
useEffect(() => {
const socket = io(process.env.NEXT_PUBLIC_SITE_URL!, {
path: "/api/socket/io",
addTrailingSlash: false,
});
socket.on("connect", () => {
setIsConnected(true);
});
socket.on("error", (error: Error) => {
console.error(error);
});
socket.on("disconnect", () => {
setIsConnected(false);
});
setSocket(socket);
return () => {
if (socket) {
socket.disconnect();
}
};
}, []);
return (
<SocketContext.Provider value={{ socket, isConnected }}>
{children}
</SocketContext.Provider>
);
};
export default SocketProvider;
Chat.tsx 라는 컴포넌트에서 간단하게 메시지를 입력하고 socket.io 를 통해 전송하는 로직이다.
"use client";
import { useSocket } from "@/components/provider/SocketProvider";
import { useEffect, useState } from "react";
export const Chat = () => {
const { socket } = useSocket();
const [message, setMessage] = useState("");
const [chat, setChat] = useState<{ user: string; content: string }[]>([]);
useEffect(() => {
socket?.on("message", (msg: { user: string; content: string }) => {
setChat((prevChat) => [...prevChat, msg]);
});
}, [socket]);
const sendMessage = async () => {
if (message) {
const res = await fetch("/api/chat", {
method: "POST",
body: JSON.stringify({
user: "user",
content: message,
}),
});
if (res.ok) setMessage("");
}
};
return (
<div className="flex flex-col">
<div className="flex flex-col h-64 overflow-y-auto mb-4 p-4 bg-white border border-gray-200">
{chat.map((msg, index) => (
<div key={index} className="text-sm text-gray-800 flex">
<span className="mr-1">{msg.user}:</span>
<span>{msg.content}</span>
</div>
))}
</div>
<div className="flex">
<input
type="text"
className="flex-1 p-2 border border-gray-300 text-slate-950"
value={message}
onChange={(e) => setMessage(e.target.value)}
onKeyDown={(e) => e.key === "Enter" && sendMessage()}
/>
<button
onClick={sendMessage}
className="ml-2 px-4 py-2 bg-blue-500 text-white font-semibold rounded hover:bg-blue-700"
>
보내기
</button>
</div>
</div>
);
};
위에 page.tsx
파일에서 Provider
를 최상단에 넣고 아래에<SocketStatus />
와 <Chat />
를 children
으로 넣어서 사용한다.
"use client";
import { Chat } from "@/components/Chat";
import SocketProvider, {
useSocket,
} from "@/components/provider/SocketProvider";
// Socket 상태를 보여주는 간단한 컴포넌트
const SocketStatus = () => {
const { isConnected } = useSocket(); // 커스텀 훅을 통해 소켓 연결 상태 가져오기
return (
<div
className={`p-4 ${
isConnected ? "bg-green-500" : "bg-red-500"
} text-white rounded-lg`}
>
{isConnected ? "연결됨" : "연결 끊김"}
</div>
);
};
// Home 컴포넌트
export default function Home() {
return (
<SocketProvider>
<main className="flex min-h-screen flex-col items-center justify-center p-24 space-y-4">
<div className="text-center">
<h1 className="text-2xl font-bold">Socket.io 예제</h1>
<SocketStatus /> {/* 소켓 연결 상태를 보여주는 컴포넌트 */}
</div>
<Chat />
</main>
</SocketProvider>
);
}
정리
위에 코드와 같이 Next.js 에서 간단하게 socket 서버를 띄우고 client 와 채팅 통신을 하는 로직을 구현했는데
만약 아래와 같은 상황이라면?
- socket 통신이 1초에 10,000 ~ 1,000,000 인 대규모 처리를 해야할 때
- 메시지 처리의 확장성과 신뢰성을 높여야 할 때
위와 같은 상황일 때 Kafka 나 RabbitMQ 를 사용해야 할 정도로 대규모 시스템 이라면 어떻게 처리하고 아키텍처를 어떤 구조로 해야할지에 대한 고민도 함께 할 수 있다.
또한 실시간 알림 SSE 를 활용하여 Notification API 로 알림을 띄우는 방법 또한 찾아보게 되었다.
웹 소켓을 활용하여 할 수 있는 것에 대해 고민할 수 있는 좋은 시간이었다.
'Next.js' 카테고리의 다른 글
[Next.JS] parallel routes interception 트러블 슈팅 (0) | 2024.07.09 |
---|---|
[Next.JS] Next.js로 블로그 만들기 (1) | 2024.03.29 |
[프로젝트] 개인 블로그 챗봇 Open API 사용하기 (0) | 2024.03.19 |
[엘리스 SW 스터디] Next.js 기초와 내장 컴포넌트 (1) | 2024.01.21 |
Next.js 기초!!! (1) | 2024.01.13 |