PJH's live chat - Socket.io

박정호·2023년 2월 6일
0

Live Chat Project

목록 보기
5/7
post-thumbnail

🚀 Start

채팅 앱의 핵심 기술이자 이번에 처음 써보는 Socket.io란 무엇일까?



⭐️ Socket.io

Socket.io란 WebSocket을 기반으로 클라이언트와 서버의 양방향 통신을 가능하게 해주는 기술이다. WebSocket을 그대로 사용해도 좋지만, Socket.io에는 편의 기능이 많이 추가되어 개발하는 입장에서 용이하다.

WebSocket이 뭔데?

Websocket은 소켓을 이용하여 자유롭게 데이터를 주고 받을 수 있어 기존의 요청-응답 관계 방식 보다 더 쉽게 데이터를 교환할 수 있다.

Socket이 뭔데?

socket란 두 호스트를 연결해주는 도구(통로)를 말한다.


💡 웹소켓(WebSocket)의 배경

1️⃣ 인터넷이 나오고 HTTP를 통해서 서버로부터 데이터를 가져오기 위해서는 오로지 URL을 통한 요청이 유일한 방법이었다. 때문에 아이디 중복 확인과 같은 유효성 검사는 서버로 데이터를 보내는 중간과정에서 새로운 페이지 요청을 하게 되었다.


2️⃣ 여기서 발전된 방식이 Ajax통신으로 클라이언트에서 XMLHttpRequest 객체를 이용하여 서버에 요청을 보내면 서버가 응답을 하는 방식이다.

따라서, 페이지 요청이 아닌 데이터 요청이라 부분적으로 정보를 갱신할 수 있게 된다.

즉, 사용자의 이벤트로부터 Javascript는 사용자가 작성한 값이 쓰여진 DOM을 읽는다.

그리고 XMLHttpRequest 객체를 통해 웹서버에 해당 값을 전송하고 웹서버는 요청을 처리하고 XML, Text, JSON 등을 이용하여 XMLHttpRequest 객체에 전송한다. 그런다음 JavaScript가 해당 응답정보를 DOM에 쓰여진다.

Ajax를 사용하면 새로운 HTML을 서버로부터 받아야하는 것이 아닌 동일한 페이지의 일부를 수정할 수 있는 가능성이 생기고, 사용자 입장에서는 페이지 이동이 발생되지 않고 페이지 내부 변화만 일어나게 해주므로 그만큼의 자원과 시간을 아낄수 있다.


3️⃣ 하지만, Ajax도 결국 HTTP를 이용하기 때문에 요청을 보내야 응답이 온다.

변경된 데이터를 가져오기 위해서 버튼을 누른다거나 일정 시간 주기로 요청을 보낸다면 번거로울 뿐더러 자원 낭비이다.

예를 들어 주식에서 기업의 주가를 보려면 매번 버튼을 갱신해서 요청을 하고 서버는 이에 응답을 해주는 것이다. (그냥 알아서 실시간으로 변경되는 모습을 보고싶은데 ㅜㅜ)

👍 이러한 문제들을 해결하기 위해 웹소켓이 탄생



✔️ useSocket

socket.io는 연길시 전역적인 특징을 가진다. 즉, 각각의 컴포넌트에서 사용하게 되면, 페이지 이동으로 인한 컴포넌트 변화마다 socket연결이 끊어지는 일이 발생할 수 있다.

따라서, useSocket이라는 커스텀훅을 생성하여 공통된 기능을 관리하고, 양방향 통신이 필요한 컴포넌트에서 해당 훅을 가져와 쓰는 식으로 진행해보자.


Code - 중요!

1️⃣ 빈 객체 생성 (workspace가 들어갈 공간)

2️⃣ 서버 socket접속용 객체 생성 및 연결

  • io : http server를 socket.io server로 upgrade

3️⃣ socket.io는 처음에 polling 연결을 시도하고, 웹소켓이 지원되는 브라우저인 경우, ws통신으로 이행한다. 즉, 먼저 http로 연결해서 웹소켓 지원여부를 확인하고, 확인되면 ws로 업그레이드 해주는 방식을 띈다. 따라서, http 통신으로 인한 CORS 정책에 또 걸리게된다.

  • 처음부터 ws로 통신하고자 할 경우, transports 옵션 값을 ['websocket']으로 추가 설정해주면 된다.
  • 웹소켓의 프로토콜은 ws:// 인데 http://를 써주는 이유가 바로 이것
    • backUrl = 'ws://localhost:3095'; (X)
       backUrl = 'http://localhost:3095'; (O)

4️⃣ disconnect: 웹소켓 연결 중지 메서드

  • 워크스페이스 이동시, 즉 채팅방이 다른데 다른 채팅방에도 데이터가 통신되면 안되니까.
  • sockets[workspace].disconnect(): 핻아 워크스페이스 소켓 연결 중지
  • delete sockets[workspace]: 객체에서 해당 워크스페이스 삭제
import { useCallback } from 'react';
import { io, Socket } from 'socket.io-client';

