[프로젝트]물건 공유 플랫폼- 3 (구현)

else·2023년 7월 10일
0

프로젝트

목록 보기
11/12

개요

  • 백엔드 쪽에서 웹소켓을 만드는데 차질이있어 기한 내에 프로젝트를 만들기 힘들 것을 예상

  • 채팅 기능을 serverless로 빠르게 구현해야겠다고 판단

  • 실시간 채팅에 최적화된 firebase사용

firebase 프로젝트 생성

  • https://console.firebase.google.com/?hl=ko&pli=1

  • 프로젝트 시작하기

  • 프로젝트 이름, 애널리틱스 설정 등 완료

  • 내가 이번에 사용할 것은 채팅만 구현 할 것이므로 Firebase Database만 사용 할 것이다.

  • '데이터 베이스 만들기' 클릭

    • 프로덕션, 테스트 아무거나 상관없다. 용도에 맞게 선택

    • 위치는 asia-northeast3(soul) 선택

필요한 테이블

  • 전체 유저 목록 (users)

    • 회원 가입 할 때 등록
  • 유저 당 채팅 목록 (chats)

    • 마찬가지로 회원가입 할 때 빈 테이블 생성
  • 채팅방 별 채팅 로그 (userChats)

    • 채팅이 시작 될 때 등록

사전 셋팅

  • 읽기, 쓰기 권한 설정

    • allow read, write: if false;
    • falsetrue로 수정
  • firebase 내 앱 등록

    • api키 및 기타 정보들 확인
  • index 에 등록 및 초기화

    • firebase database을 쓸 것이기 때문에 db도 미리 선언

    • //index.tsx//
      import { initializeApp } from "firebase/app";
      import { getFirestore } from "firebase/firestore";
      //   https://firebase.google.com/docs/web/setup#available-libraries
      
      	// Your web app's Firebase configuration
      const firebaseConfig = {
        apiKey: "값",
        authDomain: "값",
        projectId: "값",
        storageBucket: "값",
        messagingSenderId: "값",
        appId: "값",
      };
      
       // Initialize Firebase
      export const app = initializeApp(firebaseConfig);
      export const db = getFirestore();

구현

회원 가입 시 각각의 유저 당 채팅 리스트를 만든다.

  • 여기엔 마지막으로 받은 채팅, 채팅 목록 등을 저장할 수 있다.

  • const submit = () => {
        if (
          id &&
          nickName &&
          phoneNumber &&
        ) {
          submitSignUp()
            .then((res) => {
              setDoc(doc(db, "users", id), {
                id,
                phoneNumber,
                nickName,
                profile: "",
              })
                .then(() => {
                  setDoc(doc(db, "userChats", id), {}).then(() => {
                    Alert("success", "회원가입이 완료되었습니다", () =>
                      navigate("/home")
                    );
                  });
                })
                .catch();
            })
            .catch((err) => Alert("error", err.response.data.message));
        } else {
          Alert("error", "모든 항목을 올바르게 채워 주세요");
        }
      };
  • setdoc 명령으로 db에 users 테이블을 불러와 가입한 유저의 id에 대한 테이블을 userChats테이블에 만든다

    • setdoc은 기존 문서가 있으면 덮어씌우고 없다면 새로 만든다.

채팅하기 클릭 시 채팅목록을 업데이트한다.

  • 고려해야할 점

    • 채팅방의 URL이 중복되면 안된다.

      • const chatName =
        userId > otherPerson ? userId + otherPerson : otherPerson + userId;
      • 아이디는 고유 값으로 중복될 일 없으니 내 id와 상대방의 id를 조합해서 만든다.

      • 이때 비교 연산산자를 이용해 상대방이 나를 클릭했을 때도 같은 채팅을 불러와야한다.

    • 만약 이전에 대화한 적이 있다면 그 채팅을 불러와야한다.

      • const chats = await getDoc(doc(db, "chats", chatName));
         if (!chats.exists()) {
           // 존재하지 않는다면 만들기
         }
        navigate(`/user/chat/${chatName}`, {
          state: { nickName: data?.article.accountUserId },
        });
    • 채팅 클릭시 클릭한 유저의 채팅 목록에 상대방이 추가되어야한다. 반대로 클릭 당한 상대방의 채팅목록에도 클릭한 유저가 추가되어야 한다.

      • if (!chats.exists()) {
           await setDoc(doc(db, "chats", chatName), { message: [] });
           await updateDoc(doc(db, "userChats", userId), {
             [chatName + ".userInfo"]: {
               id: otherPerson,
             },
             [chatName + ".date"]: serverTimestamp(),
           });
           await updateDoc(doc(db, "userChats", data?.article.accountUserId), {
           [chatName + ".userInfo"]: {
             id: userId,
           },
           [chatName + ".date"]: serverTimestamp(),
         });
        }
      • updatedoc은 전체 문서를 덮어씌우지 않고 일부만 수정한다.
  • 코드

    const handleChating = async () => {
      const loginObject = localStorage.getItem("loginInfo");
      const { userId } = loginObject ? JSON.parse(loginObject) : null;
      const otherPerson = data?.article.accountUserId;
      const temp_reg_user_id = "test_user_id";
      const chatName =
        userId > otherPerson ? userId + otherPerson : otherPerson + userId;
      const chats = await getDoc(doc(db, "chats", chatName));
    
      if (!chats.exists()) {
        await setDoc(doc(db, "chats", chatName), { message: [] });
        await updateDoc(doc(db, "userChats", userId), {
          [chatName + ".userInfo"]: {
            id: otherPerson,
          },
          [chatName + ".date"]: serverTimestamp(),
        });
        await updateDoc(doc(db, "userChats", data?.article.accountUserId), {
          [chatName + ".userInfo"]: {
            id: userId,
          },
          [chatName + ".date"]: serverTimestamp(),
        });
      }
    
      navigate(`/user/chat/${chatName}`, {
        state: { nickName: data?.article.accountUserId },
      });
    };

