Firestore로 채팅 만들기

MM·2023년 1월 10일

make it worth

목록 보기
4/10
post-thumbnail

소켓과 서버 없이 DB로만 채팅 만들기

🤔 파이어스토어와 실시간 데이터베이스의 차이점이 뭐야?

대표적으로는 사용하는 쿼리가 다르다는 것!
그리고 사용 용도에 따라 조금 더 유리한 쪽이 있고 더 불리한 쪽이 있다고 한다.
사실 이런 채팅에 더 유리한 것은 실시간 데이터베이스라서 실시간 데이터베이스를 먼저 시도해봤지만.... 안 되는데 어쩌겠어. 되는 거 해야지.

firebaseConfig 만들기

파이어스토어 프로젝트 설정 하단에 가면 주는 내용이다.
env파일에 키를 정의해 두고 process.env로 가져와서 사용하자.

😤 참고로 말입니다

nextJS에서 env파일의 값을 사용하려면 앞에 꼭 NEXT PUBLIC 을 붙여줘야 한다!

const firebaseConfig = {
  apiKey: `${process.env.NEXT_PUBLIC_FIREBASE_APIKEY}`,
  authDomain: `${process.env.NEXT_PUBLIC_FIREBASE_AUTHDOMAIN}`,
  databaseURL: `${process.env.NEXT_PUBLIC_FIREABASE_DATABASEURL}`,
  projectId: `${process.env.NEXT_PUBLIC_FIREBASE_PROJECTID}`,
  storageBucket: `${process.env.NEXT_PUBLIC_FIREABASE_STORAGEBUCKET}`,
  messagingSenderId: `${process.env.NEXT_PUBLIC_FIREBASE_MESSAGINGSENDERID}`,
  appId: `${process.env.NEXT_PUBLIC_FIREBASE_APIID}`,
};

//먼저 설정되어 있는 파이어베이스가 있는지 확인한 후 APP정의.
const app = getApps().length ? getApp() : initializeApp(firebaseConfig);

export { app };

😡 config에서 app만 선언해 주세요!

다른 사람들이 쓴 이전 버전 파이어스토어에서는 getFirestore(app)까지 정의해준 후 해당 getFirestore를 export했는데, 버전 업데이트 이후에는 이렇게 하면 정의된 app을 못 찾아서 에러가 발생한다!
app만 export 해주고 getFirestore는 사용 장소에서 정의해 주자!
이유를 몰라서 하루종일 삽질했던 부분. 에러를 잘 읽자..



스냅샷을 써 봅시다

😎 스냅샷이란?

사용자의 콜백이 최초로 호출될 때 단일 문서의 현재 콘텐츠로 문서 스냅샷이 생성된다.
이 스냅샷 서버는 리스너와 관련된 변경 사항을 능동적으로 모니터링한다!
모니터링하던 콘텐츠가 변경될 때마다 콜백이 호출되어 문서 스냅샷을 업데이트한 후, 해당 업데이트를 로컬 데이터와 병합한다.
👉 그러니까, DB계의 useQuery같은 녀석이라는 것!

기본적인 원리는 소켓 채팅과 비슷하다! 받고 뿌리고 받고 뿌리고..

  const firestore = getFirestore(app);
  const chatRef = useRef<HTMLInputElement>(null);
  const scrollMobileRef = useRef<HTMLDivElement>(null);
  const profile = useRecoilValue(myProfileAtom);
  const [chatHistory, setChatHistory] = useState<ChatType[]>(chats);
  const [loading, setLoading] = useState<boolean>(false);

//반복해서 사용할 필요 없이, 처음에 한 번만 정의해 주면 알아서 작동한다!
  const Snapshot = onSnapshot(
    query(
      collection(firestore, `lunchbus/${id}/messages`),
      orderBy("order", "desc")
    ),
    snapshot => {
      //ui를 업데이트하기 위해 state에 넣어줬다.
        setChatHistory(makeData(snapshot));
    },
  );

//이건 항상 채팅방 맨 아래 스크롤을 유지하도록 하기 위한 덤.
  useEffect(() => {
    scrollMobileRef.current.scrollTop = scrollMobileRef.current.scrollHeight;
  }, [chatHistory]);

  const chatSubmit = async () => {
    if (chatRef.current.value == "") return;
    setLoading(true);
    //useQuery가 아니라서 loading을 수동으로 적용해야 함...
    const sendMessage = {
      name: profile.name,
      text: chatRef.current.value,
      order:
        chatHistory.length == 0
          ? 0
          : chatHistory[chatHistory.length - 1].order + 1,
    };
    //firestore의 모든 함수는 비동기!
    await addDoc(collection(firestore, `lunchbus/${id}/messages`), sendMessage);

    chatRef.current.value = "";
    setLoading(false);
  };

  const keyPress = async e => {
    //참고로, keyDown이나 keyUp은 한글 입력시 중복 submit되는 오류가 있다!
    if (e.key === "Enter") await chatSubmit();
  };

장점!

  • 낮은 러닝커브와 빠른 작업
    • 웹소켓보다 훨씬 단순하다! 별도의 라이브러리를 필요로 하지 않는다.
    • DB 쿼리만 조금 작성할 줄 알면 굉장히 쉽게 터득할 수 있다.
  • 채팅방을 일일히 생성할 필요가 없다.
    • 채팅방 삭제 및 추가가 자주 이루어지는 채팅에 효과적!
  • snapshot만으로 빠른 실시간 통신 가능
    • useQuery & useMutation 보다 훨씬 속도가 빠르다! 모르고 사용하면 DB인줄 모를 듯..

단점..

  • DB가 쌓이면 쌓일수록 채팅이 느려진다..

    • 인덱스가 없는 firestore 특성상 인덱스 속성을 추가하여 정렬 후 값을 가져와야 하는데, 채팅이 많을수록 해당 작업이 느려진다.
  • 소켓과 달리 약간의 딜레이가 존재한다.

    • 버퍼링이 거의 없는 소켓과 달리.. DB통신이니 딜레이가 조금 존재한다. 평범한 채팅에 방해될 정도는 아니지만 연속 입력 등에서 잔잔바리 딜레이가 발생함!
  • 그만 버전 업데이트를 해!!

    • 버전이 업데이트되면 이 코드도 레거시가 되겠지...
    • 공식 문서가 그닥 친절한 편이 아니다. 에러가 있으면 에러 문구를 읽으며 스스로 머리를 쥐어뜯어야 함.

그럼에도 불구하고,

웹소켓이 아니라 파이어스토어를 선택한 이유는, 현재 프로젝트 상황에서 파이어스토어의 장점이 더 유리하게 작용할 것이라고 생각했기 때문.
채팅을 작용하고자 하는 런치버스는 채팅방 생성과 삭제가 빈번하여 데이터가 많이 쌓이지 않는 가벼운 채팅을 필요로 했고, 프로젝트 스케줄과 우선순위상 채팅에 많은 에너지와 시간을 쏟을 수 없는 상황이었기 때문에 파이어스토어를 선택했다.

만약 해당 프로젝트에서 채팅이 큰 비중을 차지하거나 통신이 오래 지속되는 채팅, 0.1초 단위의 버퍼링도 허용하지 않는 리얼타임 채팅을 필요로 했다면 웹소켓을 채용했을 것이다.

profile
중요한 건 꺾여도 그냥 하는 마음

0개의 댓글