const backUrl = 'http://localhost:3095';

const sockets: { [key: string]: Socket } = {}; // 1️⃣ 번

const useSocket = (workspace?: string): [Socket | undefined, () => void] => {
  
  if (!sockets[workspace]) { // sockets 객체에 해당 workspace가 없다면...
    sockets[workspace] = io(`${backUrl}/ws-${workspace}`, { // 2️⃣ 번
      transports: ['websocket'], // 3️⃣ 번
    });
  }
  
  console.info('create socket', workspace, sockets[workspace]);
  
  const disconnect = useCallback(() => { // 4️⃣ 번
    if (workspace && sockets[workspace]) {
      sockets[workspace].disconnect();
      delete sockets[workspace];
    }
   }, [workspace]);
  
  if (!workspace)  return [undefined, disconnect]; // 워크스페이스를 인자로 받아오지 않을 경우 
  
  

  return [sockets[workspace], disconnect]; // 객체와 연결중지 메서드 두개를 return
};

export default useSocket;


✔️ socket.io in Workspace

1️⃣ socket.emit('login',{...}): 로그인한 유저에 대한 정보 송신

  • emit: 클라이언트는 서버, 서버는 클라이언트에 보내는 이벤트
  • login : 소켓을 설정한 담당자가 지정해놓은 변수 명
  • {id, channel}: 유저 아이디, 속한 채널 데이터

2️⃣ useEffect의 deps에 workspace가 설정되어있다.

  • 즉, workspace의 값이 변경에 따라 callback함수가 실행되며 이때 disconnectSocket()가 실행된다.
// sock = sockets[workspace]  , disconnectSocket = disconnect()
const [socket, disconnectSocket] = useSocket(workspace);

useEffect(() => { 
    if (channelData && userData) { 
      socket?.emit('login', { // 1️⃣ 번
        id: userData?.id,
        channels: channelData.map(v => v.id),
      });
    }
  }, [socket, userData, channelData]);

useEffect(() => { // 2️⃣ 번
    return () => {
      console.info('disconnect socket', workspace);
      disconnectSocket();
    };
  }, [disconnectSocket, workspace]);


✔️ socket.io in Channel

1️⃣ socket.on('message', (data)=>{...}): 메시지 데이터를 수신 받는 요청

  • on: 클라이언트는 서버, 서버는 클라이언트로부터 받는 이벤트
  • message : 소켓을 설정한 담당자가 지정해놓은 변수 명
  • onMessage: 송신 온 메세지가 data인수에 담겨 onMessage()실행

2️⃣ 송신 받은 데이터를 사용중인 유저의 서버에 저장된 메시지 데이터의 맨앞으로 업데이트.

  • mutate에 의해 챗팅창에 새로운 데이터 출력!!

3️⃣ on으로 인해 계속해서 데이터를 받아오면 안되므로 off는 짝꿍처럼.



const [socket] = useSocket(workspace);

const onMessage = useCallback((data: IChat) => { // 2️⃣ 번
   if(data.Channel.name === channel && data.UserId !== userData?.id)) {
      mutateChat(chatData => { 
          chatData?.[0].unshift(data);
          return chatData}, false)
      }
   },
    [channel, userData, mutateChat]
  );

useEffect(() => {
    socket?.on('message', onMessage); // 1️⃣ 번
  
    return () => {
    socket?.off('message', onMessage); // 3️⃣ 번
    };
}, [socket, onMessage]);


✔️ socket.io in DM

Channel의 양방향 통신 과정과 대부분 일치한데, Channel의 경우 여러 인원들이 메시지 데이터들을 공유하는 형식이며, DM은 1대1이기 때문에 받아올 때 수신유저 정보를 포함하고 있다.

const [socket] = useSocket(workspace);

 const onMessage = useCallback((data: IDM) => {
   if (data.SenderId === Number(id) && myData.id !== Number(id)) {
        mutateChat(chatData => {
          chatData?.[0].unshift(data);
          return chatData}, false)
      }
    }, [id, myData, mutateChat]
  );

useEffect(() => {
    socket?.on('dm', onMessage);
    return () => {
    socket?.off('dm', onMessage);
    };
}, [socket, onMessage]);


✔️ socket.io in DMList

서버로부터 로그인한 유저의 정보를 받아와 로그인한 유저들의 정보를 담는 onlinelist에 저장한다. 따라서, 해당 배열에 속한 유저를 찾아 유저의 상태 마크를 업데이트하는 것이다.

const [onlineList, setOnlineList] = useState<number[]>([]);
const [socket] = useSocket(workspace);

useEffect(() => {
    socket?.on('onlineList', (data: number[]) => {
      setOnlineList(data);
    });
  
   return () => {
      socket?.off('onlineList');
    };
  }, [socket]);


🔗 Reference
👉 [웹소켓] WebSocket의 개념 및 사용이유, 작동원리, 문제점

profile
기록하여 기억하고, 계획하여 실천하자. will be a FE developer (HOME버튼을 클릭하여 Notion으로 놀러오세요!)

0개의 댓글