import { useEffect, useState, useRef } from "react";
const WEBSOCKET_URL = "ws://localhost:8080"; // WebSocket 서버 주소
function App() {
const [messages, setMessages] = useState([]); // 채팅 메시지 상태
const [input, setInput] = useState(""); // 입력 필드 상태
const ws = useRef(null); // WebSocket 인스턴스
// useRef는 WebSocket 연결 객체와 같이 컴포넌트 라이프사이클 동안 유지되어야 하지만 렌더링에 영향을 주지 않는 값을 관리할 때 사용
// 컴포넌트가 렌더링될 때마다 새 연결을 만들면 중복 연결이 발생합니다.
// useRef를 사용하면 연결 객체가 초기화되지 않고 유지되므로
// 한 번 연결된 WebSocket을 계속 사용할 수 있습니다.
// null인 이유는 초기에는 객체가 존재하지 않기 때문
const isComposing = useRef(false); // 한글 조합 상태를 추적하기 위한 ref
const messagesEndRef = useRef(null); // 스크롤 고정을 위한 ref
// WebSocket 연결
useEffect(() => {
ws.current = new WebSocket(WEBSOCKET_URL);
ws.current.onopen = () => {
console.log("✅ WebSocket 연결 성공");
};
ws.current.onmessage = (event) => {
const newMessage = event.data;
setMessages((prev) => [...prev, newMessage]);
};
ws.current.onclose = () => {
console.log("❌ WebSocket 연결 종료");
};
return () => {
ws.current.close(); // 컴포넌트 언마운트 시 연결 종료
};
}, []);
// 메시지 전송
const sendMessage = () => {
if (input.trim() !== "") {
console.log("입력 메시지:", input); // 입력값 확인
ws.current.send(input); // WebSocket으로 메세지 전송
// setMessages((prev) => [...prev, input]); // 로컬 메시지 배열에 추가
console.log("업데이트된 메시지 배열:", [...messages, input]); // 상태 업데이트 확인
setInput(""); // 입력 필드 초기화
}
};
const handleKeyDown = (e) => {
// 한글 조합 중이 아니고, Enter 키가 눌렸을 때만 메시지 전송
if (!isComposing.current && e.key === "Enter" && input.trim() !== "") {
e.preventDefault();
sendMessage();
}
};
// 📌 메시지가 추가될 때마다 스크롤을 최하단으로 이동
useEffect(() => {
messagesEndRef.current?.scrollIntoView({ behavior: "smooth" });
}, [messages]);
return (
<div className="flex flex-col items-center justify-center min-h-screen bg-gray-100 p-4">
<h1 className="text-2xl font-bold mb-4">💬 실시간 채팅</h1>
<div className="w-full max-w-md bg-white rounded-2xl shadow-md p-4 mb-4 overflow-y-auto h-96 border-blue-500 border-2">
{messages.length > 0 ? (
messages.map((msg, index) => (
<div key={index} className="mb-2">
<span className="block p-2 bg-blue-200 rounded-xl">{msg}</span>
</div>
))
) : (
<p className="text-center text-gray-500">메시지가 없습니다.</p>
)}
<div ref={messagesEndRef} />
</div>
<div className="flex w-full max-w-md">
<input
type="text"
value={input}
onChange={(e) => setInput(e.target.value)}
onKeyDown={handleKeyDown}
onCompositionStart={() => {
isComposing.current = true;
}}
onCompositionEnd={() => {
isComposing.current = false;
}}
className="flex-grow p-2 rounded-l-2xl border-t border-l border-b focus:outline-none"
placeholder="메시지를 입력하세요..."
/>
<button
onClick={sendMessage}
className="bg-blue-500 text-white p-2 rounded-r-2xl hover:bg-blue-600"
>
전송
</button>
</div>
</div>
);
}
export default App;
import { WebSocketServer } from "ws";
import http from "http";
const PORT = 8080;
const clients = new Set();
// HTTP 서버 생성 (WebSocket 서버와 같이 사용)
const server = http.createServer((req, res) => {
res.writeHead(200, { "Content-Type": "text/plain" });
res.end("WebSocket server is running...");
});
// WebSocket 서버 생성
const wss = new WebSocketServer({ server });
// 연결 이벤트
wss.on("connection", (ws) => {
console.log("✅ 클라이언트 연결됨");
clients.add(ws);
// 메시지 수신 이벤트
ws.on("message", (data) => {
const message = data.toString();
console.log(`📨 받은 메시지: ${message}`);
// 모든 클라이언트에게 메시지 브로드캐스트
wss.clients.forEach((client) => {
if (client.readyState === WebSocket.OPEN) {
client.send(message); // ✅ 메시지 전송
}
});
});
// 연결 종료 이벤트
ws.on("close", () => {
console.log("❌ 클라이언트 연결 종료");
clients.delete(ws);
});
});
// 서버 실행
server.listen(PORT, () => {
console.log(`🚀 WebSocket 서버가 ${PORT} 포트에서 실행 중...`);
});
onKeyDown={(e) => {
if (e.key === "Enter" && input.trim() !== "") {
e.preventDefault();
sendMessage();
}
}}
를 사용하면 ㅇㅇㅇ를 입력하면 [ㅇㅇㅇ, ㅇ] 이렇게 ㅇ이 추가적으로 입력이 되는데
onKeyDown={(e) => {
if (e.keyCode === 13 && input.trim() !== "") {
e.preventDefault();
sendMessage();
}
}}
keyCode === 13을 주면 해당 문제가 해결이 되지만 keyCode가 오래된 표준이기 때문에 브라우저마다 다르게 동작할 수 있고 더이상 지원하지 않는다는 deprecated 경고가 발생하여 사용을 권장하지 않는다고 경고해줍니다.
더 이상 사용하지 않는 메소드 (deprecated)
- keyCode : 특정 키에 대한 숫자 코드를 반환합니다.
- onKeyPress : 문자 입력에 대한 이벤트로, 주로 알파벳, 숫자 등의 키에 반응합니다.
Keyboard Events인 onKeyDown 이벤트 사용시 발생하는 오류는 한글은 자음과 모음이 합쳐져 만들어지는 조합 문자이기 때문에 입력 시 글자가 조합 중인지 조합이 완료된 상태인지 파악하기 어려워서 이벤트가 중복 호출이 발생합니다. 그래서 이를 해결하기 위해서 KeyboardEvent에는 isComposing 변수를 통해 한글 입력 조합 상태를 추적하여 입력 필드에서 문자가 입력(조합) 되고 있는지를 추적을 해줘서 중복 입력 문제를 해결했습니다.
한글 입력 시 조합 문자 입력 문제를 해결하라고 하길래 ㄱ + ㅏ = 가 이런 조합 문자를 생각하여 단순 문자 입력인 ㅇㅇㅇ 에 대해서는 대응을 안 하는 줄 알았지만 ㅇㅇㅇ을 입력을 해도 대응이 됩니다.이유는 IME(Input Method Editor)가 동작하기 때문에 한글 자판에서 ㅇㅇㅇ 같은 단순 문자에도 조합 과정으로 취급하기 때문입니다.
해당 오류를 해결하는 과정에서 그냥 오래된 이벤트를 사용해서 발생하는 오류인 줄 알았지만 사실 근본적인 원인은 input 자체에 한글이 제대로 적용이 안되서 나타나는 오류였습니다.
그래서 한글을 입력하고 Enter키를 입력하면 중복 입력이 발생한거였습니다.