NODE.JS
아마존 E-commerce 클론 -33) 채팅 기능 만들기(socket io)
dodop
2021. 5. 30. 22:39
socket io는 자바스크립 라이브러리이고 리얼타임 인터랙션을 만들어내는 라이브러리다.
최상위 폴더에 npm install socket.io
server.js에 다음과 같이 입력해준다.
import http from 'http'
import {Server} from 'socket.io'
const httpServer = http.Server(app);
const io = new Server(httpServer,{cors:{origin:'*'}})//오류방지
const users = [];
io.on('connection', (socket)=>{//연결끊기
socket.on('disconnect',()=>{
const user = users.find(x=>x.socketId===socket.id);
if(user){
user.online = false;
console.log('Offline', user.name);
const admin = users.find(x=>x.isAdmin && x.online);
if(admin){
io.go(admin.socketId).emit('updateUser', user)
}
}
});
socket.on('onLogin', (user)=>{//새로운 유저가 들어왔을때(연결하기)
const updatedUser = {
...user,
online:true,
socketId:socket.id,
messages:[]
};
const existUser = users.find(x=>x._id ===updatedUser._id);
if(existUser){
existUser.socketId = socket.id;
existUser.online = true;
}else{
users.push(updatedUser);
}
console.log('Online', user.name);
const admin = users.find(x=>x.isAdmin&& x.online);
if(admin){
io.to(admin.socketId).emit('updateUser', updatedUser)
}
if(updatedUser.isAdmin){
io.to(updatedUser.socketId).emit('listUsers', users)
}
});
socket.on('onUserSelected', (user)=>{
const admin = users.find(x=>x.isAdmin&& x.online)
if(admin){
const existUser = users.find(x=>x._id===user._id);
io.to(admin.socketId).emit('selectedUser', existUser)
}
})
socket.on('onMessage', (message)=>{
if(message.isAdmin){
const user = users.find(x=>x._id===message._id && x.online)
if(user){
io.to(user.socketId).emit('message', message)
user.messages.push(message);
}
}else{
const admin = users.find(x=>x.isAdmin&& x.online)
if(admin){
io.to(admin.socketId).emit('message', message)
const user = users.find(x=>x._id===message._id && x.online)
user.messages.push(message)
}else{
io.to(socket.id).emit('message',{
name:'Admin',
body:'Sorry.I am not online right now.'
})
}
}
})
})
httpServer.listen(port, ()=>{
onsole.log(`Serve at http://localhost:${port}`);
})
채팅창을 만들어보자.
Admin에게 만들어 줄것이다.
SupportScreen.js
import React from 'react'
import MessageBox from '../components/MessageBox'
function SupportScreen() {
return (
<div className="row top full-container">
<div className="col-1 support-users">
{users.filter((x) => x._id !== userInfo._id).length === 0 && (
<MessageBox>No Online User Found</MessageBox>
)}
<ul>
{users
.filter((x) => x._id !== userInfo._id)
.map((user) => (
<li
key={user._id}
className={user._id === selectedUser._id ? ' selected' : ' '}
>
<button
className="block"
type="button"
onClick={() => selectUser(user)}
>
{user.name}
</button>
<span
className={
user.unread ? 'unread' : user.online ? 'online' : 'offline'
}
/>
</li>
))}
</ul>
</div>
<div className="col-3 support-messages">
{!selectedUser._id ? (
<MessageBox>Select a user to start chat</MessageBox>
) : (
<div>
<div className="row">
<strong>Chat with {selectedUser.name} </strong>
</div>
<ul ref={uiMessagesRef}>
{messages.length === 0 && <li>No message.</li>}
{messages.map((msg, index) => (
<li key={index}>
<strong>{`${msg.name}: `}</strong> {msg.body}
</li>
))}
</ul>
<div>
<form onSubmit={submitHandler} className="row">
<input
value={messageBody}
onChange={(e) => setMessageBody(e.target.value)}
type="text"
placeholder="type message"
/>
<button type="submit">Send</button>
</form>
</div>
</div>
)}
</div>
</div>
);
}
export default SupportScreen
이제 스테잇을 가져와보자.
여기서 소켓이 없으면 생성하는 라이브러리를 다운받자.
frontend로 가서 npm install socket.io-client
import socketIOClient from 'socket.io-client'
let allUsers = [];
let allMessages = [];
let allSelectedUser= [];
const ENDPOINT =
window.location.host.indexOf('localhost')>=0?
'http://127.0.0.1:5000' :
window.location.host;
function SupportScreen() {
const [selectedUser, setSelectedUser] = useState({});
const [socket, setSocket] = useState(null);
const uiMessagesRef = useRef(null);
const [messageBody, setMessageBody] = useState('');
const [messages, setMessages] = useState([]);
const [users, setUsers] = useState([]);
const userSignin = useSelector((state) => state.userSignin);
const { userInfo } = userSignin;
useEffect(() => {
if (uiMessagesRef.current) {//메세지 제일 최근것으로 내려간다.
uiMessagesRef.current.scrollBy({
top: uiMessagesRef.current.clientHeight,
left: 0,
behavior: 'smooth',
});
}
if (!socket) {
const sk = socketIOClient(ENDPOINT);//소켓이 없다면 생성해준다.
setSocket(sk);
sk.emit('onLogin', {//서버의 onLogin을 발생시킨다. (server.js)
_id: userInfo._id,
name: userInfo.name,
isAdmin: userInfo.isAdmin,
});
sk.on('message', (data) => {
if (allSelectedUser._id === data._id) {//이미 메세지 보낸적이 있다면
allMessages = [...allMessages, data];//메세지 목록에 추가해준다.
} else {
const existUser = allUsers.find((user) => user._id === data._id);//사용자를 찾는다.
if (existUser) {
allUsers = allUsers.map((user) =>
user._id === existUser._id ? { ...user, unread: true } : user
);
setUsers(allUsers);
}
}
setMessages(allMessages);
});
sk.on('updateUser', (updatedUser) => {
const existUser = allUsers.find((user) => user._id === updatedUser._id);
if (existUser) {
allUsers = allUsers.map((user) =>
user._id === existUser._id ? updatedUser : user
);
setUsers(allUsers);
} else {
allUsers = [...allUsers, updatedUser];
setUsers(allUsers);
}
});
sk.on('listUsers', (updatedUsers) => {
allUsers = updatedUsers;
setUsers(allUsers);
});
sk.on('selectUser', (user) => {
allMessages = user.messages;
setMessages(allMessages);
});
}
}, [messages, socket, users]);
이제 함수들을 만들어 보자.
const selectUser = (user)=>{
allSelectedUser = user;
setSelectedUser(allSelectedUser)
const existUser = allUsers.find(x=>x._id===user._id)
if(existUser){
allUsers = allUsers.map(x=>{
x._id===existUser._id? {...x, unread:false}:x
})
setUsers(allUsers)
}
socket.emit('onUserSelected', user)
}
const submitHandler = (e) => {
e.preventDefault();
if (!messageBody.trim()) {
alert('Error. Please type message.');
} else {
allMessages = [
...allMessages,
{ body: messageBody, name: userInfo.name },
];
setMessages(allMessages);
setMessageBody('');
setTimeout(() => {
socket.emit('onMessage', {
body: messageBody,
name: userInfo.name,
isAdmin: userInfo.isAdmin,
_id: selectedUser._id,
});
}, 1000);
}
};
app.js에 페이지 추가
import SupportScreen from './screens/SupportScreen';
{userInfo && userInfo.isAdmin && (
<div className="dropdown">
<Link to="#admin">Admin {' '}<i className="fa fa-caret-down"></i></Link>
<ul className="dropdown-content">
<li>
<Link to = "/dashboard">Dashboard</Link>
</li>
<li>
<Link to = "/productlist">Products</Link>
</li>
<li>
<Link to = "/orderlist">Orders</Link>
</li>
<li>
<Link to = "/userlist">Users</Link>
</li>
<li>
<Link to = "/support">Support</Link>
</li>
</ul>
</div>
<AdminRoute path="/support" component={SupportScreen} exact></AdminRoute>
이제 홈페이지에 채팅 박스를 넣어보자.
components>ChatBox.js
import React, { useEffect, useRef, useState } from 'react';
import socketIOClient from 'socket.io-client';
const ENDPOINT =
window.location.host.indexOf('localhost') >= 0
? 'http://127.0.0.1:5000'
: window.location.host;
export default function ChatBox(props) {
const { userInfo } = props;
const [socket, setSocket] = useState(null);
const uiMessagesRef = useRef(null);
const [isOpen, setIsOpen] = useState(false);
const [messageBody, setMessageBody] = useState('');
const [messages, setMessages] = useState([
{ name: 'Admin', body: 'Hello there, Please ask your question.' },
]);
useEffect(() => {
if (uiMessagesRef.current) {
uiMessagesRef.current.scrollBy({//chatbox있으면 스크롤 다운
top: uiMessagesRef.current.clientHeight,
left: 0,
behavior: 'smooth',
});
}
if (socket) {
socket.emit('onLogin', {
_id: userInfo._id,
name: userInfo.name,
isAdmin: userInfo.isAdmin,
});
socket.on('message', (data) => {//새 메세지 보내기
setMessages([...messages, { body: data.body, name: data.name }]);
});
}
}, [messages, isOpen, socket]);
const supportHandler = () => {
setIsOpen(true);
console.log(ENDPOINT);
const sk = socketIOClient(ENDPOINT);
setSocket(sk);
};
const submitHandler = (e) => {//새메시지 보내기
e.preventDefault();
if (!messageBody.trim()) {
alert('Error. Please type message.');
} else {
setMessages([...messages, { body: messageBody, name: userInfo.name }]);
setMessageBody('');
setTimeout(() => {//1초동안
socket.emit('onMessage', {
body: messageBody,
name: userInfo.name,
isAdmin: userInfo.isAdmin,
_id: userInfo._id,
});
}, 1000);
}
};
const closeHandler = () => {
setIsOpen(false);
};
return (
<div className="chatbox">
{!isOpen ? (
<button type="button" onClick={supportHandler}>
<i className="fa fa-support" />
</button>
) : (
<div className="card card-body">
<div className="row">
<strong>Support </strong>
<button type="button" onClick={closeHandler}>
<i className="fa fa-close" />
</button>
</div>
<ul ref={uiMessagesRef}>
{messages.map((msg, index) => (
<li key={index}>
<strong>{`${msg.name}: `}</strong> {msg.body}
</li>
))}
</ul>
<div>
<form onSubmit={submitHandler} className="row">
<input
value={messageBody}
onChange={(e) => setMessageBody(e.target.value)}
type="text"
placeholder="type message"
/>
<button type="submit">Send</button>
</form>
</div>
</div>
)}
</div>
);
}
채팅박스 스타일하자.
/* Chatbox */
.chatbox {
color: #000000;
position: fixed;
right: 1rem;
bottom: 1rem;
}
.chatbox ul {
overflow: scroll;
max-height: 20rem;
}
.chatbox li {
margin-bottom: 1rem;
}
.chatbox input {
width: calc(100% - 9rem);
}
.support-users {
background: #f0f0f0;
height: 100%;
}
.support-users li {
background-color: #f8f8f8;
}
.support-users button {
background-color: transparent;
border: none;
text-align: left;
}
.support-users li {
margin: 0;
background-color: #f0f0f0;
border-bottom: 0.1rem #c0c0c0 solid;
}
.support-users li:hover {
background-color: #f0f0f0;
}
.support-users li.selected {
background-color: #c0c0c0;
}
.support-messages {
padding: 1rem;
}
.support-messages input {
width: calc(100% - 9rem);
}
.support-messages ul {
height: calc(100vh - 18rem);
max-height: calc(100vh - 18rem);
overflow: scroll;
}
.support-messages li {
margin-bottom: 1rem;
}
.support-users span {
width: 2rem;
height: 2rem;
border-radius: 50%;
position: absolute;
margin-left: -25px;
margin-top: 10px;
}
.support-users .offline {
background-color: #808080;
}
.support-users .online {
background-color: #20a020;
}
.support-users .unread {
background-color: #f02020;
}
app.js에 추가
import ChatBox from './components/ChatBox';
<footer className="row center">
{userInfo && !userInfo.isAdmin && <ChatBox userInfo={userInfo}></ChatBox>}
<div>All right reserved</div> {' '}
</footer>
일반계정에서 메세지를 보내고 다시 관리자 계정으로 오면,
여기 진짜 엉망진창 나중에 꼭 다시 봐야한다.