채팅 구현 프로젝트(1): 시작

리린·2021년 8월 27일
0

웹 소켓

목록 보기
1/1

강의

https://youtu.be/ZwFA3YMfkoc?t=354

모듈 설치

  1. 백엔드 모듈 설치 : cors, nodemon, express, socket.it
npm install --save cors nodemon express socket.io 
  1. 프론트엔드 모듈 설치: react-router, socket.io-client, react-scroll-to-bottom, react-emoji, query-string
npm install --save react-router socket.io-client react-scroll-to-bottom react-emoji query-string

폴더 구조

[서버] 스크립트 추가

  • server> package.json
"scripts" {
	"start": "nodemon index.js", 
    ...

[클라이언트] 기본 파일/폴더 추가

  • src 폴더 생성 후 App.js 와 index.js 추가
  1. index.js 에 다음과 같은 내용 추가
import React from 'react';
import ReactDOM from 'react-dom';

import App from './App';

ReactDOM.render(<App />, document.getElementById('root'));
  1. App.js에 다음과 같은 내용 추가
import React from 'react';

import Chat from './components/Chat/Chat';
import Join from './components/Join/Join';

import { BrowserRouter as Router, Route } from "react-router-dom";

const App = () => {
  return (
    <Router>
      <Route path="/" exact component={Join} />
      <Route path="/chat" component={Chat} />
    </Router>
  );
}

export default App;
  1. components/Chat.js 에 다음과 같은 내용 추가
import React, { useState, useEffect } from "react";

  return (
    <h1>임시</h1>
  );
}

export default Chat;
  1. components/Join.js 에 다음과 같은 내용 추가
import React, { useState, useEffect } from "react";

  return (
    <h1>임시</h1>
  );
}

export default Chat;

[서버] 기본 파일/폴더 추가

  1. 참고서
    공식사이트: https://socket.io/
  • 리얼타임으로 무언가를 하고 싶다면 무조건 웹소켓을 써야 한다
  1. server 폴더 안에 index.js 추가
const http = require('http');
const express = require('express');
const socketio = require('socket.io');
const cors = require('cors');

const { addUser, removeUser, getUser, getUsersInRoom } = require('./users');



const app = express();
const server = http.createServer(app);
const io = socketio(server);

server.listen(process.env.PORT || 5000, () => console.log(`Server has started.`));

1) 모듈 가져오기
2) PORT 지정하기
3) express()로 서버 만들기
4) http 만들기
5) io 서버 만들기

  1. server/ router.js 만들기
const express = require("express");
const router = express.Router();

router.get("/", (req, res) => {
  res.send({ response: "Server is up and running." }).status(200);
});

module.exports = router;
  1. index.js에 router.js 추가하기
const router = require('./router');
app.use(router) 

[서버]io 연결하기

  1. disconnect

io.on('connect', (socket) => {
  socket.on(
  console.log('we are here!');
  
  socket.on('disconnect', () => {
    const user = removeUser(socket.id);
  })
});

[클라이언트] 컴포넌트 만들기

  1. components/Join/Join.js
import React, { useState } from 'react';
import { Link } from "react-router-dom";

import './Join.css';

export default function SignIn() {
  const [name, setName] = useState('');
  const [room, setRoom] = useState('');

  return (
    <div className="joinOuterContainer">
      <div className="joinInnerContainer">
        <h1 className="heading">Join</h1>
        <div>
          <input placeholder="Name" className="joinInput" type="text" onChange={(event) => setName(event.target.value)} />
        </div>
        <div>
          <input placeholder="Room" className="joinInput mt-20" type="text" onChange={(event) => setRoom(event.target.value)} />
        </div>
        <Link onClick={e => (!name || !room) ? e.preventDefault() : null} to={`/chat?name=${name}&room=${room}`}>
          <button className={'button mt-20'} type="submit">Sign In</button>
        </Link>
      </div>
    </div>
  );
}

1) onChange로 데이터 가져오기
2) link to에 쿼리스트링 형식으로 설정하기

  1. components/Chat/Chat.js
import React, { useState, useEffect } from "react";
import queryString from 'query-string';
import io from "socket.io-client";

import TextContainer from '../TextContainer/TextContainer';
import Messages from '../Messages/Messages';
import InfoBar from '../InfoBar/InfoBar';
import Input from '../Input/Input';

import './Chat.css';

const ENDPOINT = 'https://project-chat-application.herokuapp.com/';

let socket;

