Socket IO

이경택·2023년 8월 29일

백엔드

목록 보기
5/5
post-thumbnail

Socket IO

Websocket은 클라이언트와 서버가 다른 인터페이스를 가지고 사용해야 하며 Websocket 객체 또한 모든 브라우저에서 사용할 수 있는 객체가 아니기 때문에 Socket IO 라이브러리를 사용한다.

⇒ node.js에서 Websocket을 사용할 때 훨씬 편하게 사용할 수 있게 만들어주는 모듈

특징

  • Websocket을 브라우저에서 사용할 수 없는 경우 롱 폴링으로 대체됨
  • 주기적으로 연결 상태를 확인하는 하트비트 매커니즘
  • 연결 해제된 상태에서의 패킷은 버퍼링, 다시 연결되면 전송
  • 브로드캐스팅
    • 서버 측에서 연결된 모든 클라이언트 또는 하위 클라이언트 하위 집합에 이벤트를 보낼 수 있음
  • 멀티플렉싱

nodejs / client 코드

// node

const express = require('express')

const app = express()

const http = require('http')
const { Server } = require('socket.io')
const server = http.createServer(app)

const io = new Server(server)
// 위 까지가 서버 연결 및 io 설정

// socket 연결 되었을 때 ('connection')
io.on('connection', (socket) => {
// socket에는 socket id 등 정보와 메서드들이 들어있다.

// socket.on(원하는 이름, 콜백 함수) 

// 클라이언트에서 받은 data를 onMessage라는 이름으로 다시 클라이언트에 보내준다.
// 내 클라이언트 이외의 클라이언트들에게도 내가 보낸 메시지가 전달되어야 하기 때문에 작성
	socket.on('message',(data) => {
    	socket.emit('onMessage', {
          {id: data.id, message: data.message}
        })
    })

// client에서 'join'이라는 이름으로 emit 한 값들을 받는다.
  socket.on('join', (options, callback) => {
	// addUser을 통해 users에 user추가, addUser는 커스텀 함수
		const {error, user} = addUser(socket.id, ...options)
	
// addUser에서 에러가 발생했다면 callback 호출 => callback은 클라이언트에서 넘겨줬던 error 콜백
	if(error) callback(error)

	// user가 입력한 room으로 이동
    socket.join(user.room)
	// broadcast.to(룸 이름) => 나를 제외하고 방에 있는 모든 유저에게 보냄
		socket.broadcast.to(원하는 방).emit('message', 원하는 글)
	})
})

const port = 3000
app.listen(port, () => {
  console.log(`Server is listening on ${port}`)
})

client

import io from 'socket.io-client'
const socket = io('http://localhost:3000')

function App () {
	const router = useRouter()
	const query = new URLSearchParams(location.search)
	const username = query.get('username')
	const room = query.get('room')
    
const [inputValue, setInputValue] = useState('')

const onChangeInputValue = (e: React.ChangeEvent<HTMLInputElement>) => {
  setInputValue(e.target.value)
}

// 채팅방 입장할 때 로직
	const enterRoom = () => {
    socket.emit('join', { username, room }, (error) => {
			if(error) {
				alert(error) // 다른 에러 처리 방식 사용할 것이지만 예시로 alert
				router.push('/')
			}
		})
	}

// 메시지 보내는 로직
const sendMessage = () => {
	if (!inputValue) return
    socket.emit('message', { message: inputValue, id: socket.id })
}

// 다른 사람들이 입력한 메시지 받는 로직	
=> 아래 에러에 따라 이 부분도 핸들러 함수를 만들고 return문에 socket.off 메서드를 호출
	useEffect(() => {
		// on(서버와 통신 할 이름, 콜백함수)
		socket.on('message', (data) => {
			console.log(data) // 서버에서 emit으로 받은 데이터
		})
	},[socket]) // socket의 변화가 있을 때 마다 호출

	useEffect(() => {
		const handleMessage = (data: MsgType) => {
      setChats((prev) => [...prev, data])
    }
    // on(서버와 통신 할 이름, 콜백함수)
    socket.on('onMessage', handleMessage)
    // 이벤트가 끝날 때 이벤트리스너 제거
    return () => {
      socket.off('onMessage', handleMessage)
	  }
	},[])
// 의문점 1. useEffect문의 의존성 배열이 빈 배열인데 어떻게 socket이벤트를 계속 받을 수 있는지?

// 소켓 연결
	useEffect(() => {
    if (socket.connected === false) socket.connect()
    return () => {
      socket.disconnect()
    }
  }, [])
}

에러

채팅을 보냈을 때 서버에는 한 번 보내지지만 받아올 때는 두 번 받아와지는 에러

