[React.js] socket.io를 이용한 실시간 채팅 시스템 구현 (1)

이정찬·2022년 8월 20일
12

React.js

목록 보기
2/5
post-thumbnail

2022년 6월부터 2022년 8월까지 진행된 심리상담 어플리케이션의 상담사/관리자가 사용할 대시보드 외주에서 React.js 프레임워크를 사용하며 새로 알게된 것들을 기록하기 위한 포스팅입니다.
AWS EC2 t2.medium instance에 Ubuntu 20.04 버전을 설치하여 작업하였습니다.

* 해당 포스트는 socket.io-client 4.5.1 버전을 기준으로 작성되었습니다.

이번 외주의 핵심 기능인 피상담자-상담사 간의 실시간 채팅 구현 경험에 대해 써보려고 합니다. 피상담자는 핸드폰 어플리케이션으로, 상담사는 웹 페이지에서 채팅을 구현합니다. 한 편의 포스팅으로는 도저히 담을 수 없는 많은 양이라 여러 장의 포스팅으로 나눠서 포스팅 해보려고 합니다.

실제 서비스 되고 있는 채팅 프로그램 (ex. 카카오톡, 라인 등)과 최대한 유사한 UI를 구현하고, 최대한 높은 UX를 제공하기 위해 노력하였으며, 백엔드와의 긴 대화와 조율 끝에 결국 잘 구현되었습니다.

0. 구현의 어려움

제가 구현했던 채팅창은 채팅방에 들어가면 채팅 리스트가 닫히고 채팅방만 랜더링 되는 구조가 아니라, 채팅 중에도 계속 왼쪽에 실시간으로 최신의 메세지를 받아오는 채팅 리스트가 랜더링되어 있는 구조였습니다. 구글링과 유튜브의 힘을 빌려 보아도 이런 구조의 채팅창은 좀처럼 찾을 수 없었습니다.

따라서, 구현이 굉장히 까다로웠습니다. 혹시 제 글을 보고 계신 누군가가 제 구조와 같은 프로그램을 구현하게 된다면, 제 글이 도움이 됐으면 좋겠습니다.

구현 결과는 제가 socket.io-client에 대하여 전부 포스팅 했을 때 마지막 포스팅에 사진으로 첨부하도록 하겠습니다.

1. 소켓 선언, 저장

먼저, 소켓 통신을 구현하기 위해 React.js의 소켓 통신을 위한 라이브러리들을 설치해줍니다.

npm install socket.io-client

클라이언트단에서 사용할 소켓의 함수들이 모두 포함되어 있는 라이브러리입니다. npm으로 설치해줍니다.

이후, 최초로 소켓 통신을 시작할 컴포넌트에서 소켓을 선언합니다.

// Chatlist.jsx
import io from 'socket.io-client';
import { useDispatch } from 'react-redux';
import { socketStorage } from '../redux/action';

...