const Chat = ({ location }) => {
  const [name, setName] = useState('');
  const [room, setRoom] = useState('');
  const [users, setUsers] = useState('');
  const [message, setMessage] = useState('');
  const [messages, setMessages] = useState([]);

  useEffect(() => {
    const { name, room } = queryString.parse(location.search);

    socket = io(ENDPOINT);

    setRoom(room);
    setName(name)

    socket.emit('join', { name, room }, (error) => {
      if(error) {
        alert(error);
      }
    });
  }, [ENDPOINT, location.search]);
  
  useEffect(() => {
    socket.on('message', message => {
      setMessages(messages => [ ...messages, message ]);
    });
    
    socket.on("roomData", ({ users }) => {
      setUsers(users);
    });
}, []);

  const sendMessage = (event) => {
    event.preventDefault();

    if(message) {
      socket.emit('sendMessage', message, () => setMessage(''));
    }
  }

  return (
    <div className="outerContainer">
      <div className="container">
          <InfoBar room={room} />
          <Messages messages={messages} name={name} />
          <Input message={message} setMessage={setMessage} sendMessage={sendMessage} />
      </div>
      <TextContainer users={users}/>
    </div>
  );
}

export default Chat;

1) join에서 쿼리스트링으로 정보를 담에 Chat.js로 넘겼다
( 리덕스 관리로 해도 되는 정보지만, 뭐 그렇게 해도 좋지)

import queryString from 'query-string';
const { name, room } = queryString.parse(location.search);

2) 소켓과 연결하기

  • 바깥에 socket을 선언하고
let socket
  • 컴포넌트 내부에 다음과 같이 작성
const ENDPOINT = 'localhost:5000'
  • useEffect 내부 콜백함수에 다음과 같이 작성
socket = io(ENDPOINT)
  • useEffect 두 번째 인자에 변경 트리거 인자 배열 적어두기

3) join 이벤트와 전달할 데이터 정하기

socket.emit("join", {name, room})

4) useEffect return 전에는 join 이벤트를 emit 시키고, 이후에는 disconnect 이벤트를 emit 시킨다

[서버] 연결하기

  1. index.js

io.on('connect', (socket) => {
  socket.on('join', ({ name, room }, callback) => {
    const { error, user } = addUser({ id: socket.id, name, room });

    if(error) return callback(error);

    socket.join(user.room);

    socket.emit('message', { user: 'admin', text: `${user.name}, welcome to room ${user.room}.`});
    socket.broadcast.to(user.room).emit('message', { user: 'admin', text: `${user.name} has joined!` });

    io.to(user.room).emit('roomData', { room: user.room, users: getUsersInRoom(user.room) });

    callback();
  });

  socket.on('sendMessage', (message, callback) => {
    const user = getUser(socket.id);

    io.to(user.room).emit('message', { user: user.name, text: message });

    callback();
  });

  socket.on('disconnect', () => {
    const user = removeUser(socket.id);

    if(user) {
      io.to(user.room).emit('message', { user: 'Admin', text: `${user.name} has left.` });
      io.to(user.room).emit('roomData', { room: user.room, users: getUsersInRoom(user.room)});
    }
  })
});

1) join 이벤트 발생시 데이터와 컬백함수를 전달받다.

  • 콜백함수는 에러처리를 위해 쓰이는 것이고, 이 에러는 서버에게서 전달되는 듯

io.on('connect', (socket) => {
  socket.on('join', ({ name, room }, callback) => {
    const { error, user } = addUser({ id: socket.id, name, room });

    if(error) return callback(error);

    socket.join(user.room);

    socket.emit('message', { user: 'admin', text: `${user.name}, welcome to room ${user.room}.`});
    socket.broadcast.to(user.room).emit('message', { user: 'admin', text: `${user.name} has joined!` });

    io.to(user.room).emit('roomData', { room: user.room, users: getUsersInRoom(user.room) });

    callback();
  });

  socket.on('sendMessage', (message, callback) => {
    const user = getUser(socket.id);

    io.to(user.room).emit('message', { user: user.name, text: message });

    callback();
  });

  socket.on('disconnect', () => {
    const user = removeUser(socket.id);

    if(user) {
      io.to(user.room).emit('message', { user: 'Admin', text: `${user.name} has left.` });
      io.to(user.room).emit('roomData', { room: user.room, users: getUsersInRoom(user.room)});
    }
  })
});
  1. server/users.js
const users = [];

const addUser = ({ id, name, room }) => {
  name = name.trim().toLowerCase();
  room = room.trim().toLowerCase();

  const existingUser = users.find((user) => user.room === room && user.name === name);

  if(!name || !room) return { error: 'Username and room are required.' };
  if(existingUser) return { error: 'Username is taken.' };

  const user = { id, name, room };

  users.push(user);

  return { user };
}

const removeUser = (id) => {
  const index = users.findIndex((user) => user.id === id);

  if(index !== -1) return users.splice(index, 1)[0];
}

