프로젝트를 통한 배움 - (6)

응애 나 프론트애긔👶·2022년 12월 20일
0
post-thumbnail
post-custom-banner

채팅 기능 구현


교환, 나눔, 판매와 같은 B2C 서비스에서 빠질 수 없는 채팅 기능을 맡게 되었다.

이전 mockData를 가지고 메인 페이지 작업을 80% 정도를 마치게 되었다.
다른 팀원들이 작업을 끝내야 마지막 기능들을 작업 할 수 있는데 마냥 기다릴 수 만은 없기 때문에 다음 기능을 개발했다.

그 중 채팅 기능을 맡게 되었는데 채팅이라는 기능이 조금 난이도가 있어보여 다른 분들에게 경험을 여쭤보았다.

"예전에 Socket.io로 트위터 클론 코딩을 한 경험이 있어요. 자료를 드릴테니 확인 해보세요."


나 또한 알고 있는 사이트였고 해당 강의의 내용을 차근차근 살펴보았다.

Socket.io와 Firebase

강의 내용은 Node.js에서 Socket.io를 사용하여 실시간 채팅을 만드는 내용이였다.

Socket.io는 Node.js에서 Real-time communication 일명 RTC(실시간 양방향 통신)을 사용할 수 있게 해준다.

유명한 Websocket API이지만 우리의 프로젝트에서는 문제가 있었다.

바로 Node.js를 사용하지 않은 Firebase를 활용하여 실시간 채팅을 구현 해야하는 것이다.


Cloud Firestore vs Realtime Database

Firebase에는 두 개의 DB가 존재한다.

하나는 Cloud Firestore 또 다른 하나는 Realtime Database이다.

잠깐 이 둘을 비교해보도록 해보자.

Cloud Firestore

  • 문서 컬렉션으로 저장
  • 하나의 쿼리에 정렬과 필터링 모두 가능
  • 얕고 넓은 쿼리 제공
  • 데이터 세트의 크기는 쿼리 성능에 직접적인 영향이 없음
  • 하위 값은 반환하지 않기 때문에 평면적인 구조는 영향이 적음

Realtime Database

  • 데이터를 하나의 큰 json 덩어리로 저장
  • 하나의 쿼리에는 필터링/정렬 하나만 가능
  • 깊고 좁은 쿼리 제공
  • 데이터 세트의 크기가 커질수록 쿼리 성능 떨어짐
  • 최대한 데이터 평면화 필요함

그래서 나의 선택은...

간단하게나마 둘의 차이점을 비교해보았는데

채팅 기능에 사용할 데이터베이스는 바로 Realtime Database이다.

이유 첫번째 이름부터 Realtime Database이다 !!!

가장 먼저 고려 해야하는 부분은 바로 무료로 사용한다는 점이다.

Firebase는 체험판과 같이 무료로 제공해주는 데이터베이스의 크기가 정해져있다. 이후 그 크기를 벗어나게 되면 자동적으로 크기가 늘어나면서 매월 요금을 내야한다.

우리는 이미 Cloud Firestore를 상품에 대한 CRUD로 사용하는 중이다.

그렇기 때문에 조금이나마 공간을 아끼고자 Realtime Database를 선택하였다.


간단한 설계


가장 먼저 채팅 기능에 대한 간단한 플로우 차트를 그려보았다.


머릿 속에 생각나는 로직을 간단하게나마 그려보았다.

가장 중요하다고 생각이 드는 부분은 상대 유저의 UserID를 받아오는 것이라고 생각했다.
UserID를 받아와야 특정 유저와 DB를 만들 수 있을 것이며 UserID를 통해 메시지가 내가 보낸 것인지 상대방이 보낸 것인지 파악할 수 있기 때문이다.

작업 드가자

마크업 및 컴포넌트 설계

가장 먼저 간단한 채팅 페이지를 마크업하는 작업을 했다.

미리 그려놓은 와이어프레임을 바탕으로 간단하게 마크업을 하였고 input과 메시지를 나타나게 해줄 컴포넌트를 만들었다.

컴포넌트 설계

채팅을 입력할 UserChatInput
DB에서 가져올 UserChatMessage
메시지들을 감싸 놓은 UserChatMessageArea
상대방의 닉네임을 알 수 있는 UserChatNameBox


Realtime Database 서버 연결

Firebase의 Realtime Database를 연결해주어야 하기 때문에 공식문서에 나와 있는 방법으로 서버를 연결해준다.

