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 |