-
[Next.js] ws 라이브러리를 이용한 WebSocket 구현Next.js 2024. 10. 11. 14:07
버전 정보
Next.js : 14.2.5
ws : 8.18.0구조
App Router
이번에는 채팅 서비스를 구현 해보려고 한다.
이전에 블로그에 기술했던 socket.io가 아닌 ws로 websocket 통신을 구현해보았다.먼저 필요한 라이브러리들을 설치해준다.
npm install ws bufferutil utf-8-validate
※ 여기서 bufferutil을 설치하지 않으면 서버쪽에서 아래와 같은 오류가 발생한다.
TypeError: bufferUtil.unmask is not a function
아래는 서버 및 클라이언트 측 코드이다.
서버 코드
// app/api/ws/route.ts import { NextResponse } from 'next/server' import { WebSocketServer, WebSocket } from 'ws' interface Client { id: string; ws: WebSocket; } let wss: WebSocketServer | null = null let clients: Client[] = [] export async function GET() { if (!wss) { console.log('WebSocket server is initializing') // 포트번호는 원하는 포트로 변경하여 사용 wss = new WebSocketServer({ port: 3001 }) wss.on('connection', (ws) => { const clientId = Math.random().toString(36).substring(7) // 고유한 사용자의 ID 값을 임의로 생성, DB를 연동시킨다면 해당 부분을 수정 clients.push({ id: clientId, ws }) console.log(`New client connected: ${clientId}`) ws.send(JSON.stringify({ type: 'id', id: clientId })) ws.on('message', (message: string) => { const data = JSON.parse(message); // 개인에게 메시지 전송 if (data.type === 'private') { const recipient = clients.find(client => client.id === data.to) if (recipient) { recipient.ws.send(JSON.stringify({ type: 'private', from: data.from, message: data.message })) } // 전역으로 메시지 전송 } else if (data.type === 'broadcast') { clients.forEach(client => { if (client.id !== data.from) { client.ws.send(JSON.stringify({ type: 'broadcast', from: data.from, message: data.message })) } }) } }) ws.on('close', () => { clients = clients.filter(client => client.ws !== ws) console.log(`Client disconnected: ${clientId}`) }) }) } return NextResponse.json({ message: 'WebSocket server is running' }) } export const config = { api: { bodyParser: false, }, }
클라이언트 코드
// app/chat/page.tsx 'use client' import { useState, useEffect, useRef } from 'react' interface ChatMessage { text: string; isSent: boolean; from: string; to?: string; } export default function ChatPage() { const [message, setMessage] = useState('') const [messages, setMessages] = useState<ChatMessage[]>([]) const [userId, setUserId] = useState<string | null>(null) const [recipientId, setRecipientId] = useState('') const socketRef = useRef<WebSocket | null>(null) useEffect(() => { const connectWebSocket = async () => { await fetch('/api/ws') // WebSocket 서버 초기화 const ws = new WebSocket('ws://localhost:3001') ws.onopen = () => { console.log('WebSocket connection established') } ws.onmessage = (event) => { const data = JSON.parse(event.data) if (data.type === 'id') { setUserId(data.id) } else if (data.type === 'private' || data.type === 'broadcast') { setMessages((prevMessages) => [...prevMessages, { text: data.message, isSent: false, from: data.from }]) } } ws.onerror = (error) => { console.error('WebSocket error:', error) } ws.onclose = () => { console.log('WebSocket connection closed') } socketRef.current = ws } connectWebSocket() return () => { if (socketRef.current) { socketRef.current.close() } } }, []) const sendMessage = (e: React.FormEvent) => { e.preventDefault() if (message && socketRef.current && userId) { const messageData = recipientId ? { type: 'private', from: userId, to: recipientId, message } : { type: 'broadcast', from: userId, message } socketRef.current.send(JSON.stringify(messageData)) setMessages((prevMessages) => [...prevMessages, { text: message, isSent: true, from: userId, to: recipientId || 'all' }]) setMessage('') } } return ( <div className="flex flex-col h-screen bg-gray-100"> <div className="bg-blue-500 text-white p-4 flex justify-between items-center"> <h1 className="text-xl font-bold">Chat App</h1> {userId && ( <div className="bg-blue-600 px-3 py-1 rounded"> Your ID: <span className="font-bold">{userId}</span> </div> )} </div> <div className="flex-1 overflow-y-auto p-4"> {messages.map((msg, index) => ( <div key={index} className={`mb-2 p-2 rounded-lg ${ msg.isSent ? 'bg-blue-500 text-white self-end' : 'bg-white text-gray-800 self-start' }`} > <div className="font-bold">{msg.isSent ? 'You' : msg.from}</div> <div>{msg.text}</div> {msg.to && <div className="text-xs">{msg.isSent ? `To: ${msg.to}` : ''}</div>} </div> ))} </div> <form onSubmit={sendMessage} className="p-4 bg-white border-t"> <div className="flex mb-2"> <input type="text" value={recipientId} onChange={(e) => setRecipientId(e.target.value)} className="flex-1 border rounded-l px-4 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500" placeholder="Recipient ID (leave empty for broadcast)" /> </div> <div className="flex"> <input type="text" value={message} onChange={(e) => setMessage(e.target.value)} className="flex-1 border rounded-l px-4 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500" placeholder="Type a message..." /> <button type="submit" className="bg-blue-500 text-white px-4 py-2 rounded-r hover:bg-blue-600 focus:outline-none focus:ring-2 focus:ring-blue-500" > Send </button> </div> </form> </div> ) }
실행 화면 (기기 1)
실행 화면 (기기 2)
'Next.js' 카테고리의 다른 글
[Next.js] 개발 환경에 의한 삽질기... (0) 2023.07.20 [Next.js] node.js와 socket.io를 이용한 websocket (0) 2023.07.12 [Next.js] 내가 겪은 next-auth Error들 (0) 2023.02.10 [Next.js] Sentry 에러 net::ERR_BLOCKED_BY_CLIENT (0) 2023.02.05