Skip to content

웹소켓 적용기

alexgoni edited this page Jul 5, 2024 · 1 revision

웹소켓 적용기

1. 웹소켓 개념

웹소켓 프로토콜은 HTTP와는 다른 통신 프로토콜로 클라이언트와 서버 간의 실시간 양방향 통신을 가능하게 하는 통신 프로토콜입니다.

기존 HTTP 통신은 클라이언트가 요청을 보내는 경우에만 서버가 응답을 하는 단방향 통신이지만, 웹 소켓은 양방향 통신이 가능합니다.

image-8

연결을 맺기 위한 첫 번째 핸드셰이크를 주고받은 이후 지속적으로 연결이 유지되는 것이 특징이며, 통신 간에 매번 새롭게 연결을 맺을 필요가 없습니다.

2. Socket.io

현재 Planit 프로젝트에서는 웹소켓을 기반으로 한 socket.io와 socket.io-client 라이브러리를 사용하였습니다.

  • Socket.io: 서버에서 사용하는 소켓 라이브러리
  • Socket.io-client: 클라이언트에서 사용하는 소켓 라이브러리

사용 방법

socket.on

특정 이벤트가 발생할 때 실행될 콜백 함수를 설정합니다.

socket.on(eventName, callback);

socket.emit

특정 이벤트에 대해 데이터를 전송합니다. 이 메서드를 사용하여 서버와 클라이언트 간 데이터를 보낼 수 있습니다.

socket.emit(eventName, data);

socket.join

해당 메서드를 사용하여 통신을 특정 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);
});

3. Next.js에서 적용

기본 아이디어

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]);

실제 적용

카드를 생성하는 예시입니다.

Sender

클라이언트에서 카드를 생성하는 경우 서버로 데이터를 전송해야 합니다.

// CreateCardModal.tsx

const onSubmit = () => {
  // ...

  socket?.emit("card", {
    member: userInfo?.nickname,
    room: String(dashboardId),
  });
};

Listener

"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();
  });
}, []);

4. Socket 전역 관리

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();
    };
  },
}));

5. 배포

Vercel에서 배포를 시도했지만 소켓에 대한 네트워크 요청이 에러가 나는 문제가 있었습니다.

이에 대한 대안으로 다음과 같은 방법이 있습니다.

  1. 기존 Node.js 기반 서버를 제공하는 호스팅 플랫폼에서 전체 Next.js 앱 호스팅하기 (Render)
  2. Socket.io를 직접적으로 app에서 사용하는 대신 Supabase, Firebase 등과 같은 실시간 통신 제공업체 사용하기
  3. Node.js, express 기반 서버의 socket.io 코드를 Next.js에서 분리하고 클라이언트, 서버 따로따로 호스팅하기

첫 번째 방법을 선택하여 Render에서 배포했을 때 잘 적용이 되는 것을 확인했습니다.


참고 자료