백엔드 쪽에서 웹소켓을 만드는데 차질이있어 기한 내에 프로젝트를 만들기 힘들 것을 예상
채팅 기능을 serverless로 빠르게 구현해야겠다고 판단
실시간 채팅에 최적화된 firebase
사용
프로젝트 시작하기
프로젝트 이름, 애널리틱스 설정 등 완료
내가 이번에 사용할 것은 채팅만 구현 할 것이므로 Firebase Database
만 사용 할 것이다.
'데이터 베이스 만들기' 클릭
프로덕션, 테스트 아무거나 상관없다. 용도에 맞게 선택
위치는 asia-northeast3(soul)
선택
전체 유저 목록 (users)
유저 당 채팅 목록 (chats)
채팅방 별 채팅 로그 (userChats)
읽기, 쓰기 권한 설정
allow read, write: if false;
false
를 true
로 수정firebase 내 앱 등록
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>
);
}