Firebase 공식문서
https://firebase.google.com/docs/database/web/start

// firebase.js
// Import the functions you need from the SDKs you need
import firebase from 'firebase/compat/app';
import 'firebase/compat/firestore';
import 'firebase/compat/database';

import { initializeApp } from 'firebase/app';
import { getDatabase } from 'firebase/database';

// TODO: Add SDKs for Firebase products that you want to use
// https://firebase.google.com/docs/web/setup#available-libraries

// Your web app's Firebase configuration
// For Firebase JS SDK v7.20.0 and later, measurementId is optional
const firebaseConfig = {
  apiKey: process.env.REACT_APP_API_KEY,
  authDomain: process.env.REACT_APP_AUTH_DOMAIN,
  projectId: process.env.REACT_APP_PROJECT_ID,
  storageBucket: process.env.REACT_APP_STORAGE_BUCKET,
  messagingSenderId: process.env.REACT_APP_MESSAGING_SENDER_ID,
  appId: process.env.REACT_APP_APP_ID,
  measurementId: process.env.REACT_APP_MEASUREMENT_ID,
};

firebase.initializeApp(firebaseConfig);

const app = initializeApp(firebaseConfig);
const firestore = firebase.firestore();
const realtimeDatabase = getDatabase(app);

export { firestore, realtimeDatabase };

메시지를 DB에 추가

UserChatInput 컴포넌트에서 input에 메시지를 DB에 추가하는 기능을 구현하였다.


// UserChatInput.js
import React, { useState, useEffect } from 'react';
import styled from 'styled-components';

import { FiSend } from 'react-icons/fi';

import { realtimeDatabase } from '../../firebase';
import { ref, push, set, onValue } from 'firebase/database';
import { useSetRecoilState } from 'recoil';
import { getMessage } from '../../atoms/atoms';

const ChatForm = styled.form`
  display: flex;
  position: relative;
`;

const ChatInput = styled.input`
  width: 100%;
  height: 2.5rem;
  font-size: 1rem;
  border: none;
`;

const ChatEnterBtn = styled(FiSend)`
  width: 2rem;
  height: 1.7rem;
  position: absolute;
  right: 0.1rem;
  margin-top: 0.5rem;
  border: none;
  outline: none;
  background-color: white;
  font-size: 1.8rem;
  cursor: pointer;
`;

const UserChatInput = () => {
  const [chatMessage, setChatMessage] = useState('');
  const messageData = useSetRecoilState(getMessage);
  const messageRef = ref(realtimeDatabase, 'Message');

  // 보낸 메시지를 firebase의 push 문법을 사용하여 DB에 추가함.
  const sendMessage = e => {
    e.preventDefault();
    // 여기서의 push는 자바스크립트 문법이 아닌 파이어베이스 문법임.
    const newMessageRef = push(messageRef);
    set(newMessageRef, chatMessage);
    setChatMessage('');
  };
  const handleOnChange = e => {
    setChatMessage(e.target.value);
  };

  // 첫 렌더링 시 메시지들을 가져와 messageData라는 atom에 넣어줌. 
  useEffect(() => {
    onValue(ref(realtimeDatabase), snapshot => {
      const data = snapshot.val();
      const array = [];
      Object.values(data).map(el => {
        for (let key in el) {
          array.push(el[key]);
        }
        messageData(array);
      });
    });
  }, []);

  return (
    <>
      <ChatForm onSubmit={sendMessage}>
        <ChatInput onChange={handleOnChange} value={chatMessage} />
        <ChatEnterBtn onClick={sendMessage} />
      </ChatForm>
    </>
  );
};

export default UserChatInput;

여기서 파이어베이스 문법 중 snapshot을 이용한 부분이 있는데 이걸 사용해야만 실시간으로 데이터가 변경 될 때 서버에서 변경된 DB를 다시 요청하여 가져와준다.

이것이 RTC...


채팅은 위에서부터 읽지 않아

카카오톡, 라인 등 채팅 메신저를 사용할 때 우린 메시지를 가장 상단에서부터 읽지 않는다.

이러한 부분을 해결하기 위해 scrollIntoView를 사용하였다.

scrollIntoView는 스크롤을 이용할 때 사용되는 유용한 메소드이다.

