본문 바로가기

Develop_story/TIL(Today I Learned)

React socket.io-client

SMALL

  이번 실전 프로젝트를 진행하면서 채팅 방을 구현하게 되었다. 백엔드에서 이미 socket.io-client를 찾아보시고 어느 정도 공부를 진행하셔서 나 또한 socket io로 채팅을 구현하게 되었다.

  코드는 프론트엔드 위주로 서술 될 것이고, 이해를 위해 백엔드 코드를 살짝 작성할 예정이다. (같이 프로젝트를 진행한 팀원의 코드이며 허락을 받았음)

1. socket.io 를 사용하기 위한 사전 작업

npm install socket.io-client
  • npm을 이용해 socket.io-client 설치
const socket = io(`${process.env.REACT_APP_SERVER}`, { transports: ['websocket'] })
  • 라이브러리에 설치되어 있는 io를 사용해 socket 서버의 주소를 첫 번째 인자에 두고, 두 번째 인자에는 위와 같이 작성한다.
  • 두 번째 인자를 작성하지 않았을 때, socket이 연결되어있지 않다는 error가 발생해 작동하지 않았다. (Trouble shooting 글에 올려져 있습니다.)
// socket을 연결하는 함수
const initSocketConnection = () => {
  socket.connect()
}
// socket의 연결을 끊는 함수
const disconnection = () => {
  socket.disconnect()
}
...
function ChatRoom () {
  ...
}
  • 소켓을 연결하는 함수와 끊는 함수를 최상단에 선언했다.useEffect를 사용해 채팅 방에 들어왔을 때, 위에 선언했던 socket을 연결하고, unmount를 했을 시 연결을 끊는다.

2. 기본적으로 작동하는 방법 (emit, on)

  처음에 emit과 on이 무슨 작동을 하는지 처음에 헷갈렸다. 하지만 조금 더 생각을 하고 백엔드 팀원과 얘기를 해보니 emit과 on은 CRUD로 get, post 하는 것과 비슷해보였다.

  • emit
    • CRUD 로 생각했을 때 post 요청과 비슷하다 생각했다.
    • 첫 번째 인자는 백엔드와 맞춘 event 이름을 적어준다.
    • 두 번째 인자는 넘겨줄 값을 적어주면 된다.
  • on
    • CRUD 로 생각했을 때 get 요청과 비슷하다 생각했다.
    • 첫 번째 인자는 emit와 동일하게 백엔드와 맞춘 event 이름을 적어준다.
    • 두 번째 인자는 emit으로 받는 data를 적어준다.

3. 방에 접속했을 때 작동하는 event

useEffect(() => {
    initSocketConnection()
		// 채팅방에 접속했을 시 roomId를 백엔드에 넘겨줌
    socket.emit('roomId', roomId)
		// 채팅방에 접속했을 시 자신의 nickname을 백엔드에 넘겨줌
    socket.emit('newUser', nickname)
    return () => {
			// 채팅방을 나갔을 시 나간 자신의 nickname을 백엔드에 넘겨줌
      socket.emit('offUser', nickname)
			// userLisg (현재 접속해있는 user를 제거함)
      setUserList(userList.filter((userList) => userList !== nickname))
      disconnection()
    }
  }, [])

4. 메세지를 백엔드에 보내는 방법

// 프론트 엔드
const onClickSendMessageHandler = useCallback(() => {
	// content가 비어있으면 채팅이 전달되지 않음
  const noContent = chatText.trim() === ''
  if (noContent) {
    return
  } else {
    socket.emit('sendMessage', chatData)
    setChatText('')
  }
}, [chatData])
  • content가 비어있으면 채팅이 전달되지 않음
  • 채팅이 비어있지 않으면 sendMessage라는 emit이 작동되고, chatData(닉네임과 input에 작성한 메세지) 를 백엔드로 보내줌
// 백엔드 
socket.on("sendMessage", function (data) {
	// 닉네임, 메세지를 data 안에 넣어줌
  data.nickname = socket.nickname;
  Chats.create({
    roomId: socket.roomId,
    nickname: data.nickname,
    message: data.message,
  });
	// on으로 받은 sendMessage event의 메세지와 닉네임을 받아와서 다시 프론트 엔드로 보내줌
  socket.emit("receiveMessage", data); // 상대방한테
  socket.to(socket.roomId).emit("receiveMessage", data); // 나한테
});

5. 보낸 메세지를 받는 방법

useEffect(() => {
  socket.on('receiveMessage', (data) => {
    setRecieveData([...recieveData, data])
  })
}, [recieveData])

4번에 있는 receiveMessage처럼 같은 event 이름을 적어주고, data를 받는다.

  • data 안에는 내가 보냈던 닉네임과 메세지를 받게 되고, 이를 receiveData에 저장

