채팅방 목록 띄우기

사용자가 속한 채팅방 목록을 조회하고, 최신순으로 정렬하는 것을 구현했다.

Firebase의 onValue를 두 번 사용해야해서 이번에도 함수형 컴포넌트를 클래스형 컴포넌트로 변경하는 과정이 필요했다.

 

첫번째 onValue는 사용자가 속한 채팅방 번호를 모두 불러와서 상태에 저장하고, 두번째 onValue를 호출한다.

두번째 onValue는 각 채팅방의 가장 마지막 메세지를 구해서 최신순으로 정렬하고 상태에 저장하는 것이다.

라이프 사이클 함수인 componentDidMount 안에서 첫번째 onValue를 호출하도록 했다.

 

채팅방 번호와 각 채팅방의 마지막 메세지를 모두 state로 관리하므로

새로운 채팅방이 생겼거나, 기존 채팅방에서 새로운 메세지가 온다면 re-rendering 하는 것이다.

 

전체 코드는 여기 👇

 

GitHub - askges20/messenger-web-react: React 개인 프로젝트 웹 메신저 만들기

React 개인 프로젝트 웹 메신저 만들기. Contribute to askges20/messenger-web-react development by creating an account on GitHub.

github.com

 

크롬과 엣지에서 서로 다른 계정으로 접속해서 테스트를 해봤는데

아직 계정 2개까지만 테스트를 해봐서 그 이상은 오류가 발생할 수도 있다.

나중에 테스트해보고 오류가 있으면 수정할 예정!

 


 

채팅방 최신순 정렬

리액트로 구현한 채팅 시스템 코드를 참고하려고 구글링하다가 채팅방 최신순 정렬 코드를 발견했다.

그리고 약간 수정해서 적용했더니 다행히 잘 작동된다.

 

lastMessageFromFB.sort((a, b) => { return sortChatRoom(a, b)})

 

sortChatRoom 함수

export const sortChatRoom = (a, b) => {
  if (a && b) {
    const aDateTime = a.dateTime;
    const bDateTime = b.dateTime;

    if (aDateTime.slice(0, 8) === bDateTime.slice(0, 8)) {  //날짜
      if (aDateTime.slice(8, 10) === bDateTime.slice(8, 10)) {  //시
        if (aDateTime.slice(11, 13) === bDateTime.slice(11, 13)) {  //분
          return bDateTime.slice(14, 16) - aDateTime.slice(14, 16); //초
        }
        return bDateTime.slice(11, 13) - aDateTime.slice(11, 13);
      }
      return bDateTime.slice(8, 10) - aDateTime.slice(8, 10);
    }
    return bDateTime.slice(0, 8) - aDateTime.slice(0, 8);
  }
}

 

나는 전송 날짜, 시간을 하나의 문자열로 이어붙여서 계속 slice를 해주었는데

형식을 보자면 중첩 if문을 작성해서 연도부터 월, 일, 시, 분, 초 순서대로 비교해주면 된다.

 

자바스크립트에서 sort를 이용할 때 그 기준을 지정하는 방법을 알게 되었다.

 


 

디자인 개선

기능 개발만 계속 하다가 오랜만에 웹사이트 디자인을 개선해보았다.

일단 전체적인 색감은 핑크 -> 블루로 맞췄고, 리액트 Material UI의 TextField와 LinearProgress를 사용해보았다.

 

TextField 공식 문서 : https://mui.com/components/text-fields/

 

Text Field React component - MUI

Text fields let users enter and edit text.

mui.com

LinearProgress 공식 문서 : https://mui.com/components/progress/

 

Circular, Linear progress React components - MUI

Progress indicators commonly known as spinners, express an unspecified wait time or display the length of a process.

mui.com

 

 

현재 폼 양식이 쓰이는 곳은 회원가입, 로그인 화면이 있어서 TextField를 적용해서 디자인을 개선했다.

같은 TextField 컴포넌트이지만 variant 속성을 다르게 해서 다른 디자인의 input을 만들 수 있었다.

 

폼은 Formik 라이브러리를 사용해서 에러 메세지를 출력했는데, 이것을 커스텀하기 위해 ErrorMessage 컴포넌트를 이용해서 글씨를 파란색으로 바꿨다.

