Slack 클론코딩 5주차

Donghyun Hwang·2023년 12월 27일

인프런 슬랙

목록 보기
4/5
post-thumbnail

Slack 클론코딩

한 번쯤 써보고 싶었던 웹소켓에 대해 배웠다.


📚배운 점

1. socket.io

웹소켓을 통해 서버, 클라이언트 간 실시간으로 데이터를 주고 받을 수 있다.
채팅 기능을 구현하기 위해 사용하였다.

소켓 사용 프로세스는 다음과 같다.
connect을 통해 서버와 연결한다.
emit을 통해 데이터를 보낸다.
on을 통해 데이터를 받는다.
disconnect을 통해 연결을 해제한다.

서버 코드는 이미 준비되어 있기 때문에, 아래와 같이 클라이언트에서 useSocket 훅을 구현하여 필요한 곳에서 사용할 수 있도록 하였다.

useSocket 훅 구현


// 우선 알맞은 workspace에 대해 소켓 연결을 저장할 객체를 선언한다.
const sockets: { [key: string]: SocketIOClient.Socket } = {}; 

/* 
  useSocket에서 workspace 인자를 받고,
  해당 워크 스페이스에 연결된 소켓과 disconnect 함수를 반환한다.
*/
const useSocket = (workspace?: string): [SocketIOClient.Socket | undefined, () => void] => {
  // ...
  return [sockets[workspace], disconnect];
}

... 부분에 소켓 연결 생성, 연결 해제 관련 코드를 적는다.

// 소켓 연결 해제를 위한 cleanup 함수
const disconnect = useCallback(() => {
    if (workspace) {
      sockets[workspace].disconnect();
    }
  }, [workspace]);

  if (!workspace) { 
    // workspace가 올바르게 제공되지 않은 경우 undefined를 반환한다
    return [undefined, disconnect];
  }

// socket 연결이 존재하지 않을 시 생성하며, transports를 통해 polling 방식이 아닌 웹소켓으로 업그레이드한다.
  if (!sockets[workspace]) {
    sockets[workspace] = io.connect(`${backUrl}/ws-${workspace}`, {
      transports: ['websocket'],
    });
  }

  return [sockets[workspace], disconnect];

최종 코드는 다음과 같다.

// useSocket.ts
import io from 'socket.io-client';
import { useCallback } from 'react';

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

const sockets: { [key: string]: SocketIOClient.Socket } = {};
const useSocket = (workspace?: string): [SocketIOClient.Socket | undefined, () => void] => {
  const disconnect = useCallback(() => {
    if (workspace) {
      sockets[workspace].disconnect();
    }
  }, [workspace]);

  if (!workspace) {
    return [undefined, disconnect];
  }

  if (!sockets[workspace]) {
    sockets[workspace] = io.connect(`${backUrl}/ws-${workspace}`, {
      transports: ['websocket'],
    });
  }

  return [sockets[workspace], disconnect];
};

export default useSocket;

이제 다른 컴포넌트에서 useSocket 훅을 통해 소켓을 사용할 수 있다.

사용 예시

import useSocket from '@hooks/useSocket';


const [socket] = useSocket(workspace);

  const onMessage = useCallback(
    () => {
      // 메세지가 왔을 때 필요한 기능 구현
    }
    [],
  );

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

서버에서 message 이벤트를 수신할 시, onMessage 함수를 호출한다.
이 때 useEffect를 통해 컴포넌트 언마운트 시 리스너를 제거하여 메모리 누수, 원치 않은 동작을 방지한다.

느낀 점

생각보다 클라이언트에서 데이터를 주고 받는 것 자체에 필요한 코드가 많지 않은 것 같다. 오히려 스크롤 처리, 파일 업로드 등 부가적인 요소에 훨씬 할 일이 많은 것 같다.

2. 리버스 무한 스크롤

무한 스크롤은 구현해 본 경험이 있지만, 이를 역으로 구현하는 것은 처음이었다. 채팅의 경우 아래가 아닌 위로 계속 올려 이전 채팅 내역을 보기 때문에, 역방향의 무한 스크롤이 필요했다.
여기서는 swr의 도움을 받아 구현한다.

먼저 대략적인 구조는 다음과 같다.

  1. DirectMessage (혹은 Channel)ChatList의 부모 컴포넌트이다.
  2. ChatList에는 채팅 박스가(스크롤 포함) 구현되어 있다.
  3. DirectMessage에 useSWRInfinite를 정의한 뒤, ChatList에서 스크롤바의 ref를 받는다.
// DirectMessage.tsx
const DirectMessage = () => {

  const { data: chatData, mutate: mutateChat, revalidate, setSize } = useSWRInfinite<IDM[]>(
    (index) => `/api/workspaces/${workspace}/dms/${id}/chats?perPage=20&page=${index + 1}`,
    fetcher,
  );
  // index -> page의 수를 의미한다.
  
  const isReachingEnd = isEmpty || (chatData && chatData[chatData.length - 1]?.length < 20) || false;
  // isReachingEnd를 통해 채팅 데이터를 다 불러왔는지 확인한다.
                                    
  const scrollbarRef = useRef<Scrollbars>(null);
  // scrollbar에 접근하기 위해 ref 선언
  
  return (    
    // ...    
      <ChatList ref={scrollbarRef} setSize={setSize} isReachingEnd={isReachingEnd} />
    // setSize -> page 수를 바꿔주는 함수이다.
    // ...    
  );
};

export default DirectMessage;
  1. ChatList에 스크롤, 페이징 관련 함수 onScroll을 정의한다.
// ChatList.tsx
const ChatList = forwardRef<Scrollbars, Props>(({ chatSections, setSize, isReachingEnd }, scrollRef) => {
  const onScroll = useCallback(
    (values) => {
      // 채팅 스크롤이 가장 위에 닿았는지 확인
      if (values.scrollTop === 0 && !isReachingEnd) {
        // size를 변경하여 무한 스크롤이 되도록
        setSize((prevSize) => prevSize + 1).then(() => {
          // 스크롤 위치 유지 코드
          const current = (scrollRef as MutableRefObject<Scrollbars>)?.current;
          if (current) {
            current.scrollTop(current.getScrollHeight() - values.scrollHeight);
          }
        });
      }
    },
    [scrollRef, isReachingEnd, setSize],
  );
  
  
return (
    <ChatZone>
      <Scrollbars autoHide ref={scrollRef} onScrollFrame={onScroll}>
		// ...
      </Scrollbars>
    </ChatZone>
  );
});

export default ChatList;

  

체크할 부분

  • forwardRef를 통해 하위 컴포넌트(ChatList)의 scrollRef를 상위 컴포넌트(DirectMessage)에서 접근할 수 있다.

  • 스크롤 관련 작성 코드, Optimistic UI를 위한 처리 등등이 추가로 필요한데, 이는 따로 정리하는 것이 좋을 것 같다.

💎 느낀 점

  1. 좋은 라이브러리를 많이 알고 사용할 줄 아는 것.
    프론트엔드 개발자의 미덕이다.
  2. 우리가 흔히 쓰는 채팅 기능을 구현하기 위해 처리할 부분들이 한 두가지가 아니었다. 실제 프로젝트에 적용시키며 익숙해지자..
profile
앞만 보고 가

0개의 댓글