6. 채팅방에 들어와 있는 user list 보여주기

useEffect(() => {
  socket.on('onUser', (data) => {
		// 백엔드가 보내준 nickname을 userList에 넣어준다
    setUserList([...userList, data])
  })
}, [userList])

useEffect(() => {
  socket.on('offUser', (nickname) => {
		// 백엔드가 보내준 nickname과 겹치는 nickname을 userList에서 삭제
    setUserList(userList.filter((userList: string) => userList !== nickname))
  })
}, [userList])
  • 3번에서 작동하는 emit event를 토대로 백엔드가 onUser, offUser에 대한 data를 넘겨주고, 이를 setUserList 핸들러를 통해 userList를 핸들링 한다.

7. 전체 코드

import React, { useRef } from 'react'
import { useState, useEffect, useCallback } from 'react'
import { useParams } from 'react-router-dom'
import { io } from 'socket.io-client'

const socket = io(`${process.env.REACT_APP_SERVER}`, {
  transports: ['websocket'],
})
const initSocketConnection = () => {
  socket.connect()
}
const disconnection = () => {
  socket.disconnect()
}

function ChatRoom() {
  const [chatText, setChatText] = useState<string>('')
  const [recieveData, setRecieveData] = useState<RecieveData[]>([])
  const [beforeChatData, setBeforeChatData] = useState<BeforeChatData[]>([])
  const [userList, setUserList] = useState<string[]>([])

  const param = useParams()

  const nickname: string = 'jaeuk'
  const roomId: number = Number(param.id)

  useEffect(() => {
    initSocketConnection()
    socket.emit('roomId', roomId)
    socket.emit('newUser', nickname)
    return () => {
      socket.emit('offUser', nickname)
      setUserList(userList.filter((userList) => userList !== nickname))
      disconnection()
    }
  }, [])

  const onSubmitChattingHandler = (e: React.FormEvent<HTMLFormElement>) => {
    e.preventDefault()
  }
  const onChangeChatTextHandler = (e: React.ChangeEvent<HTMLInputElement>) => {
    setChatText(e.target.value)
  }
  const onClickSendMessageHandler = useCallback(() => {
    const noContent = chatText.trim() === ''
    if (noContent) {
      return
    } else {
      socket.emit('sendMessage', chatData)
      setChatText('')
    }
  }, [chatData])

  useEffect(() => {
    socket.on('onUser', (data) => {
      setUserList([...userList, data])
    })
  }, [userList])

  useEffect(() => {
    socket.on('offUser', (nickname) => {
      setUserList(userList.filter((userList: string) => userList !== nickname))
    })
  }, [userList])

  useEffect(() => {
    socket.on('receive', (data) => {
      setBeforeChatData(data)
    })
  }, [beforeChatData])

  useEffect(() => {
    socket.on('receiveMessage', (data) => {
      setRecieveData([...recieveData, data])
    })
  }, [recieveData])
  
  return (
    <StDivChatRoomWrap>
      <StDivChatRoomChatListWrap ref={scrollRef}>
        {beforeChatData?.map((beforeChatData) => {
          return (
            <StDivChatRoomChatListContain key={beforeChatData.chatId}>
              <StPChatRoom>
                {beforeChatData.nickname} : {beforeChatData.message}
              </StPChatRoom>
            </StDivChatRoomChatListContain>
          )
        })}

        {recieveData.map((recieveData, index) => {
          return (
            <StDivChatRoomChatListContain
              key={`${recieveData.message} + ${index}`}
            >
              <StPChatRoom>
                {recieveData.nickname} : {recieveData.message}
              </StPChatRoom>
            </StDivChatRoomChatListContain>
          )
        })}
      </StDivChatRoomChatListWrap>

      <div>
        <form onSubmit={onSubmitChattingHandler}>
          <input
            value={chatText}
            onChange={onChangeChatTextHandler}
            placeholder="채팅 입력"
          />
          <button onClick={onClickSendMessageHandler}>보내기</button>
        </form>
      </div>
      <div>
        {userList.map((v) => {
          return (
            <div key={v}>
              {' '}
              <p>{v}</p>{' '}
            </div>
          )
        })}
      </div>
    </StDivChatRoomWrap>
  )
}

export default ChatRoom

 

LIST

'Develop_story > TIL(Today I Learned)' 카테고리의 다른 글

Music Player Bar  (0) 2023.04.20
채팅방 역 무한 스크롤 구현 하기 (intersection-observer)  (0) 2023.03.24
TypeScript  (0) 2023.03.11
REACT-COOKIE / basic  (0) 2023.03.01
RESTful API  (0) 2023.02.28