공식 문서 : https://formik.org/docs/api/errormessage

 

| Formik

Copyright © 2020 Formium, Inc. All rights reserved.

formik.org

 

 

 

친구 검색 화면은 배경색과 버튼색만 바꿨다.

 

 

 

그리고 오늘 구현한 채팅 목록 화면~!

테스트 결과 실시간으로 채팅이 전송할/전송받을 때마다 최신순으로 정렬되는 것을 확인했다.

아직 안읽은 메세지 개수 구하는건 구현하지 않아서 일단 레이아웃만 잡고 숫자 2가 뜨도록 했다ㅋㅋ

 

 

 

채팅방 목록을 클릭해서 해당 채팅방으로 이동할 수 있다.

아직 참여자 이름 대신 아이디가 뜨도록 되어있는데 나중에 고치려고 한다.

 


로딩 화면 만들기

Material UI의 LinearProgress은 로딩 화면을 만들 때 사용했다.

App.js에서 리액트의 라우팅 처리로 사용자 정보가 없을 땐 (로그인하지 않았을 때) 웰컴 화면이, 사용자 정보를 불러왔을 땐 (로그인했을 때) 메인 화면이 뜨도록 코드를 작성했었다.

 

그런데 로그인한 사용자 정보를 redux로 저장해놓고 불러오다보니 로그인한 상태여도 웰컴 화면이 잠깐 떴다가 메인 화면이 뜨는 경우가 자주 발생했다.

따라서 redux에 저장된 사용자 정보를 조회하기 전에는 로딩 화면이, 그 이후엔 웰컴 or 메인 화면이 뜨도록 수정했다.

 


 

채팅방 목록 구현에 성공해서 일단 큰 고비(?)는 넘겼고

이제 프로필 수정 기능을 구현할지, 안읽은 메세지 개수 구하는 것을 구현할지 고민해봐야겠다.

현재 채팅이 자동 스크롤되도록 구현된 상태인데

이것은 Firebase Realtime Database의 onValue 메소드를 이용해서

새로운 채팅이 등록될 때마다 스크롤이 되는 것이었다.

 

평소에 카톡을 이용해보면

1. 채팅방에 입장했을 때

2. 내가 채팅을 전송했을 때만

자동 스크롤이 되고, 다른 사람으로부터 채팅을 전송받았을 때는 스크롤이 되지 않는다.

그래서 이것을 구현하기로 했다.

 

채팅방에 입장했을 때 채팅 기록을 state로 관리하고 있다.

따라서 가장 마지막으로 저장된 채팅의 senderId를 현재 로그인한 사용자의 Id와 비교해서 같을 때만 자동 스크롤이 되도록 수정했다.

 

 


 

그리고 현재는 사이드바에 있는 친구 목록을 클릭해서 해당 친구와 1:1 채팅방으로 이동할 수 있는 상태이다.

일반적인 채팅 앱에서는 친구 목록과 별개로 채팅방 목록을 보여주므로 이것을 구현하기 위해 DB 구조를 추가했다.

 

1. 사용자마다 자신이 속한 채팅방 번호를 가지도록 추가

루트

ㄴMyChatRooms

  ㄴ아이디

    ㄴ채팅방 번호

      ㄴisMember: true

 

로그인한 사용자가 속한 채팅방을 빠르게 구하기 위해 추가한 것이다.

 

2. 채팅방마다 마지막으로 전송된 메세지를 따로 가지도록 추가

루트

ㄴChatRooms

  ㄴ채팅방 번호

  ㄴChatMembers

  ㄴChatMessages

  ㄴLastMessage

    ㄴcontent: 채팅 내용

    ㄴdateTime: 전송 시간 (yyyyMMddhh:mm:ss)

    ㄴsenderId: 아이디

 

마지막 메세지를 알아야하는 이유는 채팅방 목록을 최신순으로 정렬해야하기 때문이다.

기존의 채팅 내역은 ChatRooms - ChatMessages 하위에 저장되어 있는데

채팅방 목록을 불러올 때 사용자가 속한 모든 채팅방의 채팅 내역을 읽는 것은 비효율적이라고 생각했다.

따라서 ChatRooms - LastMessage 를 따로 추가해준 것이다.

채팅 내역 불러오기 및 실시간 업데이트