useEffect 문을 통해 socket에 변화가 있을 때 마다 socket.on 메서드를 통해 서버에서 emit 해주는 데이터를 받아서 프론트에서 처리를 해주려고 했었다.
프론트에서 보내는 채팅 문자를 서버에서 받아서 연결되어 있는 모든 프론트에게 데이터를 보내주는 방식인데,
분명 프론트에서 보내는 데이터를 서버에서 콘솔 찍어봤을 때는 단일 데이터가 들어오는데 프론트의 useEffect 문에서 찍어보면 두 개의 데이터가 찍혔다.

처음에는 서버에서 보내주는 데이터의 문제인가 싶어서 서버쪽 코드를 봤는데 서버에는 단순히 emit 해주는 코드밖에 없었기 때문에 useEffect 문에서 문제를 찾아보자 했고 문제를 해결할 수 있었다.

문제는 useEffect문의 이벤트 핸들러가 중복 호출되는 문제였고 return문을 통해 socket.off 메서드를 호출해 이벤트 리스너를 제거해주니 문제가 해결되었다.

Chat GPT의 첨언
이벤트 리스너를 등록하면, 이벤트가 여러 번 발생할 수 있으며 각 이벤트마다 해당 리스너 함수가 실행됩니다.
때로는 이러한 이벤트 리스너가 중복으로 등록되어 같은 이벤트가 여러 번 처리될 수 있는 상황이 발생할 수 있습니다.
이는 예상치 못한 결과를 초래할 수 있으며, 특히 웹소켓과 같은 통신에서는 데이터의 무결성을 보장하기 위해 중복 호출을 방지해야 합니다.
따라서 socket.off를 사용하여 이벤트 리스너를 명시적으로 제거하는 것은 코드의 예측 가능성과 안정성을 높이는 데 도움이 됩니다.
특히 React 컴포넌트의 생명주기와 같이 이벤트 리스너를 적절하게 추가 및 제거하면 메모리 누수나 불필요한 리소스 소비를 방지할 수 있습니다

의문점

    useEffect(() => {
      const handleMessage = (data: MsgType) => {
        setChats((prev) => [...prev, data])
      }
      // on(서버와 통신 할 이름, 콜백함수)
      socket.on('onMessage', handleMessage)
      // 이벤트가 끝날 때 이벤트리스너 제거
      return () => {
        socket.off('onMessage', handleMessage)
      }
    },[])

useEffect 문의 의존성 배열은 빈 배열이다.

빈 배열일 때 useEffect 문은 컴포넌트 렌더링 시에 한 번만 동작하고 동작하지 않는다.

우리는 socket의 변화가 있을 때 마다 (새로운 데이터가 있을 때 마다) 변화를 감지하고 새로운 데이터를 받아서 화면에 그려줘야 한다.

어떻게 동작하는 것일까?

socket.on 이벤트는 컴포넌트가 처음 렌더링 될 때 호출된다.

그리고 컴포넌트가 언마운트 되기 전 까지는 off되지 않으며 on 된 socket은 이후 컴포넌트의 렌더링 여부와 관계 없이 계속 on 되어있어, 변화를 감지할 수 있다.

또한 컴포넌트가 언마운트 될 때는 더 이상 socket을 유지 할 필요가 없으므로 off를 해준다.

Chat GPT의 칭찬
좋은 질문입니다! useEffect 훅의 의존성 배열이 빈 배열인 경우, 이는 해당 useEffect가 컴포넌트가 처음 렌더링될 때만 실행되고, 그 이후에는 다시 실행되지 않는다는 것을 의미합니다. 그러나 여기서 socket.on을 통해 등록한 이벤트 리스너는 컴포넌트 렌더링 여부와는 관계없이 계속 존재합니다.

useEffect 내부의 함수에 socket.on을 사용하여 이벤트 리스너를 등록하면, 해당 이벤트 리스너는 컴포넌트가 언마운트되지 않는 한 계속 존재합니다. 그렇기 때문에 의존성 배열이 빈 배열인 useEffect는 컴포넌트가 처음 렌더링될 때 한 번만 실행되어 등록된 이벤트 리스너를 설정하고, 반환된 함수는 컴포넌트가 언마운트될 때 이벤트 리스너를 해제합니다.

등록된 이벤트 리스너가 컴포넌트의 상태나 다른 컴포넌트의 상태에 의존하지 않기 때문에, 의존성 배열이 빈 배열인 useEffect를 사용하여 이벤트 리스너를 등록하고 해제하는 패턴을 사용해도 괜찮습니다. 새로운 데이터가 도착하면 socket.on을 통해 등록된 이벤트 리스너가 동작하고, 해당 이벤트 리스너는 상태나 프롭스의 변경에 영향을 받지 않습니다.

profile
한 줄로 소개 할 수 없는 개발자

0개의 댓글