간단한 소켓프로그래밍/소켓통신을 통한 웹 채팅 프로그램을 구현해보자. 🎨
아래 소스코드가 길어 보이지만, CSS 및 주석으로 인해 길어 보일 뿐 실제로 사용되는 소스코드는 30줄 정도 밖에 안된다.
(실제로 코드를 읽어보면 단순하다.)
서버(backend)와 클라이언트(frontend)로 나눠진다.
서버인 백엔드부터 살펴보도록 하자.
먼저 node.js를 이용한 서버를 사용할 것이므로, 다음 명령어를 통해 백엔드 디렉터리에 패키지를 설치해 주도록 한다.
// backend 디렉터리 생성 및 이동
mkdir backend
cd backend
// 서버 패키지 설치
npm init -y
npm install socket.io
npm install express
npm install
이후 backend 폴더 내 app.js를 다음과 같이 작성해 주도록 하자.
// backend/app.js
const httpServer = require("http").createServer();
const io = require("socket.io")(httpServer, {
cors: {
origin: "http://localhost:3000",
methods: ["GET", "POST"],
},
});
// on은 받기, emit은 보내기
// 최소 실행됨
io.on("connection", (socket) => {
console.log("connection");
// [수신] 클라이언트로부터 메세지를 받음
socket.on("init", (payload) => {
console.log(payload);
});
// [수신] 클라이언트로부터 메세지를 받음
socket.on("send message", (item) => {
console.log(item.name + " : " + item.message);
// [송신] 연결된 클라이언트 전체에 메세지를 보냄
io.emit("receive message", { name: item.name, message: item.message });
});
});
httpServer.listen((80), function(){
console.log('Http server listening on port 80');
});
터미널을 열고, node app.js
를 통해 작성한 소켓 서버를 실행시켜 보자.
다음과 같이 나온다면 서버는 완료되었다.🎉
실행된 서버는 그대로 두도록하자. (클라이언트 작성하고 바로 쓸 거니까.)
이제 클라이언트 쪽으로 넘어가보자.
서버가 실행중인 터미널은 그대로 두고, 새 터미널 창을 하나 더 만든다.
클라이언트는 React를 이용해서 구성할 것이다.
단순하게 구현할 것이므로 npx CRA를 이용해서 아래와 같이 react-app를 설치한다.
// backend 폴더에서 빠져나온다.
cd ..
// 프로젝트 폴더에서 입력.
npx create-react-app frontend
cd frontend
npm install socket.io-client
이 과정을 거치면 src, public, node_moudules등 React 실행에 필요한 기본적인 폴더 및 파일들이 설치된다.
이제 소스코드인 src 폴더의 파일들의 내용을 아래와 같이 편집해 주도록 하자.
import React from 'react';
import ReactDOM from 'react-dom/client';
import './index.css';
import App from './App';
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
<App />
);
//App.js
import React, { useCallback, useEffect, useState } from "react";
import "./App.css";
// 0. 클라이언트 소캣 생성
import io from "socket.io-client";
// React 창이 새롭게 생설될 때 마다, 클라이언트 또한 새롭게 생성됨
// 1. 서버로 소켓 conncet 요청
const socket = io.connect("http://127.0.0.1:80");
// 2. 클라이언트에서 서버쪽으로 데이터를 전달 (데이터 송/수신 확인)
socket.emit("init", "[init] Client -> Server");
function App() {
// State
// chartArr = [];
const [chatArr, setChatArr] = useState([]);
// chat = { name: "", message: "" };
const [chat, setChat] = useState({ name: "", message: "" });
// useEffect : [deps] 컴포넌트가 랜더링 될 때마다 특정 작업을 수행하도록 함. (function, [deps])로 구성 됨
// [deps] 배열에 여러 컴포넌트들이 들어 있다면 해당 컴포턴트가 변경된 개수 만큼 실행
// [deps]에 빈 배열 -> 최초 한번만 실행
// 3. 소켓 종료
useEffect(() => {
return () => {
socket.close();
};
}, []);
// [수신] 서버로부터 온 메세지를 받음
useEffect(() => {
socket.on("receive message", (message) => {
// chatArr 배열에 chatArr 콜백함수를 뒤에 붙여줌. (추가해줌)
setChatArr((chatArr) => chatArr.concat(message));
}); //receive message이벤트에 대한 콜백을 등록해줌
}, []);
// [요약] 한 클라이언트가 서버로 메세지를 전송하고, 해당 메세지는 서버에서 연결된 모든 클라이언트들에게 보낸다.
// useCallback : deps가 변경될 때만 기억해둔 콜백함수를 새로 생성해서 사용
// 즉, 모든 렌더링 마다 만드는 것이 아니라, 함수내용을 기억해놓고 특정 조건(의존성 변경)에서만 함수를 재 생성해서 사용하도록 하는 것.
// [송신] 서버로 메세지를 전달.
//전송 버튼을 눌렀을 때 send message이벤트 발생
const buttonHandler = useCallback((e) => {
socket.emit("send message", { name: chat.name, message: chat.message });
}, [chat]);
// setChat으로 State를 변경
// 내용이 변경될 때 -> 현재 이벤트가 발생한 객체에서의 변경되는 부분을 가지고 옴
const changeMessage = useCallback((e) => {
setChat({ name: chat.name, message: e.target.value });
},[chat]);
// 이름이 변경될 때 -> 현재 이벤트가 발생한 객체에서의 변경되는 부분을 가지고 옴
const changeName = useCallback((e) => {
setChat({ name: e.target.value, message: chat.message });
},[chat]);
return (
<div className="App">
<div className="Box">
{/* 메세지 출력 창 */}
<div className="ChatBox">
{console.log(chatArr, "< 현재 입력된 채팅 배열 ")}
{chatArr.map((ele) => (
<div className="Chat">
<div className="ChatName">[{ele.name}]</div>
<div className="ChatLog">{ele.message}</div>
</div>
))}
</div>
{/* 입력 창 */}
<div className="InputBox">
<input className="InputName" placeholder="이름" onChange={changeName}></input>
<span></span>
<input className="InputText" placeholder="내용" onChange={changeMessage}></input>
<button onClick={buttonHandler}>등록</button>
</div>
</div>
</div>
);
}
export default App;
길어보이지만 주석이 절반이다. 정말.
.App {
width: 100vw;
height: 100vh;
display: flex;
justify-content: center;
align-items: center;
}
.Box{
width:40%;
height: 80%;
border:3px solid black;
border-radius: 1rem;
}
.ChatBox{
border:1px solid deepskyblue;
border-radius: 1rem;
height:95%;
width: 99%;
display: flex;
flex-direction: column;
overflow: auto;
background-color: deepskyblue;
}
.Chat{
align-items: center;
border:3px solid whitesmoke;
border-radius: 1rem;
background-color: skyblue;
display: grid;
grid-template-columns: minmax(10px, max-content) 1fr;
width: 350px;
margin : 3px;
}
.ChatName{
font-weight: bold;
margin-left: 10px;
}
.ChatLog{
margin-left:30px;
background-color: whitesmoke;
border-radius: 0.5rem;
}
.InputBox{
display: flex;
height:5%;
justify-content: center;
align-items: center;
}
.InputText {
width: 50%;
border-radius: 3rem;
margin : 5px
}
.InputName {
width: 30%;
border-radius: 3rem;
}
node App.js
가 아닌, npm start
를 통해서 실행 시켜주도록 하자.
cd frontend
cd src
npm start
참고로 서버처럼
node App.js
한다면 실행되지 않는다.
그 이유는 JSX 문법을 js으로 compile할 수 없기 때문.
실행이 완료되면 왼쪽과 같이 React 서버가 동작중인 주소가 출력된다.
해당 주소를 인터넷 창에 그대로 입력해서 접속해보면, 다음과 같이 채팅화면이 출력되는 것을 볼 수 있다.
자, 이제 모든 코드 작성 및 기타 등등이 완료되었다. 채팅을 해보자!
먼저 다음과 같이 서버쪽 콘솔화면을 보면 클라이언트가 연결 되었다는 것이 출력되어 있다.
이제 채팅을 쳐보면?
서버로 메세지가 전달 된 것을 확인할 수 있다!
채팅의 꽃인 서로 다른 클라이언트창 2개를 켜서 테스트 해보면 잘 되는 것을 확인할 수 있다!
소스코드에 대한 상세한 설명은 주석으로 대체한다.
주석만 보고도 충분히 잘 이해할 수 있다. (함수의 자세한 사용법 같은 건 찾아보는 게,,)
활용한 소스코드 출처
해당 프로그램은 AWS EC2서버를 통해 실제 여러 사람이 들어와 통신할 수 있도록 만들 수 있다.