채팅 내역 Realtime Database에서 불러오고 실시간 업데이트를 하도록 구현 완료했다!

Realtime DB의 onValue 메소드를 이용하면 쉽게 구현할 수 있었다.

 

export function getChatHistory(chatRoomNum) {
    return ref(database, '/ChatRooms/' + chatRoomNum + '/ChatMessages/');
}

ref를 이용해서 DB 경로를 지정하고

 

import { onValue } from '@firebase/database';

const ChatRoom = (props) => {
	...
    const chatHistoryRef = getChatHistory(chatRoomNum);

    useEffect(() => {
        onValue(chatHistoryRef, (snapshot) => {
            let chatFromFB = []
            snapshot.forEach((chatDate) => {
                chatDate.forEach((chat) => {
                    const content = chat.val().content;
                    const senderId = chat.val().senderId;
                    const time = chat.val().time;
                    chatFromFB.push({content, senderId, time});
                })
            })
            setChatHistory(chatFromFB);	//상태 업데이트 -> 리렌더링
        });
 	...

채팅 내역을 불러올 js의 useEffect에서 onValue 메소드를 작성해주면 지속적으로 업데이트가 된다.

Firestore을 이용해서 전체 조회를 할 수 있도록 그렇게 고민을 했었는데 이렇게 쉽게 만들 수 있는거였다니...🥺

 

forEach로 돌릴 수 있게 되어서 각 날짜 별 채팅 가장 상단에 날짜를 표시하면 좋겠다 싶어서 ChatDateLine.js를 새로 만들고

 

{
    this.state.chatHistory.map((value, i) => {
        if (!value.senderId){
            return (
                <ChatDateLine key={i} year={value.year} month={value.month} date={value.date}/>
            )
        } else if (value.senderId == this.props.loginId){
            return (
                <SendChatMessage key={i} content={value.content} time={value.time}/>
            );
        } else {
            return (
                <ReceiveChatMessage key={i} content={value.content} time={value.time} friendName={this.friendName}/>
            );
        }
    })
}

JSX 안에서 조건부 렌더링을 하는데

수신자 아이디가 없으면 날짜를, 아이디가 현재 로그인한 아이디와 같으면 내가 보낸 메세지로,

아이디가 다르면 받은 메세지를 출력하도록 한 것이다.

map을 할 때는 유니크한 key를 지정해주는 것을 잊지 말자!

 


 

채팅 자동 스크롤 & react-custom-scrollbars을 이용한 커스텀 스크롤

일반적으로 채팅방에 입장했을 때, 내가 채팅을 전송했을 때 채팅의 가장 아래 부분으로 자동 스크롤이 된다.

이것을 구현하기 위해 여러 방법을 시도해보았는데

useEffect 안에서 상태 업데이트 후 스크롤 코드를 작성했더니 마지막 메세지 바로 위까지만 스크롤이 되는 것이다.

 

이것은 상태가 바뀌기 전의 렌더링 상태에서 스크롤 높이 계산을 하기 때문이었고

결국 클래스형 컴포넌트의 ComponentDidUpdate 라이프 사이클 함수를 사용하기 위해

함수형 컴포넌트를 클래스형 컴포넌트로 변경하는 수정 작업을 해야했다.

 

클래스형 컴포넌트로 변경하면서

1. useSelector 대신 connect 함수와 mapStateToProps 이용

2. useRef 대신 createRef 이용

3. 상태를 변경하기 위해 useState 대신 setState 이용

4. constructor 작성 등

수정할 부분이 은근히 많아서 시간이 꽤 걸렸다.

 

 

수정을 완료한 후에는 HTML의 기본 스크롤이 너무 투박해보여서 react-custom-scrollbar 라이브러리를 적용했다.

 

https://github.com/malte-wessel/react-custom-scrollbars

 

GitHub - malte-wessel/react-custom-scrollbars: React scrollbars component

React scrollbars component. Contribute to malte-wessel/react-custom-scrollbars development by creating an account on GitHub.

github.com

 

스크롤 모양도 훨씬 나아졌고 autoHide 속성을 이용해서 일정 시간이 지나면 자동으로 스크롤을 숨길 수 있었다.

자동 스크롤은 scrollToBottom() 메소드를 이용해서 구현할 수 있었다.

 

 

이 라이브러리를 이용하다보니 자동으로 가로 스크롤도 생성이 되길래 이것을 없애는 방법을 찾아보았고

 

https://github.com/malte-wessel/react-custom-scrollbars/issues/213

 

How to disable the horizontal scrollbar? · Issue #213 · malte-wessel/react-custom-scrollbars

 

github.com

해당 issue에 나온 첫번째 답변대로 renderTrackHorizontal 속성 안의 style에서 display: 'none'을 지정하여 해결했다.

다른 답변처럼 overflowX: 'hidden'도 작성해보았는데 이것은 작동하지 않았다.

 


기타 수정 사항

1. 채팅방 입장 시 input 박스에 focus 주기

2. 엔터키 입력 시 채팅 전송하기

3. input 박스가 전송 버튼에 가려지지 않도록 수정

 

개선해야할 사항

채팅방 팝업 띄우는 부분을 window.open으로 작성했는데,

창 크기를 고정하기 위해 resizeable=no 옵션을 추가했지만 적용되지 않길래 찾아봤더니

 

https://stackoverflow.com/questions/15480252/make-window-not-resizable-in-chrome/15481333

 

Make window not resizable in Chrome

I have some JavaScript that I use to make a window not resizable, something along the lines of: window.open(URL, id, "resizable=no"); This works in most browsers but it appears to not be supporte...

stackoverflow.com

이것은 IE에서만 작동하는 옵션이고, 최신 브라우저는 지원하지 않는다고 한다.

window.open 자체가 old school JS라며 다른 방법을 이용할 것을 권장하는 것으로 보인다.

새로운 창을 띄우는 라이브러리들도 존재하는 것 같은데 다음에 자세히 찾아봐야겠다.

 


 

크롬과 엣지에서 다른 계정으로 접속하고 채팅을 보낸 것이다. 제대로 작동한다~

채팅 기능이 완성되어서 친구들에게 배포할 수 있는 상태가 되긴 했으나

아직 레이아웃 보완 또는 세부적인 기능 구현할게 많이 남아있어서 천천히 배포할 생각이다ㅎㅎ

오늘 열심히 개발해서 뿌듯

Firebase Realtime Database를 import할 때 오류가 발생해서 막혀있었는데

최근에 나온 9 버전을 이용해보면 괜찮을까 싶어서 새로 9 버전을 설치했다.

 

Firestore 관련 코드는 이미 8 버전으로 작성을 했기 때문에 건드리지 않고

Realtime DB만 9 버전 코드를 작성했는데 다행히도? 어째서인지? import 오류가 더이상 발생하지 않았다.

 

import firebase from 'firebase/compat/app';
import 'firebase/compat/firestore';
import 'firebase/compat/auth';
import { getDatabase } from 'firebase/database';

const firebaseConfig = {
	//config
};

firebase.initializeApp(firebaseConfig);
const firestore = firebase.firestore();  //firestore DB 연결
const auth = firebase.auth;
const database = getDatabase();

export {firestore, auth, database};

 

9 버전으로 업데이트를 해도 compat 폴더를 import하면 이전 버전을 사용할 수 있다.

Realtime DB 공식 문서에 9 버전 코드가 업데이트 되어있어서 해당 내용을 참고하여 작성했다.

 

https://firebase.google.com/docs/database/web/read-and-write#web-v9_1

 

웹에서 데이터 읽기 및 쓰기  |  Firebase 실시간 데이터베이스

의견 보내기 웹에서 데이터 읽기 및 쓰기 (선택사항) Firebase 로컬 에뮬레이터 도구 모음으로 프로토타입 제작 및 테스트 앱에서 실시간 데이터베이스의 데이터를 읽고 쓰는 방법에 대해 논의하

firebase.google.com

 

import 에러 해결 이후엔 DB와의 연결은 순조로웠다.

오늘은 1. 친구 추가 시 DB에 채팅방 멤버 등록하기, 2. 채팅 전송 시 DB에 채팅 기록 저장하기

이렇게 2개의 기능을 구현해서 DB에 데이터 등록이 제대로 되는지 확인해보았다.

 

채팅방과 관련된 DB 구조는 크게 ChatMembers와 ChatMessages로 나눌 수 있다.

 

ChatRooms

ㄴ채팅방 번호

  ㄴChatMembers (채팅방 멤버)

    ㄴ멤버 아이디

      ㄴemail : 멤버 이메일

      ㄴisMember : true

  ㄴChatMessages (채팅 메세지)

    ㄴ채팅 날짜 (yyyyMMdd)

      ㄴ채팅 번호

        ㄴcontent : 내용

        ㄴsenderId : 보낸 사람 아이디

        ㄴtime : 보낸 시간

 

아직 1:1 채팅만 가능한 상태이긴 하지만 추후 단톡방 기능을 추가하여 확장하는 것을 고려해서 ChatMembers를 만들었다.

 

DB에 데이터를 저장하는 코드 :

import { database } from '../services/firebase';
import { ref, set } from 'firebase/database';

export function addChatMember(chatRoomNum, id, email) {
    set(ref(database, '/ChatRooms/' + chatRoomNum + '/ChatMembers/' + id + '/'), {
        email: email,
        isMember: true
    });
} 

export function addChatMessage(chatRoomNum, date, messageCode, content, id, sendTime) {
    set(ref(database, '/ChatRooms/' + chatRoomNum + '/ChatMessages/' + date + '/' + messageCode + '/'), {
        content: content,
        senderId: id,
        time: sendTime
    });
 }

 

이제 DB에 저장된 데이터를 불러오는 기능을 구현해야 한다.

오늘은 웹 메신저의 메인 기능인 채팅 조회 및 실시간 업데이트 코드를 작성했다.

Firestore에 채팅 내역을 저장해놓았고, DB에서 불러온 채팅 내역을 상태로 관리하여

onSnapshot 메소드를 이용해서 조회할 때, 채팅이 추가되었을 때 상태를 변경해서 즉시 반영하는 것이다.

 

 

크롬과 엣지를 이용해서 혼자 채팅 테스트를 해보니 채팅 조회와 실시간 반영이 잘 되는 것을 확인했다.

 

그러나... 아주 큰 문제가 있는데 현재 코드는 다음과 같다.

 

chatMessageFB.doc('20210909').collection('userMessage').onSnapshot((docs) => {
    let chatFromFB = [];
    console.log(docs)
    docs.forEach((doc) => {
        chatFromFB.push({
            senderId: doc.data().senderId,
            content: doc.data().content,
            time: doc.data().time
        })
    })
    setChatHistory(chatFromFB);
});

 

document의 id (날짜) 를 지정해서 불러오는 상태인 것이다.

내가 원하는 것은 컬렉션 아래에 있는 모든 document 목록을 forEach로 돌려서

전체 채팅 내역이 뜨게 하는 것인데 나름대로 코드를 작성해보았지만 작동을 하지 않았다.

 

몇 시간을 삽질하고 뭔가 잘못되었음을 느끼고 결국 스파르타 리액트 튜터님께 슬랙에 질문을 남겼는데

Firestore에서는 내가 생각했던 기능을 구현할 수 없다는게 결론이었다...

 

사실 실시간 업데이트 기능때문에 Firestore 말고 Realtime Database를 이용하는 것을 진작에 고려해보았는데

어쩐 일인지 import 'firebase/database';를 작성하면 오류가 발생해서 최대한 Firestore을 이용하는 방향으로 잡았었다.

하지만 이제는 Realtime Database를 사용해야만 하는 상황이 되었기 때문에 어떻게든 그 오류를 해결해보려고 한다.

 

부디 빠른 시일 내에 오류를 해결할 수 있길 바란다🙏

1. 1:1 채팅방 번호 생성

추후에 자신이 속한 채팅방 목록을 쉽게 조회하기 위해서, 그리고 1:1뿐만 아니라 단체톡방 기능 갖도록 확장할 것을 고려하여

채팅방마다 고유의 번호를 가지고 있는 것이 편리할 것이라는 판단을 했다.

따라서 친구 등록 시간과 자신의 아이디를 조합하여 채팅방 번호를 만들었다.

(ex. 2021년 9월 5일 오전 1시 33분 4초에 친구 등록 -> 20210905013304아이디 번호를 가지는 채팅방 생성)

 

자바스크립트에서 날짜를 다루는 함수를 찾아보았는데 사용자 임의로 포맷을 만들어서

그 형식에 맞게 날짜를 리턴하는 함수는 따로 없는 것으로 보인다.

자바는 format 함수가 있어서 편리한데 자바스크립트에는 없어서 의외다.

 

그래서 처음에는 이렇게 코드를 작성했다가

 

    //현재 날짜와 시간을 이용해서 채팅방 번호 생성
    function makeChatRoomNum() {
        const today = new Date();   //현재 날짜
        const year = today.getFullYear();
        const month = ('0' + (today.getMonth() + 1)).slice(-2);
        const day = ('0' + today.getDate()).slice(-2);
        const hours = ('0' + today.getHours()).slice(-2);
        const minutes = ('0' + today.getMinutes()).slice(-2);
        const seconds = ('0' + today.getSeconds()).slice(-2);
        
        return year + month + day + hours + minutes + seconds + loginUserId;
    }

 

좀 더 찾아보니 moment라는 라이브러리가 자바의 format과 같은 기능을 제공한다고 하여 적용해보았다.

 

import moment from 'moment';

	//현재 날짜와 시간을 이용해서 채팅방 번호 생성
    function makeChatRoomNum() {
		return moment().format('YYYYMMDDHHmmss') + loginUserId;
    }

 

8줄의 코드를 단 한줄로 줄일 수 있었다...!

이 외에도 날짜 더하기, 빼기 등 유용한 함수를 많이 제공하는 것으로 보인다.

라이브러리를 잘 활용하는 것이 개발 생산성 향상에 매우 도움된다는 것을 다시 한번 체감한다.

 

 

2. 채팅방 멤버 DB 저장

친구 추가 시 자동으로 만들어지는 1:1 채팅방에

현재 로그인한 사용자와 친구로 등록된 사용자를 멤버로 등록한다.

 

    //친구 추가
    const addFriend = () => {
        let popup = window.confirm('친구로 추가하시겠습니까?');
        if (popup) {    //'예'를 선택했을 때
            console.log('친구 추가하기');
            const chatRoomNum = makeChatRoomNum();

            //친구 등록 완료
            firestore.collection('users').doc(loginUserEmail).collection('friends').doc(friendEmail).set({id: friendId, name: friendName, chatRoomNum: chatRoomNum})
                .then(() => {
                    alert('친구로 등록되었습니다.')}
                );

            //채팅방 멤버로 등록
            firestore.collection('chatRooms').doc(chatRoomNum).collection('members').doc(loginUserEmail).set({isMember: true});
            firestore.collection('chatRooms').doc(chatRoomNum).collection('members').doc(friendEmail).set({isMember: true});
        }
    }

 

chatRooms (컬렉션)

ㄴ채팅방 번호 (문서)

  ㄴmembers (컬렉션)

    ㄴ로그인한 사용자 이메일 (문서)

       ㄴisMember: true (필드)

    ㄴ친구로 등록된 사용자 이메일 (문서)

       ㄴisMember: true (필드)

 

친구 등록이 완료되면 DB에 위처럼 등록이 된다.

 

 

3. 전송한 채팅 메세지 DB 저장

드디어 채팅 기능 구현을 시작했다! 가장 쉬운 채팅 전송 기능부터 만들어보았다.

채팅방에서 메세지를 입력하고 전송 버튼을 클릭하면 DB에 저장된다.

 

const content = React.useRef(); //채팅 input 박스

    function sendMessage() {
        const value = content.current.value;

        if (value.length == 0) {
            alert('채팅 내용을 입력해주세요');
        } else {
            const date = getDate(); //채팅 내역 document에 사용
            const time = getTime(); //채팅 내역 메세지 field에 사용
            const messageCode = time + loginId;

            firestore.collection('chatRooms').doc(chatRoomNum).collection('chatMessages')
            .doc(date).collection('userMessage').doc(messageCode).set({content: value, time: time.slice(0, -3)}).then(
                //전송 완료 (DB 등록)
            );

            content.current.value = ''; //채팅 input 박스 비우기
        }
    }

 

input 박스 ref에 React.useRef로 만든 ref를 등록해서

전송 버튼 클릭 시 해당 input 안에 입력된 값을 가져오고

내용이 입력되었으면 DB에 저장, 만약 입력되지 않았으면 alert 메세지를 띄운다.

 

chatRooms (컬렉션)

ㄴ채팅방 번호 (문서)

  ㄴchatMessages (컬렉션)

    ㄴ날짜 (문서)

      ㄴuserMessage (컬렉션)

        ㄴ메세지 번호 (문서)

          ㄴcontent: 메세지 내용 (필드)

          ㄴtime: 전송 시간 (필드)

 

현재 DB 구조는 이러한데 추후에 수정될 수도 있다.

 

채팅 메세지 번호를 생성할 때도 역시 moment 라이브러리를 이용해서 간단히 날짜와 시간을 얻을 수 있었다.

 

    function getDate() {
        return moment().format('YYYYMMDD');
    }

    function getTime() {
        return moment().format('HH:mm:ss');
    }

 

 

4. 채팅 메세지 레이아웃 만들기

본격적으로 채팅 내역 조회 기능을 구현하기 전에, 먼저 메세지들의 레이아웃을 만들어보았다.

 

 

일반 메신저와 큰 차이점 없이 이름과 메세지 내용, 전송 시간을 나타내도록 했다.

재사용을 위해 보낸 메세지, 받은 메세지를 각각 컴포넌트로 만들었다.

 

채팅 내역 조회까지 구현하면 주요 기능은 거의 완성되는 건데

구현하면서 얼마나 많은 시행착오를 겪게 될지ㅎㅎ 기대된다.

1. 친구 추가 시 Firestore DB에 저장

친구 찾기 기능을 이용하여 사용자 아이디를 검색하면 해당 사용자가 검색 결과로 나온다.

친구 추가하기 버튼을 클릭하면 친구 추가 진행 확인 alert가 뜨고, 확인 버튼을 클릭했을 때 Firestore DB에 저장된다.

 

 

현재 DB의 구조는 다음과 같이 되어있다.

users (컬렉션)

ㄴ사용자 이메일 (문서)

  ㄴfriends (컬렉션)

   ㄴ친구 이메일 (문서)

    ㄴ친구 아이디 (필드)

    ㄴ친구 이름 (필드)

로그인한 사용자의 문서 아래에 있는 friends 목록에 추가한 친구 정보가 등록되는 것이다.

 

이름은 추후에 수정 기능을 제공할 예정이라 필드에 넣고 싶지 않았는데

사이드바에 친구 목록을 불러오는 것을 구현할 때 중첩으로 DB를 읽어와야해서 비동기 처리가 필요한데

이것을 해결하지 못해서 나중에 다시 시도해보기로 하고, 현재는 이름 필드도 추가하게 해놨다.

 

 

2. 사이드바 친구 목록 표시

친구 추가, 그리고 나중에 구현될 친구 삭제 기능을 이용하여 친구 목록이 수정되면 친구 목록이 바로 업데이트될 수 있도록

첫 렌더링 후에 실행되는 useEffect 안에서 Firestore의 onSnapshot 메소드를 이용했다.

 

    const loginEmail = useSelector(state => state.user.email); //redux에서 관리하는 로그인한 사용자 이메일
    const [friends, setFriends] = React.useState([]);	//친구 목록을 상태로 관리함
    const usersFB = firestore.collection('users');	//Firestore에 있는 user 컬렉션

	useEffect(() => {
        usersFB.doc(loginEmail).collection('friends').onSnapshot((docs) => {
            let listFromFB = [];
            docs.forEach((doc) => {
                listFromFB.push({id: doc.data().id, name: doc.data().name, chatRoomNum: doc.data().chatRoomNum});
            });
            setFriends(listFromFB);
    })}, []);

 

DB에서 읽어온 친구 목록을 listFromFB라는 배열에 담고 이것을 상태로 넘겨주면 리렌더링 되면서 사이드바에 친구 목록이 반영된다.

 

 

친구 등록 후 사이드바에 해당 사용자가 추가된 모습이다.

 

 

3. 친구 이름 클릭 시 1:1 채팅방 팝업 띄우기

사이드바에 있는 친구 목록에서 각 사용자를 클릭하면 해당 사용자와 1:1 채팅을 할 수 있도록 채팅방 팝업을 띄운다.

리액트 라우팅에서 URL 파라미터를 이용하여 채팅 상대에 대한 정보를 전달한다.

 

 

전체적인 레이아웃 영역만 잡은 상태이고 조만간 채팅 메세지 레이아웃을 만들 예정이다.

1. 친구 검색 바로 업데이트하기

부모 컴포넌트(FindFriends.js)에서 검색할 아이디를 입력하면 props로 넘겨서

자식 컴포넌트(FriendProfile.js)가 바뀌어서 검색 결과를 보여주도록 의도했다.

 

그런데 처음 검색할 때는 제대로 출력이 되지만 두번째로 검색을 할 때는 FriendProfile.js가 바뀌지 않는 것이다.

FindFriends.js에서 검색한 아이디를 상태로 관리하기 때문에

예상대로라면 FriendProfile.js의 props 값이 바뀌므로 출력되는 내용이 바뀌어야하는데 그렇지 않았다.

 

라이프 사이클 함수를 이용하면 해결될까 싶어서 FriendProfile.js를 함수형 컴포넌트에서 클래스형 컴포넌트로 바꿔보기도 했으나 아무 변화가 없었다.

 

관련 자료를 찾아보다가 뜻밖의 해결책을 발견했다.

 

https://www.py4u.net/discuss/976613

 

React: why child component doesn't update when prop changes

Answer #11: I was encountering the same problem. I had a Tooltip component that was receiving showTooltip prop, that I was updating on Parent component based on an if condition, it was getting updated in Parent component but Tooltip component was not rende

www.py4u.net

 

 

나에게 도움이 된 답변은 Answer #6으로, key값을 추가하면 제대로 작동한다는 것이다.

그리고 이 방법대로 했더니, 정말 잘 작동되는 것이다!

리액트에서의 key값의 의미를 좀 더 찾아봐야겠다.

 

그리고 중첩 삼항 연산자를 사용할 부분이 생겨서 관련 자료를 찾아보았다.

 

https://nm-it-diary.tistory.com/38

 

[JAVA] 삼항 연산자 사용법 - 여러개 중첩으로 사용하기

if문 대신 삼항 연산자로 간단한 조건문을 구현할 수 있습니다. if문과 비교하여 삼항 연산자에 대해 알아보겠습니다. 1. if문 예제 int num = 5; String result = ""; if( num == 5 ) { result = "num은 5"; } el..

nm-it-diary.tistory.com

 

삼항 연산자는 JAVA와 JS의 문법이 같은 듯 하다.

 

 

코드 수정 이후 검색을 한번 하고 바로 이어서 다른 아이디를 검색하는 것이 가능해졌다.

그리고 검색 결과가 없을 때 메세지를 출력하도록 수정했다.

 

2. 로그아웃 시 웰컴 화면으로 이동하기

redux를 이용하여 로그인 사용자 상태를 관리하는 것으로 변경되어서

로그아웃 시 redux로 관리하는 상태를 초기화해야 웰컴 화면으로 이동하게 되었다.

 

redux 모듈에 초기값으로 상태를 변경하는 액션 생성 함수 및 액션을 추가하고

useDispatch를 이용해서 해당 액션 생성 함수를 실행시켰다.

 

redux 모듈인 user.js에서 관련 부분만 나타내면 이렇다.

//Actions
const RESET_USER = 'user/RESET_USER';

const initialState = {
    email: '',
    id: '',
    name: '',
    is_loaded: false,
}

//Action Creators
export const resetUser = () => {
    return {type: RESET_USER};
}

//Reducer
export default function reducer(state = initialState, action = {}){
    switch(action.type){
        //do reducer stuff
        case 'user/RESET_USER': {
            return initialState;
        }
        default:
            return state;
    }
}

 

useDispatch를 이용해서 resetUser 액션 생성 함수를 실행하는 부분이다.

import { useDispatch } from 'react-redux';

const Main = (props) => {
    const dispatch = useDispatch();
    
    const logout = () => {
        let popup = window.confirm('로그아웃 하시겠습니까?');
        if (popup) {    //'예'를 선택했을 때
            userSignOut();  //Firebase Authentication 로그아웃
            dispatch(resetUser());  //redux 유저 정보 초기화
            history.push('/'); //웰컴 화면으로 이동
        }
    }
    ...
}

 

+ Recent posts