const getUser = (id) => users.find((user) => user.id === id);

const getUsersInRoom = (room) => users.filter((user) => user.room === room);

module.exports = { addUser, removeUser, getUser, getUsersInRoom };

socket.io

socket io

  1. 기본 가져오기
..
const app = express()
const server = http.createServer(app)
cconst io = socketio(server)
  1. socketio(server).on( 이벤트, 콜백함수)
  • 이벤트를 들으면 콜백함수를 실행한다는 의미
  • 예시1: 백엔드 서버에서 '연결' 이벤트를 들을 때
io.on('connection', (socket)=> .. 
  1. socket.on
  • 소켓 이벤트를 들으면 실행한다는 의미.
  • 첫 번째 인자는 이벤트 이름, 두 번째 인자는 전달되는 콜백함수다
  • 두 번째 인자(콜백함수)의 첫 번째 인자는 객체고 두 번째 인자는 콜백함수다.
  • 두 번째 인자 안에는 받아들인 인자가 컨트롤러에 의해 가공되어 서버에 저장되어지거나, 콜백함수를 리턴함으로써 에러처리를 하게 된다.
  • 예시1: 백엔드 서버에서 '연결' 이벤틀르 듣고 있을 때, {name, room} 이라는 데이터과 콜백함수가 인자로 전달되는 콜백함수를 실행할 때
io.on('connection', (socket)=>{
	socket.on('join', ({ name, room}, callback) =>{
		const{ error, user} = addUser({id: socket.id, name, room})
    	if (error) return callback(error);
    ...
  1. socket.emit
  • 소켓 이벤트 트리거라는 의미
  • 첫 번째 인자는 이벤트 이름, 두 번째 인자는 전달되는 객체 데이터다.
io.on('connection', (socket)=>{
	socket.on('join', ({ name, room}, callback) =>{
		const{ error, user} = addUser({id: socket.id, name, room})
    	if (error) return callback(error);
            socket.emit('message', { user: 'admin', text: `${user.name}, welcome to room ${user.room}.`}); 
  1. socket.broadcast
  • 모두 전달한다는 의미
  1. socket.to(대상)
  • 예시 for 5, 6

io.on('connect', (socket) => {
  socket.on('join', ({ name, room }, callback) => {
    const { error, user } = addUser({ id: socket.id, name, room });

    if(error) return callback(error);

    socket.join(user.room);

    socket.emit('message', { user: 'admin', text: `${user.name}, welcome to room ${user.room}.`});
    socket.broadcast.to(user.room).emit('message', { user: 'admin', text: `${user.name} has joined!` });

[클라이언트 구성]

  1. useState
  • input에 입력하는 value를 가져오고자 사용한다
  const [name, setName] = useState('');
  const [room, setRoom] = useState('');
  const [users, setUsers] = useState('');
  const [message, setMessage] = useState('');
  const [messages, setMessages] = useState([]);
  ...
  
  setRoom(room);
    setName(name)

..
  1. ENDPOINT
  • 서버와 연결하기 위해 사용한다
  1. useEffect (1) : 방에 입장할 경우
    1) return 이전
  • queryString으로 name과 room 등의 정보를 받아온다
const { name, room } = queryString.parse(location.search);

2) socket을 io(서버) 로 정의한다

socket = io(ENDPOINT);

3) 값을 세팅한다 (useState)

setRoom(room);
    setName(name)

4) 이벤트를 발생시킨다

socket.emit('join', { name, room }, (error) => {
      if(error) {
        alert(error);
      }
    });

5) ENDPOINT와 location.search가 바뀔 때만 리렌더링한다
(전체파일)

  useEffect(() => {
    const { name, room } = queryString.parse(location.search);

    socket = io(ENDPOINT);

    setRoom(room);
    setName(name)

    socket.emit('join', { name, room }, (error) => {
      if(error) {
        alert(error);
      }
    });
  }, [ENDPOINT, location.search]);
  
  1. useEffect(2): 메시지를 보낼 경우 /

    
     useEffect(() => {
       socket.on('message', message => {
         setMessages(messages => [ ...messages, message ]);
       });
       
       socket.on("roomData", ({ users }) => {
         setUsers(users);
       });
    }, []);
  2. return 하는 컴포넌트

    
     return (
       <div className="outerContainer">
         <div className="container">
             <InfoBar room={room} />
             <Messages messages={messages} name={name} />
             <Input message={message} setMessage={setMessage} sendMessage={sendMessage} />
         </div>
         <TextContainer users={users}/>
       </div>
     );
  3. event.prevent
    기본 이벤트를 블록하는 것이다.

profile
개발자지망생

0개의 댓글