최근 웹 사이트들을 보면 특정 키워드를 클릭 시 해당 위치로 스크롤이 자동으로 내려가는 기능을 볼 수 있는데
이러한 스크롤 관련 이벤트를 만들 수 있는 것이 scrollIntoView 메소드이다.

scrollIntoView은 가장 기본적으로 빈괄호 안에 아무것도 넣지 않는다면 해당 Element로 이동하게 된다.

두번째는 boolean 값을 사용하여 true이면 스크롤 최상단 false이면 스크롤 최하단으로 이동 할 수 있도록 만든다.

마지막으로 소괄호 안에 객체를 넣어 아래와 같은 효과도 줄 수 있다.

  • behavior : 애니메이션효과

  • block : 수직 정렬

  • inline : 수평 정렬

우리는 최초 렌더링 시 그리고 메시지가 입력 될 때 최하단에 위치하고 있으면 된다.

// UserChatMessage.js
import React, { useEffect, useRef } from 'react';
import styled from 'styled-components';

import { getMessage } from '../../atoms/atoms';
import { useRecoilValue } from 'recoil';

const MyChattingBox = styled.div`
  display: flex;
  justify-content: center;
  align-items: center;
  width: 8rem;
  height: auto;
  border-radius: 1rem;
  background-color: white;
  margin-left: 5rem;
  margin-top: 1rem;
  padding: 1rem 1rem 1rem 1rem;
  word-break: keep-all;
  word-wrap: normal;
`;

const TraderChattingBox = styled.div`
  display: flex;
  justify-content: center;
  align-items: center;
  width: 8rem;
  height: auto;
  border-radius: 1rem;
  background-color: white;
  margin-left: 45rem;
  margin-top: 1rem;
  padding: 1rem 1rem 1rem 1rem;
  word-break: keep-all;
  word-wrap: normal;
`;

const UserChatMessage = () => {
  const messageData = useRecoilValue(getMessage);
  const bottomRef = useRef();

  useEffect(() => {
    bottomRef.current.scrollIntoView();
  }, [messageData]);

  return (
    <>
      {messageData
        ? messageData.map((el, index) => {
            if (el) {
              return <MyChattingBox key={index}>{el}</MyChattingBox>;
            } else {
              return <TraderChattingBox key={index}>{el}</TraderChattingBox>;
            }
          })
        : null}
      <div ref={bottomRef}></div>
    </>
  );
};

export default UserChatMessage;

메시지들이 모두 렌더링 된 후 그 바로 아래 빈 div태그를 생성한다.
그리고 그 div의 DOM을 조작하기 위해 useRef를 사용하였고 useEffect를 통해 messageData가 변경 될 때마다 scrollIntoView가 동작하도록 했다.

느낀점


처음에 채팅 기능을 구현할 때 난이도가 꽤 있어보여서 겁을 먹었었다.

다행히 수 많은 블로그와 유튜브에서 채팅 기능에 대한 설명이 잘 나와있었고 파이어베이스 공식문서 또한 정말 잘 되어있어 기능을 완성시켰다.

하지만 채팅 기능은 현재 30% 정도 밖에 구현이 안되어있다.

  • 로그인 기능 이후 로그인 상태에 따른 채팅 기능 수정
  • 채팅 리스트 페이지 제작
  • 조금 더 디테일한 DB 설계 및 추가 기능 (메시지 전송 시간, 읽음 표시, 이미지 전송 등...)

그리고 아직 코드를 리팩토링하지 않아 꽤나 지저분하다.

그래서 다음 작업은 리팩토링을 먼저 할 계획인데

  1. UserChatInput.js이 아닌 atom으로 바로 데이터 받아오기
    • 기능 구현에만 초점을 맞추다보니 컴포넌트에서 데이터를 받아오고 그걸 atom에 넣는 방식으로 했는데
      이를 atom에서 바로 데이터를 넣은 상태로 리팩토링 해야한다.
  2. Firebase의 8.x 버전을 9.x버전으로 변경
    • 현재 firebase.js 파일에서 8버전과 9버전을 함께 사용하여 코드가 지저분한 상태인데
      이 또한 모두 9버전으로 변경하고 9버전에 맞는 문법으로 변경할 예정이다.

참조 사이트

Realtime Database vs Cloud Firestore
https://firebase.google.com/docs/database/rtdb-vs-firestore

MDN scrollIntoView
https://developer.mozilla.org/ko/docs/Web/API/Element/scrollIntoView

post-custom-banner

0개의 댓글