-
Notifications
You must be signed in to change notification settings - Fork 4
웹소켓 적용기
웹소켓 프로토콜은 HTTP와는 다른 통신 프로토콜로 클라이언트와 서버 간의 실시간 양방향 통신을 가능하게 하는 통신 프로토콜입니다.
기존 HTTP 통신은 클라이언트가 요청을 보내는 경우에만 서버가 응답을 하는 단방향 통신이지만, 웹 소켓은 양방향 통신이 가능합니다.
연결을 맺기 위한 첫 번째 핸드셰이크를 주고받은 이후 지속적으로 연결이 유지되는 것이 특징이며, 통신 간에 매번 새롭게 연결을 맺을 필요가 없습니다.
현재 Planit 프로젝트에서는 웹소켓을 기반으로 한 socket.io와 socket.io-client 라이브러리를 사용하였습니다.
- Socket.io: 서버에서 사용하는 소켓 라이브러리
- Socket.io-client: 클라이언트에서 사용하는 소켓 라이브러리
특정 이벤트가 발생할 때 실행될 콜백 함수를 설정합니다.
socket.on(eventName, callback);
특정 이벤트에 대해 데이터를 전송합니다. 이 메서드를 사용하여 서버와 클라이언트 간 데이터를 보낼 수 있습니다.
socket.emit(eventName, data);
해당 메서드를 사용하여 통신을 특정 room에서만 가능케 할 수 있습니다. 같은 room의 소켓들은 서로 메시지를 주고 받을 수 있습니다.
socket.join(roomName);
대시보드 페이지에 진입했을 때 각 dashboard id를 기준으로 사용자를 특정 room에 진입시키게 하려면 다음과 같이 할 수 있겠네요!
// dashboard/[id]/page.tsx
useEffect(() => {
// ...
// socket 연결이 활성화되면 join-room 이벤트에 대해 dashboardId 데이터를 서버로 방출합니다.
socket.on("connect", () => {
socket.emit("join-room", dashboardId);
});
// ...
}, []);
// join-room 이벤트가 발생 시 해당 socket을 room에 입장시킵니다.
socket.on("join-room", (room: string) => {
socket.join(room);
});
Next.js 페이지 라우터의 api route를 사용해서 작은 백엔드를 만듭니다.
// pages/api/socket.js
import { Server } from "Socket.IO";
export default async function handler(req, res) {
if (res.socket.server.io) {
console.log("Socket is already running");
} else {
// res.socket.server.io가 존재하지 않으면, 소켓 서버를 초기화합니다.
console.log("Socket is initializing");
const io = new Server(res.socket.server);
res.socket.server.io = io;
}
res.end();
}
dashboard/[id] 페이지에 들어가는 경우 fetch("/api/socket")
를 호출하여 socket.io 서버와의 연결을 시작합니다.
// dashboard/[id]/page.tsx
useEffect(() => {
const initializeSocket = async () => {
await fetch("/api/socket");
};
initializeSocket();
// ...
}, [id]);
카드를 생성하는 예시입니다.
클라이언트에서 카드를 생성하는 경우 서버로 데이터를 전송해야 합니다.
// CreateCardModal.tsx
const onSubmit = () => {
// ...
socket?.emit("card", {
member: userInfo?.nickname,
room: String(dashboardId),
});
};
"card"에 대한 이벤트가 발생하면 서버에서 이에 대해 콜백 함수를 실행합니다.
// pages/api/socket.ts
socket.on("card", (data) => {
const { member, room } = data;
// 메시지를 같은 room에 있는 다른 클라이언트들에게 전송합니다.
socket.broadcast
.to(room)
.emit("card", `${member} 님이 카드를 생성하였습니다.`);
});
소켓의 멋진 점은 이벤트 리스너를 서버뿐만 아니라 클라이언트에서도 등록할 수 있다는 것입니다.
// components/dashboard/Column.tsx
useEffect(() => {
socket.on("card", (message: string) => {
// 서버에서 방출된 메시지를 toast로 보여주고, 데이터를 다시 가져와 리렌더링합니다.
toast.success(message);
fetchColumnsAndCards();
});
}, []);
dashboard/[id]/page.tsx
에서 선언한 소켓을 다른 컴포넌트에서 사용해야 하고, 이를 props로만 내려주기에는 한계가 있습니다.
따라서 zustand를 사용하여 다른 컴포넌트에서 쉽게 소켓에 접근할 수 있도록 하였습니다.
// socketStore.ts
import { Socket, io } from "socket.io-client";
import { create } from "zustand";
type SocketState = {
socket: Socket | null;
setSocket: (newSocket: Socket) => void;
initializeSocket: (dashboardId: string) => Promise<() => void>;
};
export const useSocketStore = create<SocketState>((set) => ({
socket: null,
setSocket: (newSocket) => set({ socket: newSocket }),
initializeSocket: async (dashboardId: string) => {
await fetch("/api/socket");
const newSocket = io();
newSocket.on("connect", () => {
newSocket.emit("join-room", dashboardId);
});
set({ socket: newSocket });
return () => {
newSocket.disconnect();
};
},
}));
Vercel에서 배포를 시도했지만 소켓에 대한 네트워크 요청이 에러가 나는 문제가 있었습니다.
이에 대한 대안으로 다음과 같은 방법이 있습니다.
- 기존 Node.js 기반 서버를 제공하는 호스팅 플랫폼에서 전체 Next.js 앱 호스팅하기 (Render)
- Socket.io를 직접적으로 app에서 사용하는 대신 Supabase, Firebase 등과 같은 실시간 통신 제공업체 사용하기
- Node.js, express 기반 서버의 socket.io 코드를 Next.js에서 분리하고 클라이언트, 서버 따로따로 호스팅하기
첫 번째 방법을 선택하여 Render에서 배포했을 때 잘 적용이 되는 것을 확인했습니다.
- socket.io 메서드: https://stackoverflow.com/questions/32674391/io-emit-vs-socket-emit
- socket.io Next.js 적용: https://mxx-kor.github.io/blog/websocket-with-nextjs
- Socket.io + ReactJS 튜토리얼: https://www.youtube.com/watch?v=djMy4QsPWiI&t=75s
- Vercel 배포 에러: https://stackoverflow.com/questions/76753565/next-js-with-socketio-connection-is-showing-308-and-404-status-when-deployed-th