Socket.IO를 사용하여 간단한 React 채팅 애플리케이션을 만드는 방법
게시 됨: 2022-02-10Slack과 같은 애플리케이션이 어떻게 작동하는지 궁금하신가요? 아니면 그런 앱을 만드는 것이 얼마나 어려울까요?
이 기사에서는 ReactJS와 SocketIO를 사용하여 Slack과 같은 간단한 반응 채팅 애플리케이션을 만드는 방법에 대한 단계별 가이드를 보여줍니다. 우리는 Slack이 제공해야 하는 모든 기능의 다소 단순화된 버전을 구축할 것이므로 이 튜토리얼을 좋은 시작 예제로 사용하십시오.
핵심 개발 작업에 들어가기 전에 준비해야 할 몇 가지 필수 사항이 있습니다.
3가지 주요 전제 조건:
- 기본 JavaScript 지식이 있어야 합니다.
- 기기에 NodeJS와 NPM이 설치되어 있어야 합니다.
- IDE 또는 선호하는 텍스트 편집기가 있습니다.
설정이 완료되면 다음과 같은 3가지 매우 간단한 기능이 있는 응용 프로그램을 만드는 단계를 거칩니다.
- 닉네임 제공을 통해 로그인합니다.
- 정적으로 제공되는 채널 간에 전환합니다.
- 채널(이모티콘 포함)에 메시지를 보냅니다.
완료되면 다음과 같은 애플리케이션이 있어야 합니다.
모든 준비가 되었나요? 네!? 그럼 가자, 그럼...
1. ReactJS 애플리케이션 초기화
먼저 ReactJS 애플리케이션을 생성하고 초기화해야 합니다. 이를 위해 우리는 create-react-app.
터미널을 열고 다음을 실행합니다.
npx create-react-app simple-react-js-chat-application
이렇게 하면 기본 ReactJS 골격이 있는 simple-react-js-chat-application 디렉터리가 새로 생성됩니다. 우리는 현재 기본 프로젝트의 구조를 살펴보지 않을 것입니다.
2. 종속성 설치
다음 단계는 프런트 엔드 클라이언트에 필요한 종속성을 설치하는 것입니다. 터미널에서:
- 프로젝트 디렉토리로 이동:
cd simple-react-js-chat-application
- 달리다:
npm install axios emoji-mart node-sass skeleton-css socket.io-client uuid
이것은 전제 조건 종속성을 설치합니다:
- axios – 채널과 메시지를 가져오기 위해 백엔드를 호출하는 데 사용하고 있습니다.
- emoji-mart – 이모티콘을 위한 React 컴포넌트입니다.
- skeleton-css – 간단한 반응형 CSS 상용구입니다.
- socket.io-client – 소켓에 연결하기 위한 NPM 패키지.
- uuid - 고유한 사용자 ID 라이브러리
- node-sass – SCSS를 사용할 것입니다.
3. 백엔드 서버 생성
Socket.IO를 사용하려면 이벤트와 일부 API 끝점(예: 채널 및 메시지 검색)을 처리할 서버를 만들어야 합니다. 이 경우 NodeJS에서 처리되는 가능한 간단한 서버를 사용합니다.
src 폴더에 새 디렉토리 서버 를 만드는 것으로 시작하십시오. 그런 다음 다음 파일 만들기를 시작합니다.
Package.json 파일
package.json 파일은 npm 처리, 종속성 및 dev 종속성을 지정합니다. JavaScript 객체가 아닌 실제 JSON 파일입니다.
Node.JS 자체에 필요한 주요 필드는 이름과 버전입니다. 이름에는 프로젝트의 이름과 버전(패키지 버전)이 포함됩니다.
{ "name": "server", "version": "1.0.0", "description": "", "main": "index.js", "scripts": { "test": "echo \"Error: no test specified\" && exit 1" }, "author": "", "license": "ISC", "dependencies": { "cors": "^2.8.5", "express": "^4.17.1", "socket.io": "^3.0.4", "uuid": "^8.3.2" } }
Server.js 파일
이 파일은 백엔드 서버가 서버 인스턴스화, 사용자 정의 경로 및 이벤트/방출 수신기를 처리하는 논리를 따릅니다.
const http = require("http"); const express = require("express"); const cors = require("cors"); const socketIO = require("socket.io"); const { addMessage, getChannelMessages } = require("./messages"); const { channels, addUserToChannel } = require("./channels"); const { addUser, removeUser } = require("./users"); const app = express(); app.use(cors()); const server = http.createServer(app); const io = socketIO(server, { cors: { origin: "*", }, }); const PORT = process.env.PORT || 8080; io.on("connection", (socket) => { // Get nickname and channel. const { nickname, channel } = socket.handshake.query; console.log(`${nickname} connected`); // Join the user to the channel. socket.join(channel); addUser(nickname, socket.id); addUserToChannel(channel, nickname); // Handle disconnect socket.on("disconnect", () => { console.log(`${nickname} disconnected`); removeUser(nickname); }); socket.on("CHANNEL_SWITCH", (data) => { const { prevChannel, channel } = data; if (prevChannel) { socket.leave(prevChannel); } if (channel) { socket.join(channel); } }); socket.on("MESSAGE_SEND", (data) => { addMessage(data); const { channel } = data; socket.broadcast.to(channel).emit("NEW_MESSAGE", data); }); }); app.get("/channels/:channel/messages", (req, res) => { const allMessages = getChannelMessages(req.params.channel); return res.json({ allMessages }); }); app.get("/getChannels", (req, res) => { return res.json({ channels }); }); server.listen(PORT, () => console.log(`Server listening to port ${PORT}`));
Users.js 파일
이 구성 요소는 우리가 만들고 있는 응용 프로그램의 "사용자"를 담당합니다. 이 경우 사용자를 추가/삭제하기만 하면 됩니다.
const users = {}; const addUser = (nickname, socketId) => { users[nickname] = socketId; } const removeUser = (nickname) => { if(users.hasOwnProperty(nickname)) { delete users[nickname]; } } module.exports = { users, addUser, removeUser };
Messages.js 파일
이 파일은 메시지에 대한 기능을 추가합니다. 즉, 어레이에 메시지를 추가하고 특정 채널 메시지를 가져옵니다.
const messages = []; const addMessage = (data) => { messages.push(data); return data; }; const getChannelMessages = (channel) => messages.filter((message) => message.channel === channel); module.exports = { addMessage, getChannelMessages };
Channels.js 파일
이 파일은 채널에 대한 로직을 담고 있습니다 – 기본 초기화와 채널에 사용자를 추가하는 기능
const channels = [ { id: 1, name: "general", users: [], }, { id: 2, name: "test 1", users: [], }, { id: 3, name: "test 2", users: [], }, ]; const addUserToChannel = (channel, nickname) => { channels.filter((c) => c.name === channel).map((c) => { c.users.push(nickname); return c; }); } module.exports = { channels, addUserToChannel };
터미널을 실행하고 디렉터리에 NPM 종속성을 설치합니다.
npm run install
NPM이 완료되고 서버가 준비될 때까지 기다리십시오. 당신은 실행하여 그것을 시도 할 수 있습니다
npm start
4. 프런트 엔드 파트 만들기
중요하지는 않지만 순서대로 마지막으로 달성해야 할 것은 애플리케이션의 프론트 엔드 부분을 만드는 것입니다. 프론트 엔드는 닉네임 입력, 채널 간 전환 및 메시지 전송과 같은 핵심 기능을 제공하기 위해 백엔드 서버와 통신합니다.
애플리케이션의 초기 화면부터 시작하겠습니다.
src 폴더로 이동하여 App.js 를 엽니다. 그런 다음 내용을 아래 내용으로 바꿉니다.
import "./index.css"; import "./App.css"; import { useState } from "react"; import Chat from "./components/Chat/Chat"; import LoginDialog from "./components/LoginDialog"; function App() { const [nickname, setNickname] = useState(""); const [loggedIn, setLoggedIn] = useState(false); const handleNicknameChange = (event) => { setNickname(event.target.value.trim()); }; const handleNicknameSubmit = (e) => { if (!nickname.length) return; e.preventDefault(); setLoggedIn(true); }; return ( <div className="main-div"> {!loggedIn ? ( <LoginDialog nicknameChange={handleNicknameChange} nicknameSubmit={handleNicknameSubmit} /> ) : ( <Chat nickname={nickname} /> )} </div> ); } export default App;
앱 구성 요소에는 닉네임의 상태와 사용자가 "로그인"했는지 여부에 대한 논리가 포함되어 있습니다. 또한 상태에 따라 LoginDialog 또는 Chat이 되는 적절한 구성 요소를 렌더링합니다.
일부 모양 추가
App.css를 열고 콘텐츠를 다음으로 바꿉니다.
.login-container { position: fixed; left: 10%; right: 10%; top: 50%; transform: translate(0, -50%); display: flex; flex-direction: column; } .text-input-field { padding: 24px 12px; border-radius: 7px; font-size: 24px; } .text-input-field:focus { outline: none; } .login-button { margin-top: 20px; padding: 24px 12px; font-size: 28px; background-color: rgb(0, 132, 255); color: white; font-weight: 600; text-align: center; text-decoration: none; border-radius: 7px; }
새 폴더 도우미 를 만들고 이름 이 socket.js 인 파일을 안에 넣습니다.
import io from "socket.io-client"; import axios from "axios"; let socket; const SOCKET_URL = "http://localhost:8080"; export const initiateSocket = (channel, nickname) => { socket = io(SOCKET_URL, { query: { channel, nickname }, }); console.log("Connecting to socket"); if (socket && channel) { socket.emit("CHANNEL_JOIN", channel); } }; export const switchChannel = (prevChannel, channel) => { if (socket) { socket.emit("CHANNEL_SWITCH", { prevChannel, channel }); } }; export const subscribeToMessages = (callback) => { if (!socket) { return; } socket.on("NEW_MESSAGE", (data) => { callback(null, data); }); }; export const sendMessage = (data) => { if (!socket) { return; } socket.emit("MESSAGE_SEND", data); }; export const fetchChannels = async () => { const response = await axios.get(`${SOCKET_URL}/getChannels`); return response.data.channels; }; export const fetchChannelMessages = async (channel) => { const response = await axios.get( `${SOCKET_URL}/channels/${channel}/messages` ); return response.data.allMessages; };
이 구성 요소는 백엔드 서버와 통신하기 위해 나중에 React 구성 요소에서 사용할 필수 도우미 기능을 내보냅니다.
거의 준비가 되었습니다!
이제 닉네임 제공 및 채팅 레이아웃을 위한 대화 상자를 계속 생성합니다.
로그인 설정
새 폴더 구성 요소 를 만들고 응용 프로그램에 필요한 구성 요소를 계속 만들어 보겠습니다.
로그인다이얼로그.js:
function LoginDialog({ nicknameChange, nicknameSubmit }) { return ( <div className="dialog-container"> <div className="dialog"> <form className="dialog-form" onSubmit={nicknameSubmit}> <label className="username-label" htmlFor="username"> Nickname: </label> <input className="username-input" autoFocus onChange={nicknameChange} type="text" name="userId" placeholder="Enter your nickname to continue" /> <button type="submit" className="submit-btn"> Continue </button> </form> </div> </div> ); } export default LoginDialog;
애플리케이션이 처음 로드될 때 열리는 로그인 화면 구성 요소입니다. 이 시점에서 닉네임은 제공되지 않습니다. 여기에는 마크업만 포함되며 상태와 관련된 것은 없습니다. 코드에서 알 수 있듯이 상태 및 핸들러는 소품을 통해 전달됩니다.
채팅에 생명을 불어넣다
계속해서 폴더를 하나 더 만듭니다. Chat 에서 여러 구성 요소를 만듭니다.
채팅.js
import { useEffect, useRef, useState } from "react"; import "skeleton-css/css/normalize.css"; import "skeleton-css/css/skeleton.css"; import "./Chat.scss"; import { initiateSocket, switchChannel, fetchChannels, fetchChannelMessages, sendMessage, subscribeToMessages, } from "../../helpers/socket"; import { v4 as uuidv4 } from "uuid"; import "emoji-mart/css/emoji-mart.css"; import Channels from "./Channels"; import ChatScreen from "./ChatScreen"; function Chat({ nickname }) { const [message, setMessage] = useState(""); const [channel, setChannel] = useState("general"); const [channels, setChannels] = useState([]); const [messages, setMessages] = useState([]); const [messagesLoading, setMessagesLoading] = useState(true); const [channelsLoading, setChannelsLoading] = useState(true); const [showEmojiPicker, setShowEmojiPicker] = useState(false); const prevChannelRef = useRef(); useEffect(() => { prevChannelRef.current = channel; }); const prevChannel = prevChannelRef.current; useEffect(() => { if (prevChannel && channel) { switchChannel(prevChannel, channel); setChannel(channel); } else if (channel) { initiateSocket(channel, nickname); } }, [channel]); useEffect(() => { setMessages([]); setMessagesLoading(true); fetchChannelMessages(channel).then((res) => { setMessages(res); setMessagesLoading(false); }); }, [channel]); useEffect(() => { fetchChannels().then((res) => { setChannels(res); setChannelsLoading(false); }); subscribeToMessages((err, data) => { setMessages((messages) => [...messages, data]); }); }, []); const handleMessageChange = (event) => { setMessage(event.target.value); }; const handleMessageSend = (e) => { if (!message) return; e.preventDefault(); const data = { id: uuidv4(), channel, user: nickname, body: message, time: Date.now(), }; setMessages((messages) => [...messages, data]); sendMessage(data); setMessage(""); }; const handleEmojiSelect = (emoji) => { const newText = `${message}${emoji.native}`; setMessage(newText); setShowEmojiPicker(false); }; return ( <div className="chat-container"> <Channels nickname={nickname} channelsLoading={channelsLoading} channels={channels} }channel={channel} setChannel={setChannel} /> <ChatScreen channel={channel} messagesLoading={messagesLoading} messages={messages} showEmojiPicker={showEmojiPicker} handleEmojiSelect={handleEmojiSelect} handleMessageSend={handleMessageSend} setShowEmojiPicker={setShowEmojiPicker} message={message} handleMessageChange={handleMessageChange} /> </div> ); export default Chat;
이것은 사용자가 "로그인"할 때 채팅 응용 프로그램에 대한 논리를 유지하는 주요 구성 요소입니다. 채널, 메시지 및 전송 메시지에 대한 핸들러 및 상태를 유지합니다.
스타일 업
구성 요소와 하위 구성 요소에 대한 스타일 지정을 계속해 보겠습니다.
채팅.scss
.chat-container { width: 100vw; height: 100vh; display: grid; grid-template-columns: 1fr 4fr; } .right-sidebar { border-left: 1px solid #ccc; } .left-sidebar { border-right: 1px solid #ccc; } .user-profile { height: 70px; display: flex; align-items: flex-start; padding-right: 20px; padding-left: 20px; justify-content: center; flex-direction: column; border-bottom: 1px solid #ccc; } .user-profile span { display: block; } .user-profile .username { font-size: 20px; font-weight: 700; } .chat-channels li, .room-member { display: flex; align-items: center; padding: 15px 20px; font-size: 18px; color: #181919; cursor: pointer; border-bottom: 1px solid #eee; margin-bottom: 0; } .room-member { justify-content: space-between; padding: 0 20px; height: 60px; } .send-dm { opacity: 0; pointer-events: none; font-size: 20px; border: 1px solid #eee; border-radius: 5px; margin-bottom: 0; padding: 0 10px; line-height: 1.4; height: auto; } .room-member:hover .send-dm { opacity: 1; pointer-events: all; } .presence { display: inline-block; width: 10px; height: 10px; background-color: #ccc; margin-right: 10px; border-radius: 50%; } .presence.online { background-color: green; } .chat-channels .active { background-color: #eee; color: #181919; } .chat-channels li:hover { background-color: #d8d1d1; } .room-icon { display: inline-block; margin-right: 10px; } .chat-screen { display: flex; flex-direction: column; height: 100vh; } .chat-header { height: 70px; flex-shrink: 0; border-bottom: 1px solid #ccc; padding-left: 10px; padding-right: 20px; display: flex; flex-direction: column; justify-content: center; } .chat-header h3 { margin-bottom: 0; text-align: center; } .chat-messages { flex-grow: 1; overflow-y: auto; display: flex; flex-direction: column; justify-content: flex-end; margin-bottom: 0; min-height: min-content; position: relative; } .message { padding-left: 20px; padding-right: 20px; margin-bottom: 10px; display: flex; justify-content: space-between; align-items: center; } .message span { display: block; text-align: left; } .message .user-id { font-weight: bold; } .message-form { border-top: 1px solid #ccc; width: 100%; display: flex; align-items: center; } .message-form, .message-input { width: 100%; margin-bottom: 0; } .rta { flex-grow: 1; } .emoji-mart { position: absolute; bottom: 20px; right: 10px; } input[type="text"].message-input, textarea.message-input { height: 50px; flex-grow: 1; line-height: 35px; padding-left: 20px; border-radius: 0; border-top-left-radius: 0; border-top-right-radius: 0; border-bottom-left-radius: 0; border-bottom-right-radius: 0; border: none; font-size: 16px; color: #333; min-height: auto; overflow-y: hidden; resize: none; border-left: 1px solid #ccc; } .message-input:focus { outline: none; } .toggle-emoji { border: none; width: 50px; height: auto; padding: 0; margin-bottom: 0; display: flex; align-items: center; justify-content: center; } .toggle-emoji svg { width: 28px; height: 28px; } /* RTA ========================================================================== */ .rta { position: relative; border-left: 1px solid #ccc; display: flex; flex-direction: column; } .rta__autocomplete { position: absolute; width: 300px; background-color: white; border: 1px solid #ccc; border-radius: 5px; } .rta__autocomplete ul { list-style: none; text-align: left; margin-bottom: 0; } .rta__autocomplete li { margin-bottom: 5px; padding: 3px 20px; cursor: pointer; } .rta__autocomplete li:hover { background-color: skyblue; } /* Dialog ========================================================================== */ .dialog-container { position: absolute; top: 0; right: 0; bottom: 0; left: 0; background-color: rgba(0, 0, 0, 0.8); display: flex; justify-content: center; align-items: center; } .dialog { width: 500px; background-color: white; display: flex; align-items: center; } .dialog-form { width: 100%; margin-bottom: 0; padding: 20px; } .dialog-form > * { display: block; } .username-label { text-align: left; font-size: 16px; } .username-input { width: 100%; } input[type="text"]:focus { border-color: #5c8436; } .submit-btn { color: #5c8436; background-color: #181919; width: 100%; } .submit-btn:hover { color: #5c8436; background-color: #222; }
ChatMessages.js
import ChatMessages from "./ChatMessages"; import MessageForm from "./MessageForm"; function ChatScreen({ channel, messagesLoading, messages, showEmojiPicker, handleEmojiSelect, handleMessageSend, setShowEmojiPicker, message, handleMessageChange, }) { return ( <section className="chat-screen"> <header className="chat-header"> <h3>#{channel}</h3> </header> <ChatMessages messagesLoading={messagesLoading} messages={messages} /> <footer className="chat-footer"> <MessageForm emojiSelect={handleEmojiSelect} handleMessageSend={handleMessageSend} setShowEmojiPicker={setShowEmojiPicker} showEmojiPicker={showEmojiPicker} message={message} handleMessageChange={handleMessageChange} /> </footer> </section> ); } export default ChatScreen;
채팅 화면의 주요 구성 요소에는 ChatMessages 및 MessageForm 구성 요소가 포함됩니다.
메시지폼.js
import { Smile } from "react-feather"; import { Picker } from "emoji-mart"; function MessageForm({ emojiSelect, handleMessageSend, setShowEmojiPicker, showEmojiPicker, message, handleMessageChange, }) { let messageInput; const handleEmojiSelect = (emoji) => { emojiSelect(emoji); messageInput.focus(); }; return ( <div> {showEmojiPicker ? ( <Picker title="" set="apple" onSelect={handleEmojiSelect} /> ) : null} <form onSubmit={handleMessageSend} className="message-form"> <button type="button" onClick={() => setShowEmojiPicker(!showEmojiPicker)} className="toggle-emoji" > <Smile /> </button> <input type="text" value={message} ref={(input) => (messageInput = input)} onChange={handleMessageChange} placeholder="Type your message here..." className="message-input" /> </form> </div> ); } export default MessageForm;
메시지 입력 필드의 논리 및 렌더링(이모지 선택기 및 메시지 입력 상자)이 포함되어 있습니다.
사이드바 만들기
응용 프로그램 구조의 마지막 구성 요소는 "사이드바", 즉 채널 목록을 포함합니다.
Channels.js
import { useState } from "react"; function Channels({ nickname, channelsLoading, channels, channel, setChannel, }) { return ( <aside className="sidebar left-sidebar"> <div className="user-profile"> <span className="username">@ {nickname}</span> </div> <div className="channels"> <ul className="chat-channels"> {channelsLoading ? ( <li> <span className="channel-name">Loading channels....</span> </li> ) : channels.length ? ( channels.map((c) => { return ( <li key={c.id} onClick={() => setChannel(c.name)} className={c.name === channel ? "active" : ""} > <span className="channel-name">{c.name}</span> </li> ); }) ) : ( <li> <span className="channel-name">No channels available</span> </li> )} </ul> </div> </aside> ); } export default Channels;
애플리케이션이 준비되었습니다. 여기에서 고려해야 할 유일한 것은 helpers/socket.js 에서 API URL을 확인하는 것입니다.
const SOCKET_URL = "http://localhost:8080";
사용 중인 백엔드 서버의 URL 및 PORT에 맞게 변경해야 합니다.
프런트 엔드 부분과 서버를 모두 실행합니다.
루트 디렉터리로 이동하여 다음을 실행합니다.
npm start
src/server로 이동하여 다음을 실행합니다.
npm start
이제 http://localhost:3000 또는 프런트 엔드 부분에 사용하는 다른 포트를 열고 액세스할 수 있습니다.
이 응용 프로그램에는 매우 기본적인 기능이 있습니다. 로그인 인증이 없습니다. 닉네임만 있으면 됩니다. 그리고 자신만의 채널을 만들 수 없습니다. 제공되는 고정 채널 간에만 전환할 수 있습니다.
잘 했어요!
당신은 우리의 가이드를 따랐고 이제 당신은 당신만의 단순화된 Slack과 같은 채팅 애플리케이션을 갖게 되었습니다.
자유롭게 실험하고 앱을 확장하십시오. 다음과 같은 것을 추가할 수 있습니다.
- "사용자가 입력 중" 기능입니다.
- 채널 가입/생성을 위한 기능입니다.
- 입증.
- 사용자 아바타.
- 온라인에서 활성 상태인 사용자를 표시하는 기능입니다.
마음에 드는지 알려주세요. 추가 지원이 필요하면 주저하지 말고 저희에게 연락하십시오.