채팅

  • 실시간 업데이트

    • firebase에는 onsnapshot 이라는 매우 편리한 기능이 있다.

    • 이는 실시간으로 문서를 리슨할 수 있는 기능으로 채팅 기능에 사용하기 매우 적합하다.

    • useEffect(() => {
        const unSub = onSnapshot(doc(db, "chats", chatName), (doc: any) => {
          doc.exists() && setMessages(doc.data().messages);
        });
        return () => {
          unSub();
        };
      }, []);
    • 컴포넌트가 언마운트될 때 unSub()를 해 리슨을 종료시켜줘야한다.

    • 메세지가 보내지면 두 유저의 마지막 메세지 상태를 변경시켜주고, 두 유저의 채팅 목록을 업데이트 해줘야한다.

    •  const send = async () => {
        await updateDoc(doc(db, "chats", chatName), {
          messages: arrayUnion({
            id: uuid(),
            msg,
            senderId: userId,
            date: Timestamp.now(),
          }),
        });
      
        setMsg("");
      
        await updateDoc(doc(db, "userChats", userId), {
          [chatName + ".lastMessage"]: {
            msg,
          },
          [chatName + ".date"]: serverTimestamp(),
        });
      
        await updateDoc(doc(db, "userChats", state.nickName), {
          [chatName + ".lastMessage"]: {
            msg,
          },
          [chatName + ".date"]: serverTimestamp(),
        });
      };
  • 채팅 내역 표시

    • 위에서 onSnapShot으로 리슨중인 메세지 들을 불러와 useState에 담아둔다.
    • useEffect(() => {
         const unSub = onSnapshot(doc(db, "chats", chatName), (doc: any) => {
           doc.exists() && setMessages(doc.data().messages);
         });
         return () => {
           unSub();
         };
       }, []);
  • 이를 새로운 컴포넌트에서 순회하며 나타낸다.

/** @jsxImportSource @emotion/react */
import { css } from "@emotion/react";
import { useEffect, useRef } from "react";
import profile from "../../../assets/testObject.jpg";

interface PropType {
  data: any;
  sender: string;
}

const container = css`
  width: 100%;
  height: 82vh;
  overflow: scroll;

  .senderMsg,
  .receiverMsg {
    height: auto;
    width: auto;
    display: flex;
    align-items: center;
    margin: 10px 0;
  }
  .senderMsg {
    height: 7vh;
    width: 100%;
    display: flex;
    justify-content: end;
  }

  .imgBox {
    width: 6vh;
    height: 6vh;
    border-radius: 70%;
    overflow: hidden;
    border: 1px solid rgba(0, 0, 0, 0.5);
    margin: 0 10px;
    .profile {
      width: 100%;
      height: 100%;
      object-fit: cover;
    }
  }
`;
const receiverContent = (len: number) => css`
  width: ${len * 18 < 250 ? len * 18 : 250}px;
  height: ${len * 18 < 240 ? 4 : 4 + ((len * 18) / 250) * 1.2}vh;
  background-color: #fcc8d1;
  color: white;
  display: flex;
  align-items: center;
  border-radius: 20px;
  padding: 0 10px;
  flex-wrap: wrap;
  word-break: break-all;
`;
const senderContent = (len: number) => css`
  width: ${len * 18 < 250 ? len * 18 : 250}px;
  height: ${len * 18 < 240 ? 4 : 4 + ((len * 18) / 250) * 1.2}vh;
  background-color: #ffabab;
  color: white;
  display: flex;
  justify-content: end;
  align-items: center;
  border-radius: 20px;
  padding: 0 10px;
  flex-wrap: wrap;
  word-break: break-all;
  margin-right: 10px;
`;

export default function MessageBox({ data }: any) {
  function getStringByte(str: string): number {
    let byte = 0;
    for (let i = 0; i < str.length; i++) {
      byte += str.charCodeAt(i) > 127 ? 3 : 1.4;
    }
    return byte / 3;
  }
  const loginObject = localStorage.getItem("loginInfo")!;
  const { userId } = JSON.parse(loginObject);

  const ref = useRef<any>();
  useEffect(() => {
    ref.current.scrollIntoView({ behavior: "smooth" });
  }, [data]);
  return (
    <div css={container}>
      {data?.map((ele: any, idx: number) =>
        ele?.senderId === userId ? (
          <div key={idx} className="senderMsg">
            <div css={senderContent(getStringByte(ele?.msg))}>
              <div>{ele?.msg}</div>
            </div>
          </div>
        ) : (
          <div className="receiverMsg">
            <div className="imgBox">
              <img src={profile} alt="profile" className="profile" />
            </div>
            <div css={receiverContent(getStringByte(ele?.msg))}>
              <div>{ele?.msg}</div>
            </div>
          </div>
        )
      )}
      <div ref={ref} />
    </div>
  );
}
  • 위는 텍스트의 길이에 따라 메세지 박스의 크기가 동적으로 변하게 만들었다.
profile
피아노 -> 개발자

0개의 댓글