Firebase를 사용해서 무엇을 할 수 있을까?
Firebase는 처음엔 데이터베이스였지만 지금은 백엔드 기능을 포괄한다. app이 필요로 하는 많은 것들을 해줄 수 있다. 즉, 데이터베이스 관련 코드 없이 데이터베이스를 사용할 수 있게 해 준다. 그러나 데이터베이스나 사용자나 서버 전부 구글로부터 빌려서 쓰기 때문에 아이디어를 테스트할 때만 사용하는 것이 좋다고 한다.
Firebase 초기 설정
✅ Firebase 설치
사이트에 들어가서 앱을 등록하고 프로젝트에 firebase를 설치한다. 그리고 Firebase SDK를 추가해준다.
npm install firebase
✅ 환경변수 설정
React.js application의 경우에는 create-react-app을 사용한 경우에 환경 변수를 설정해야 된다면 REACT_APP으로 시작해야 되고 그 뒤로 이름을 붙여줘야 한다. (ex REACT_APP_"SOMETHING") create-react-app은 만든 사람들에 의해서 REACT_APP으로 시작하는 환경변수를 찾도록 자동으로 설정이 되어 있기 때문이다.
❗ Github Key 보안
.env 파일은 firebase.js 파일 안에 있는 값을 보안을 위해 키값 그대로가 아닌 다른 값으로 치환시키기 위해 존재하며, github 같은 공용 웹사이트에 코드를 올릴 때 개인정보 보호 겸 사용된다.
+ .env 파일은 프로젝트 최상위에 있어야 한다.
로그인
✅ Login
import { getAuth } from "firebase/auth";
export const authService = getAuth();
{
"compilerOptions": {
"baseUrl": "src"
},
"include": ["src"]
}
이메일/비밀번호, Google, GitHub에 로그인 제공 요청을 한다.
EmailAuthProvider
아래 사이트에서 이메일 프로바이더를 제공받아 사용!
createUserWithEmailAndPassword
signInWithEmailAndPassword
import {
createUserWithEmailAndPassword,
signInWithEmailAndPassword,
getAuth,
} from "firebase/auth";
const handleSubmit = async (e) => {
e.preventDefault();
try {
const auth = getAuth();
if (newAccount) {
const data = await createUserWithEmailAndPassword(
auth,
email,
password
);
console.log(data);
} else {
const data = await signInWithEmailAndPassword(auth, email, password);
console.log(data);
}
} catch (error) {
console.log(error);
}
};
프로젝트 폼을 통해 새로운 계정을 만드니 firebase 홈페이지에 계정이 생성된 것을 볼 수 있다.
❗여기서 중요한 것! persistence
사용자들을 얼마나 기억할 것인지 선택할 수 있도록 해준다.
- local은 기본값이며 브라우저를 닫더라도 사용자 정보는 기억되는 것
- session은 브라우저가 열려있는 동안에는 사용자 정보를 기억하는 것
- none은 유저를 기억하지 않는다는 것
이 프로젝트에서는 local을 사용해 브라우저를 닫더라도 사용자 정보가 기억되도록 할 것이다.
firebase authService.currentUser에서는 실제로 로그인된 건지 로그아웃한 건지 확인할 수 없다.
그래서 onAuthStateChanged 메서드를 사용해 확인해준다.
const [init, setInit] = useState(false);
const [isLoggedIn, setIsLoggedIn] = useState(false);
useEffect(() => {
authService.onAuthStateChanged((user) => {
if (user) {
setIsLoggedIn(true);
} else {
setIsLoggedIn(false);
}
setInit(true);
});
}, []);
✅ 소셜 로그인
팝업창으로 소셜 로그인되도록 만들기
firebase는 생각보다 강력한 친구였다.... 너무나도 간단하게 구현할 수 있었음
import {
signInWithPopup,
GoogleAuthProvider,
GithubAuthProvider,
} from "firebase/auth";
import { authService } from "fbase";
```생략```
const handleSubmit = async (e) => {
e.preventDefault();
try {
const auth = getAuth();
if (newAccount) {
const data = await createUserWithEmailAndPassword(
auth,
email,
password
);
console.log(data);
} else {
const data = await signInWithEmailAndPassword(auth, email, password);
console.log(data);
}
} catch (error) {
setError(error.message);
}
};
return(
<button name="google" onClick={onSocialClick}>
Continue with Google
</button>
<button name="github" onClick={onSocialClick}>
Continue with Github
</button>
)
}
Firestore Database CRUD
✅ 데이터베이스 setup
테스트 모드로 만들어준다.
✅ DB collection 추가 (addDoc) CREATE
프로젝트에 firestore을 연결해준다.
import { getFirestore } from "firebase/firestore";
export const dbService = getFirestore();
폼을 제출하면 DB에 collection이 저장되도록 구현한다.
import { dbService } from "fbase.js";
import { addDoc, collection } from "firebase/firestore";
```생략```
const handleOnSubmit = async (e) => {
e.preventDefault();
try {
const docRef = await addDoc(collection(dbService, "nweets"), {
nweet,
createdAt: Date.now(),
});
setNweet("");
} catch (error) {
console.log(error);
}
};
✅ DB 글 가져오기 (getDocs) READ
forEach를 사용해 새로운 객체를 만들고 글을 업데이트해줬다.
import { dbService } from "fbase.js";
import { addDoc, getDocs, collection } from "firebase/firestore";
```생략```
const getNweets = async () => {
const dbNweets = await getDocs(collection(dbService, "nweets"));
dbNweets.forEach((document) => {
const nweetObject = {
...document.data(),
id: document.id,
};
setNweets((prev) => [nweetObject, ...prev]);
console.log(nweets);
});
};
return (
<div>
{nweets.map((nweet) => (
<div>
<h4>{nweet.nweet}</h4>
</div>
))}
</div>
);
But 게시글이 두 번 렌더링 되는 문제 발생했다. 이유를 찾아보니 index.js의 strictmode는 side effect를 줄이기 위해 일부러 두 번씩 실행시킨다고 한다. 그래서 개발 환경에서만 두번씩 호출되고 배포하면 무시된다고 한다!
✅ DB 글 가져오기 (onSnapshot) READ
위처럼 forEach 하는 방식은 리렌더가 많이 되고 실시간으로 업데이트가 되지 않았다.
그래서 onSnapshot()를 사용해, 더 적게 리렌더되고 빠르게 실행되도록 해줬다.
+ 작성 순서대로 화면에 나타나게 하기 위해서는 orderBy()를 사용한다.
import { dbService } from "fbase.js";
import {
query,
addDoc,
collection,
orderBy,
onSnapshot,
} from "firebase/firestore";
```생략```
useEffect(() => {
const q = query(
collection(dbService, "nweets"),
orderBy("createdAt", "desc")
);
onSnapshot(q, (snapshot) => {
const nweetArray = snapshot.docs.map((document) => ({
id: document.id,
...document.data(),
}));
setNweets(nweetArray);
});
}, []);
✅ DB 글 수정, 삭제 (onSnapshot) UPDATE, DELETE
유저가 직접 쓴 글이여야만 수정 삭제가 가능하도록 구현했다.
수정모드일 때는 form이 열리고, 완료가 되면 닫히도록 토글 구현
import React, { useState } from "react";
import { dbService } from "fbase.js";
import { doc, deleteDoc, updateDoc } from "firebase/firestore";
export default function Nweet({ nweetObj, isOwner }) {
const [editing, setEditing] = useState(false);
const [newNweet, setNewNweet] = useState(nweetObj.text);
// DELETE
const onDeleteClick = async () => {
const ok = window.confirm("Are you sure you want to delete this nweet?");
if (ok) {
const NweetTextRef = doc(dbService, "nweets", `${nweetObj.id}`);
await deleteDoc(NweetTextRef);
} else {
console.log(ok);
}
};
// UPDATE
const toggleEditing = () => setEditing((prev) => !prev);
const onChange = (e) => {
const { value } = e.target;
setNewNweet(value);
};
const onSubmit = async (e) => {
e.preventDefault();
const NweetTextRef = doc(dbService, "nweets", `${nweetObj.id}`);
await updateDoc(NweetTextRef, {
text: newNweet,
});
setEditing(false);
};
return (
<div>
{editing ? (
<>
<form onSubmit={onSubmit}>
<input
type="text"
placeholder="Edit your nweet"
value={newNweet}
required
onChange={onChange}
/>
<input type="submit" value="완료" />
</form>
<button onClick={toggleEditing}>취소</button>
</>
) : (
<>
<h4>{nweetObj.text}</h4>
{isOwner && (
<>
<button onClick={onDeleteClick}>삭제</button>
<button onClick={toggleEditing}>수정</button>
</>
)}
</>
)}
</div>
);
}
Storage
❗FileReadaer
파일을 읽고 미리보기를 보여주는 방법이다. 파일을 만들고 문자열로 변환해서 사용한다.
먼저, reader에 이벤트 리스너를 추가하여 파일 로딩이 끝날 때 finishedEvent를 갖게 된다.
다음으로 reader.readAsDataURL을 실행하게 되어 데이터를 얻게 된다.
const handleOnFileChange = (e) => {
const { files } = e.target;
const theFile = files[0];
const reader = new FileReader();
reader.onloadend = (finishedEvent) => {
setphoto(finishedEvent.currentTarget.result);
};
reader.readAsDataURL(theFile);
};
const handleOnClearPhotoClick = () => setphoto(null);