Post

Next.js에서 WebSocket을 사용한 채팅 구현하기

Next.js에서 WebSocket을 사용한 채팅 구현하기

1. 초기 설정

1-1. 패키지 설치

1
npm install socket.io socket.io-client

1-2. socket.ts 파일 생성

서버 측 설정을 위해 pages/api/socket.ts 파일을 생성한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
import { Server } from "socket.io";
import type { NextApiRequest } from "next";
import type { Socket as NetSocket } from "net";
import type { Server as HttpServer } from "http";

interface SocketServer extends HttpServer {
  io?: Server;
}

interface SocketWithIO extends NetSocket {
  server: SocketServer;
}

interface NextApiResponseWithSocket extends NextApiRequest {
  socket: SocketWithIO;
}

const SocketHandler = (req: NextApiResponseWithSocket, res: any) => {
  if (res.socket.server.io) {
    console.log("Socket이 이미 실행중입니다");
    res.end();
    return;
  }

  const io = new Server(res.socket.server, {
    path: "/api/socket",
    addTrailingSlash: false
  });

  res.socket.server.io = io;

  io.on("connection", (socket) => {
    console.log("클라이언트가 연결되었습니다:", socket.id);

    socket.on("send-message", (message) => {
      io.emit("receive-message", message);
    });

    socket.on("disconnect", () => {
      console.log("클라이언트가 연결을 해제했습니다:", socket.id);
    });
  });

  console.log("Socket 서버가 시작되었습니다");
  res.end();
};

export default SocketHandler;

2. 소켓 프로바이더 구현

2-1. 소켓 컨텍스트 생성

클라이언트에서 사용할 Socket 컨텍스트를 생성한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
import { createContext, useContext, useEffect, useState } from "react";
import io, { Socket } from "socket.io-client";

interface SocketContextType {
  socket: Socket | null;
  isConnected: boolean;
}

const SocketContext = createContext<SocketContextType>({
  socket: null,
  isConnected: false
});

export const SocketProvider = ({ children }: { children: React.ReactNode }) => {
  const [socket, setSocket] = useState<Socket | null>(null);
  const [isConnected, setIsConnected] = useState(false);

  useEffect(() => {
    // Socket 서버 초기화
    const socketInitializer = async () => {
      await fetch("/api/socket");

      const socketInstance = io({
        path: "/api/socket"
      });

      socketInstance.on("connect", () => {
        console.log("소켓 연결됨");
        setIsConnected(true);
      });

      socketInstance.on("disconnect", () => {
        console.log("소켓 연결 해제됨");
        setIsConnected(false);
      });

      setSocket(socketInstance);
    };

    socketInitializer();

    return () => {
      if (socket) {
        socket.disconnect();
      }
    };
  }, []);

  return (
    <SocketContext.Provider value=>
      {children}
    </SocketContext.Provider>
  );
};

export const useSocket = () => useContext(SocketContext);

2-2. layout.tsx에 SocketProvider 추가

app/layout.tsx에 SocketProvider를 추가한다.

1
2
3
4
5
6
7
8
9
10
11
import { SocketProvider } from "@/contexts/SocketContext";

function MyApp({ Component, pageProps }) {
  return (
    <SocketProvider>
      <Component {...pageProps} />
    </SocketProvider>
  );
}

export default MyApp;

3. 컴포넌트에서 소켓 적용

컴포넌트에서 소켓 사용 예시

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
import { useSocket } from "@/contexts/SocketContext";
import { useState, useEffect } from "react";

const Chat = () => {
  const { socket, isConnected } = useSocket();
  const [message, setMessage] = useState("");
  const [messages, setMessages] = useState<string[]>([]);

  useEffect(() => {
    if (!socket) return;

    // 메시지 수신 이벤트 리스너
    socket.on("receive-message", (newMessage: string) => {
      setMessages((prev) => [...prev, newMessage]);
    });

    return () => {
      socket.off("receive-message");
    };
  }, [socket]);

  const sendMessage = () => {
    if (message.trim() && socket) {
      socket.emit("send-message", message);
      setMessage("");
    }
  };

  return (
    <div>
      <div>연결 상태: {isConnected ? "연결됨" : "연결 안됨"}</div>

      <div className="messages">
        {messages.map((msg, index) => (
          <div key={index}>{msg}</div>
        ))}
      </div>

      <input
        value={message}
        onChange={(e) => setMessage(e.target.value)}
        placeholder="메시지를 입력하세요"
      />
      <button onClick={sendMessage}>전송</button>
    </div>
  );
};

export default Chat;

주요 사용 팁:

  • 소켓 이벤트는 항상 cleanup 함수를 통해 정리해주어야 한다.
  • 실시간 업데이트가 필요한 컴포넌트에서 useSocket 훅을 사용한다.
  • 소켓 연결 상태를 항상 확인하고 처리해야 한다.
  • 에러 처리와 재연결 로직을 구현하는 것이 좋다.

추가로 에러 처리를 포함한 예시:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
useEffect(() => {
  const socketInitializer = async () => {
    try {
      await fetch("/api/socket");
      const socketInstance = io({
        path: "/api/socket",
        reconnection: true,
        reconnectionAttempts: 5,
        reconnectionDelay: 1000
      });

      socketInstance.on("connect_error", (err) => {
        console.error("소켓 연결 에러:", err);
      });

      socketInstance.on("reconnect_attempt", (attempt) => {
        console.log(`재연결 시도 ${attempt}`);
      });

      setSocket(socketInstance);
    } catch (err) {
      console.error("소켓 초기화 에러:", err);
    }
  };

  socketInitializer();
}, []);

이렇게 설정하면 Next.js 앱에서 실시간 양방향 통신이 가능한 Socket.IO를 사용할 수 있다.

This post is licensed under CC BY 4.0 by the author.