const Chatlist = () => {
	const dispatch = useDispatch();
  	const socket = io.connect(process.env.REACT_APP_SOCKET_URL);
  	dispatch(socketStorage(socket);      
 	...
}

io.connect 함수를 이용하여 소켓을 선언합니다. 소켓 서버의 URL은 .env 파일을 이용하여 미리 빼 놓았습니다. React.js에서의 .env 파일 사용법에 대해서는 차후 포스팅 하도록 하겠습니다.

전역으로 소켓을 저장하고, 다른 컴포넌트에서도 사용하기 위해 redux를 이용하여 소켓 저장 공간을 만들었습니다.

완벽하게 돌아간다고 생각했지만, useEffect hooks를 사용하지 않았기 때문에, 매번 랜더링 될 떄마다 소켓이 새로 생성되는 문제가 발생하였습니다.

Chatlist라는 컴포넌트 내에서도 socket을 사용해야 했기 때문에, useEffect() 안에 넣어서 선언할 수 없었고, 어떻게 한다 해도 소켓이 있으면 만들고, 없으면 selector로 가져오는 과정에서, reduxuseSelectorif depth에 넣을 수 없다는 문제 때문에 저 문제는 소켓을 생성할 때 마다 특정 요청을 보내서, 이미 socket이 존재하면 지우고, 없으면 통신을 계속하는 식으로 구현하였습니다.

2. 저장된 소켓 사용 - 메세지 수신

Chatlist.jsx에서 선언되고 저장된 소켓을 다른 컴포넌트에서 사용합니다.

// Chatroom.jsx
import { useSelector } from 'react-redux';
...

const Chatroom = () => {
 	const socket = useSelector(state => state.socketStorage.socket);
  	...
}

이런식으로 redux에 저장된 소켓을 가져오면, 정상적으로 socket.emit()이나 socket.on() 같은 함수들을 사용할 수 있습니다.

소켓으로 데이터를 보낼 땐 socket.emit(socketName, data)
소켓으로부터 데이터를 받을 땐 socket.on(socketName, callback)

이제, 채팅방에 들어가고, 채팅을 받아오는 기능이 필요합니다. 함수형의 React.js에는 useEffect라는 hooks가 존재합니다. 이 useEffect 함수를 이용하여 컴포넌트가 랜더링 될 때 최초 1회만 수행하거나, 어떤 state나 변수에 변화가 생겼을 때만 실행하는 함수를 만들 수 있습니다. 이를 이용하여 채팅을 받아올 함수를 구현해줍니다.

// Chatroom.jsx
import { useState, useEffect } from 'react';
import { useLocation } from 'react-router-dom';
...

const Chatroom = () => {
 	const socket = useSelector(state => state.socketStorage.socket);
  	const { pathname } = useLocation();
  	const [arrivalChat, setArrivalChat] = useState(null); // 도착한 메세지 저장
  	const [chats, setChats] = useState([]);
  	...
    
    useEffect(() => {
    	arrivalChat && setChats((prev) => [...prev, arrivalChat]) // 채팅 리스트에 추가
    }, [arrivalChat]);
  
    useEffect(() => {
        socket.on('message-expert', (chatObj) => { // 메세지 수신
            const { result, errmsg } = chatObj;
            setArrivalChat(result);
        });
    }, [socket]) // 괄호 안의 변수에 변화가 생기면 useEffect 내부 함수 실행
  
  	...
}

chats 변수에 이미 존재하는 채팅 내역을 저장하는 부분은 생략하였습니다. socket이라는 소켓 변수에 변화가 생긴다. 즉, 서버 측에서 메세지를 보내면 useEffect부분이 자동으로 실행되며, 서버가 보낸 message-expert 라는 이름의 호출로 서버로부터 넘어오는 데이터를 받을 수 있게 됩니다.

받은 데이터는 arrivalChat이라는 컴포넌트의 state에 임시로 저장해주고, 저장이 완벽히 됐음을 확인 한 후, 채팅내역을 저장하는 state인 chats에 저장해줍니다.

3. 생겼던 문제점

메세지 수신 구현 중, 처음에는 서버로부터 크기가 1개인 배열을 받아서 concat 함수를 실행시켜서 원래 존재하는 chats 배열에 붙이는 식으로 구현하려고 했지만, javascript에서의 배열 처리가 생각처럼 잘 되지 않아서, json을 이용하여 한 개의 메세지만 받아서 따로 추가해주는 형식으로 채팅 수신을 구현하였더니 제대로 작동하였습니다.

React.js에서는 서버로부터 배열을 받아 map() 함수를 이용하여 랜더링을 할 떄에도, {사용하고자 하는 배열} && {사용하고자 하는 배열}.map(...) 처럼 구현해야 map() 함수가 존재하지 않는다는 에러를 보지 않을 수 있습니다.

이 부분에 대해서는 조금 더 공부를 해야 한다고 생각했습니다. 그러나 구글링을 해도 일단은 잘 나오지 않아서 보류해둔 상태입니다. React.js에서의 배열 처리가 느린 이유에 대해서 제대로 공부하게 된다면, 다시 포스팅을 남기겠습니다.

4. 마치며

채팅 수신은 일단 여기까지이며, 방에 들어가고, 메세지를 송신하는 부분은 다음 포스팅에서 진행하도록 하겠습니다.

사실 기능적 측면에서는 메세지 수신보다 방에 들어가는 기능이 먼저이지만, 처음부터 소켓을 공부한다는 마음으로 상대적으로 생각하기 간단한 기능부터 먼저 포스팅 하였습니다.

profile
개발자를 꿈꾸는 사람

0개의 댓글