새 프로젝트 만들기 - vite로
터미널에 설치한다.
npm i firebase react-firebase-hooks
firebase를 사용할 때 도움이 되는 hook 임
와 함께 두 개 설치함
그리고 파일들을 정리한다.
App.css 내용 지우기, App.jsx div만 남겨놓고 삭제
index.css는 삭제 -> main.jsx에서 링크삭제
https://firebase.google.com/?hl=ko
파이어 베이스에서 새 프로젝트 생성한다.
firebase.js 생성해서 붙여넣기하기
근데 이 키는 중요한 키이기 때문에 .env파일로 만들어야함
이메일은 본인 거
구글 계정으로 인증하게됨
firebase.js 에서 설정해줘야함
import { initializeApp } from "firebase/app";
import { GoogleAuthProvider, getAuth } from "firebase/auth";
import { getFirestore } from "firebase/firestore";
const firebaseConfig = {
apiKey: import.meta.env.VITE_API_KEY,
authDomain: import.meta.env.VITE_AUTH_DOMAIN,
projectId: import.meta.env.VITE_PROJECTID,
storageBucket: import.meta.env.VITE_STORAGE_BUCKET,
messagingSenderId: import.meta.env.VITE_MESSAGEING_SENDERID,
appId: import.meta.env.VITE_APPID,
};
//fire base 초기화
const app = initializeApp(firebaseConfig);
//구글 인증 객체
const googleAuth = new GoogleAuthProvider();
//fire store db 사용
const db = getFirestore();
//파이어 베이스 인증객체
const auth = getAuth();
export { auth, googleAuth, db };
아까 설치한 react firebase hooks 를사용해서
https://github.com/CSFrequency/react-firebase-hooks/tree/master/auth#useauthstate
인증이 됐는지 안됐는지 바로 체크가 가능하다.
테스트 용으로 user 바로 콘솔로 찍어본다
현재 인증이 되지 않았으니까 유저가 없을거임
App.jsx
import "./App.css";
import { useAuthState } from "react-firebase-hooks/auth";
import { auth, googleAuth } from "./firebase";
import { signInWithPopup, signOut } from "firebase/auth";
function App() {
const [user, loading, error] = useAuthState(auth);
console.log(user);
//로딩중, 에러일 경우 출력함
if (loading) {
return (
<div>
<p>로딩중...</p>
</div>
);
}
if (error) {
return (
<div>
<p>Error:{error}</p>
</div>
);
}
return (
<div className="App">
<header>
<h1>⚛️🔥💬</h1>
{/* 로그인이 되어있으면 로그아웃 보여줌 */}
{user && <SignOut />}
</header>
{/* 로그인이 되어있으면 채팅룸을 보여주고 아니면 signIn보여줌 */}
<section>{user ? <div>채팅룸</div> : <SignIn />}</section>
</div>
);
}
// SignIn 컴포넌트 -signInWithPopup import필요
function SignIn() {
//구글 인증 버튼 클릭 시 실행
const signInWithGoogle = async () => {
try {
await signInWithPopup(auth, googleAuth);
} catch (error) {
console.log(error);
}
};
return (
<>
<button className="sign-in" onClick={signInWithGoogle}>
Sign in with Google
</button>
<p>🌱 커뮤니티에서 예의를 지켜주세요 😁</p>
</>
);
}
// SignOut 컴포넌트 -signout import 필요
function SignOut() {
const logout = async () => {
try {
await signOut(auth);
} catch (error) {
console.log(error);
}
};
return (
<button className="sign-out" onClick={logout}>
Sign Out
</button>
);
}
export default App;
App.css
body {
background-color: #282c34;
}
.App {
text-align: center;
max-width: 728px;
margin: 0 auto;
}
.App header {
background-color: #181717;
height: 10vh;
min-height: 50px;
color: white;
position: fixed;
width: 100%;
max-width: 728px;
top: 0;
display: flex;
align-items: center;
justify-content: space-between;
z-index: 99;
padding: 10px;
box-sizing: border-box;
}
.App section {
display: flex;
flex-direction: column;
justify-content: center;
min-height: 100vh;
background-color: rgb(40, 37, 53);
}
main {
padding: 10px;
height: 80vh;
margin: 10vh 0 10vh;
overflow-y: scroll;
display: flex;
flex-direction: column;
}
main::-webkit-scrollbar {
width: 0.25rem;
}
main::-webkit-scrollbar-track {
background: #1e1e24;
}
main::-webkit-scrollbar-thumb {
background: #6649b8;
}
form {
height: 10vh;
position: fixed;
bottom: 0;
background-color: rgb(24, 23, 23);
width: 100%;
max-width: 728px;
display: flex;
font-size: 1.5rem;
}
form button {
width: 20%;
background-color: rgb(56, 56, 143);
}
input {
line-height: 1.5;
width: 100%;
font-size: 1.5rem;
background: rgb(58, 58, 58);
color: white;
outline: none;
border: none;
padding: 0 10px;
}
button {
background-color: #282c34; /* Green */
border: none;
color: white;
padding: 15px 32px;
text-align: center;
text-decoration: none;
display: inline-block;
cursor: pointer;
font-size: 1.25rem;
}
button:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.sign-in {
color: #282c34;
background: white;
max-width: 400px;
margin: 0 auto;
}
ul,
li {
text-align: left;
list-style: none;
}
p {
max-width: 500px;
margin-bottom: 12px;
line-height: 24px;
padding: 10px 20px;
border-radius: 25px;
position: relative;
color: white;
text-align: center;
}
.message {
display: flex;
align-items: center;
}
.sent {
flex-direction: row-reverse;
}
.sent p {
color: white;
background: #0b93f6;
align-self: flex-end;
}
.received p {
background: #e5e5ea;
color: black;
}
img {
width: 40px;
height: 40px;
border-radius: 50%;
margin: 2px 5px;
}
버튼을 클릭하면 Google 계정으로 로그인 팝업 창이 뜨게된다.
위의 함수가 실행되기 때문 signInWithPopop
인증을하게 되면 채팅룸이 뜬다
콘솔로 확인하면 로그인 유저가 확인된다.
그리고 파이어 베이스에서 확인해봐도 인증된 유저가 확인된다.
로그아웃 버튼을 클릭하면
아래의 함수가 실행되고
유저 null 이되고 기존 페이지로 돌아온다.
이제 채팅룸을 만든다.
https://www.npmjs.com/package/react-firebase-hooks
https://github.com/csfrequency/react-firebase-hooks/tree/09bf06b28c82b4c3c1beabb1b32a8007232ed045/firestore#usecollection
을 사용할거임
import {
addDoc,
collection,
limit,
orderBy,
query,
serverTimestamp,
} from "firebase/firestore";
import { useRef, useState } from "react";
import { useCollectionData } from "react-firebase-hooks/firestore";
import { auth, db } from "./firebase";
const ChatRoom = () => {
const dummy = useRef(); //html을 선택하기 위한 객체
//자바스크립트를 쓸때는 쿼리셀렉터를 사용했는데, 리액트에는 항상 변하기 때문에 useRef를 사용한다 .
const [formValue, setFormValue] = useState(""); //메세지 저장
const messagesRef = collection(db, "messages"); //컬렉션 자동으로 생성됨
//qurey -> 생성일자 기준으로 정렬 , 최대 25개까지
const q = query(messagesRef, orderBy("createdAt"), limit(25));
//실시간 메세지들을 가져온다.
const [messages] = useCollectionData(q); //import 필요
//submit 시 실행되는 함수
const sendMessage = async (e) => {
e.preventDefault();
console.log(auth.currentUser); // 유저 정보 출력해보기
const { uid, photoURL } = auth.currentUser; //인증객체 auth에서 현재 유저 정보를 가져옴
//addDoc import 필요 messages 컬렉션에 저장함
await addDoc(messagesRef, {
text: formValue, //메세지
createdAt: serverTimestamp(), //생성일자 import 필요
uid, // user.id
photoURL, // 유저 프로필 사진
});
setFormValue(""); // 저장이 다 되면 공백으로 저장
};
return (
<>
<main>
{/* 메세지들이 있으면 map반복문으로 출력한다. */}
{messages &&
messages.map((msg, idx) => <ChatMessage key={idx} message={msg} />)}
<span ref={dummy}></span>
</main>
<form onSubmit={sendMessage}>
<input
value={formValue}
onChange={(e) => setFormValue(e.target.value)}
placeholder="메세지를 입력하세요~"
/>
{/* 메세지에 값이 저장되어있지앟으면 disabled(클릭못함) */}
<button type="submit" disabled={!formValue}>
🕊️
</button>
</form>
</>
);
};
//ChatMessage 컴포넌트 -> 반복문 시 해당 컴포넌트 사용
//message의 모든 내용을 props로 받는다.
function ChatMessage(props) {
// props의 객체들을 분리한다.
const { text, uid, photoURL } = props.message;
//글쓴사람의 uid가 본인과 같으면 클래스는 sent 아니면 received (받은것) 왼쪽 오른쪽 구분
const messageClass = uid === auth.currentUser.uid ? "sent" : "received";
return (
<>
{/* 위에서 함수로 만든 class 넣음(본인/상대방) */}
<div className={`message ${messageClass}`}>
<img
src={
// 구글 계정에 프로필이 없는 사람은 샘플이미지 들어감
}
/>
<p>{text}</p>
</div>
</>
);
}
export default ChatRoom;
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
match /{document=**} {
allow read, write: if false;
}
match /messages/{docId} {
allow read: if request.auth.uid != null;
allow create: if request.auth.uid != null
&& request.auth.uid == request.resource.data.uid;
}
}
}
allow read , wrate : if false; -> 아무도 못씀
messages 컬렉션에
read : 인증된 사람만 읽을 수 있다.
//create는 write,delete,update포함임
create: 생성은 인증되고,문서의 작성만 가능하다
다른 사람이 들어와서 채팅하는 테스트를 하는 와중에
본인의 ip 주소로 바로 들어가지지 않는 오류가 발생함
그래서
package.json의 dev에 본인의 ip주소와 port번호 입력하고
다른 유저가 들어와서 글을 남기면 왼쪽에 위치한다.
파이어베이스에서 인증-> settings-> 승인된 도메인에 본인 도메인 추가하면 공유가 됨
내 도메인 추가하고 승인하면
현재 채팅이 새로 추가되면 자동으로 스크롤이 안내려감
화면에 마지막까지 내려왔을경우 메세지가 추가되었을때 보이지 않게 된다.
이를 방지하기 위해 dummy를 메세지 아래에 잡고 스크롤 한다.
메세지 밑에 dummy로 span태그 잡아놓은거임(채팅 제일 마지막 위치)
테스트해본다