이전에 file을 추가하는 form이 있어서
사진을 추가할 수 있었는데,
저장이 되기 위해서는 파이어스토어에서 스토리지라는 기능을 사용해야한다.
해당 기능은 파이어스토어를 생성하면 자동으로 스토리
이제 스토리지로 넘어와서 시작하기 눌러서 시작하면된당.
이것도 테스트 모드로 시작하면됨
firebase.js에서 파이어 스토어를 사용 가능하도록 설정한다.
firebase.js
import { initializeApp } from "firebase/app";
import { getAuth } from "firebase/auth";
import { getStorage } from "firebase/storage";
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,
};
const app = initializeApp(firebaseConfig);
//파이어베이스 인증
const auth = getAuth();
//파이어스토어 DB사용
const db = getFirestore(app);
//스토리지 사용
const storage = getStorage(app);
export { auth, storage, db };
PostTweetForm.jsx
form에 onsubmit함수 추가한다.
onSubmit 함수에 트윗 내용을 DB에 저장하는 코드 추가
const onSubmit = async (e) => {
e.preventDefault();
const user = auth.currentUser;
//유저 정보가 없거나, 로딩상태거나, 트윗 내용이 없거나,180자 이상이면 리턴한다.
if (!user || isLoading || tweet === "" || tweet.length > 180) return;
try {
setLoading(true);
//파이어 스토어에 tweet 저장하기 -> addDoc
await addDoc(collection(db, "tweets"), {
tweet,//tweet : tweet과 동일한 코드
createdAt: Date.now(),
username: user.displayName || "익명",
userId: user.uid,
});
} catch (e) {
//에러가 날 경우에 콘솔로 출력함.
console.log(e);
} finally {
setLoading(false);
}
};
db는 firebase.js 라는 파일에서 가져오는거고
tweets는 컬렉션 이름 설정하는거임
tweet : usestate에 저장한 tweet 데이터 사용
createAt : 현재 날짜로 생성일자 설정
username : 유저의 displayName으로 저장하는데 만약에 없으면 익명으로 저장
유저는 : user의 uid로 저장
테스트하기
이미지가 있을 경우?
https://firebase.google.com/docs/storage/web/create-reference?hl=ko
먼저 업데이트를 할려면 추가한 데이터(doc)를 찾아서 추가해야 하므로
처음 tweet 을 저장시 그 레퍼런스를 변수에 저장한다.
const onSubmit = async (e) => {
e.preventDefault();
const user = auth.currentUser;
//유저 정보가 없거나, 로딩상태거나, 트윗 내용이 없거나,180자 이상이면 리턴한다.
if (!user || isLoading || tweet === "" || tweet.length > 180) return;
try {
setLoading(true);
//파이어 스토어에 tweet 저장하기 -> addDoc
const doc = await addDoc(collection(db, "tweets"), {
tweet, //tweet : tweet과 동일한 코드
createdAt: Date.now(),
username: user.displayName || "익명",
userId: user.uid,
});
if (file) {
//먼저 이미지를 저장할 참조 주소를 만든다.(tweets/유저아이디/문서아이디/이미지명.jpg)
//나중에 가져올 때 쉽게 가져오게하기위해서
const locationRef = ref(storage, `tweets/${user.uid}/${doc.id}`);
//파일을 서버에 저장하는 함수
const result = await uploadBytes(locationRef, file); //파일업로드
const url = await getDownloadURL(result.ref); //파일의 주소
await updateDoc(doc, {
photo: url,
});
setTweet(""); // 저장이 끝난 후에 트윗내용을 삭제함
setFile(null); // 파일도 삭제함
}
} catch (e) {
//에러가 날 경우에 콘솔로 출력함.
console.log(e);
} finally {
setLoading(false);
}
};
테스트를 해봤다.
photo라는 항목에 추가된것이 확인된다.
저기 photo에 저장된 url값을 가지고 브라우저에서 확인하면
저장한 사진이 출력된다.
이미지 파일 용량 체크
파이어베이스 스토어의 하루 무료 사용양은 1Gbyte 이다.
이미지파일을 1MB로 제한하면 1mb*1000 = 1gb 즉 1000번 업로드 가능하다.
그러므로 업로드할 파일 사이즈를 제한해 보자.
포스트트윗폼에서 파일을 우선 스테이트에 저장시 1mb보다 클 경우에 함수를 종료
const onFileChange = (e) => {
const { files } = e.target;
if (files && files.length === 1) {
const changeIamage = files[0];
if (changeIamage.size > 1000 * 1000) {
alert("이미지 사이즈는 1MB 이하로 해주세요");
setFile(null);
return;
}
setFile(changeIamage);
}
};
기존에 사용하던 onFileChange 함수에 코드를 추가하는데,
1mb -> 1,000,000보다 크면 경고창 발생하고 리턴함
1mb가 넘지 않으면 저장한다.
대용량 사이즈의 이미지를 첨부해봤다.
아래와 같은 경고창이 발생함
이제 작성한 트윗을 불러올 Timeline.jsx를 생성한다.
import { collection, getDocs, orderBy, query } from "firebase/firestore";
import { useEffect, useState } from "react";
import { styled } from "styled-components";
import { db } from "../firebase";
import Tweet from "./Tweet";
const Wrapper = styled.div`
margin-top: 20px;
`;
const Timeline = () => {
const [tweets, setTweet] = useState([]);
//트윗 가져오는 함수
const fetchTweets = async () => {
//tweets 컬렉션에서 가져온다.
//작성일자 기준으로 내림차순으로 가져온다(최신일자)
const q = query(collection(db, "tweets"), orderBy("createdAt", "desc"));
//다 가져올때 까지 기다림
const snapshot = await getDocs(q);
//snapshot안의 docs가 실제 문서임
//가져온 데이터를 분해해서 리턴한다.
const tweets = snapshot.docs.map((doc) => {
const { tweet, createdAt, userId, username, photo } = doc.data();
return {
tweet,
createdAt,
userId,
username,
photo,
id: doc.id,
};
});
setTweet(tweets);// 트윗에 저장한다.
};
useEffect(() => {
fetchTweets();//모든 트윗들을 가져오기
}, []);
return (
<Wrapper>
{tweets.map((tweet) => (
<Tweet key={tweet.id} {...tweet} />
))}
</Wrapper>
);
};
export default Timeline;
{...tweet} -> 트윗의 모든 내용 전달한다.
Tweet 컴포넌트로 하나의 트윗 데이터를 전달한다.
Tweet.jsx 생성
import { styled } from "styled-components";
const Wrapper = styled.div`
display: grid;
grid-template-columns: 3fr 1fr;
padding: 20px;
border: 1px solid rgba(255, 255, 255, 0.5);
border-radius: 15px;
margin-bottom: 10px;
`;
const Column = styled.div`
display: flex;
flex-direction: column;
justify-content: center;
`;
const Photo = styled.img`
width: 100px;
height: 100px;
border-radius: 15px;
`;
const Username = styled.span`
font-weight: 600;
font-size: 15px;
`;
const Payload = styled.p`
margin: 10px 0px;
font-size: 18px;
`;
const Tweet = ({ username, photo, tweet }) => {
return (
<Wrapper>
<Column>
<Username>{username}</Username>
<Payload>{tweet}</Payload>
</Column>
<Column>{photo ? <Photo src={photo} /> : null}</Column>
</Wrapper>
);
};
export default Tweet;
사진이 있으면? 사진을 출력하고 없으면 null
그리고 만든 타임라인을 Home.jsx에 추가한다.
import PostTweetForm from "../components/PostTweetForm";
import Timeline from "../components/Timeline";
const Home = () => {
return (
<h1>
<PostTweetForm />
<Timeline />
</h1>
);
};
export default Home;
아까 작성했던 트윗들이 잘 출력되는 것을 확인할 수 있다.
현재는 새로운 트윗을 추가해도 실시간 반영이 되지 않는다.
Timeline.jsx의 fetchTweets를 수정한다.
//트윗 가져오는 함수
const fetchTweets = async () => {
//tweets 컬렉션에서 가져온다.
//작성일자 기준으로 내림차순으로 가져온다(최신일자)
const q = query(collection(db, "tweets"), orderBy("createdAt", "desc"));
//다 가져올때 까지 기다림
// const snapshot = await getDocs(q);
//snapshot안의 docs가 실제 문서임
onSnapshot(q, (snapshot) => {
//가져온 데이터를 분해해서 리턴한다.
const tweets = snapshot.docs.map((doc) => {
const { tweet, createdAt, userId, username, photo } = doc.data();
return {
tweet,
createdAt,
userId,
username,
photo,
id: doc.id,
};
});
setTweet(tweets); // 트윗에 저장한다.
});
};
실시간 업데이트가 되는것을 확인할 수 있다.
화면을 사용하지 않으면 실시간 업데이트가 중지되어야함.
https://blog.naver.com/drv983/223261667392
import {
collection,
getDocs,
onSnapshot,
orderBy,
query,
} from "firebase/firestore";
import { useEffect, useState } from "react";
import { styled } from "styled-components";
import { db } from "../firebase";
import Tweet from "./Tweet";
const Wrapper = styled.div`
margin-top: 20px;
`;
const Timeline = () => {
const [tweets, setTweet] = useState([]);
useEffect(() => {
let unsub = null;
//트윗 가져오는 함수
const fetchTweets = async () => {
//tweets 컬렉션에서 가져온다.
//작성일자 기준으로 내림차순으로 가져온다(최신일자)
const q = query(collection(db, "tweets"), orderBy("createdAt", "desc"));
//다 가져올때 까지 기다림
//const snapshot = await getDocs(q);
//snapshot안의 docs가 실제 문서임
//실시간으로 가져오는 작업을 중단하는 함수가 unsub임
unsub = onSnapshot(q, (snapshot) => {
//가져온 데이터를 분해해서 리턴한다.
const tweets = snapshot.docs.map((doc) => {
const { tweet, createdAt, userId, username, photo } = doc.data();
return {
tweet,
createdAt,
userId,
username,
photo,
id: doc.id,
};
});
setTweet(tweets); // 트윗에 저장한다.
});
};
fetchTweets(); //모든 트윗들을 가져오기
return () => unsub();
}, []);
return (
<Wrapper>
{tweets.map((tweet) => (
<Tweet key={tweet.id} {...tweet} />
))}
</Wrapper>
);
};
export default Timeline;
이렇게 코드 수정하고 오류가 있는지 테스트 해본다.
최대 25개만 가져오게 쿼리문 수정
const q = query(
collection(db, "tweets"),
orderBy("createdAt", "desc"),
limit(25)
);
삭제하기 기능 추가
import { deleteDoc, doc } from "firebase/firestore";
import { deleteObject, ref } from "firebase/storage";
import { styled } from "styled-components";
import { auth, db, storage } from "../firebase";
const Wrapper = styled.div`
display: grid;
grid-template-columns: 3fr 1fr;
padding: 20px;
border: 1px solid rgba(255, 255, 255, 0.5);
border-radius: 15px;
margin-bottom: 10px;
`;
const Column = styled.div`
display: flex;
flex-direction: column;
justify-content: center;
`;
const Photo = styled.img`
width: 100px;
height: 100px;
border-radius: 15px;
`;
const Username = styled.span`
font-weight: 600;
font-size: 15px;
`;
const Payload = styled.p`
margin: 10px 0px;
font-size: 18px;
`;
const DeleteButton = styled.button`
background-color: tomato;
color: white;
font-weight: 600;
border: 0;
font-size: 12px;
padding: 5px 10px;
text-transform: uppercase;
border-radius: 5px;
cursor: pointer;
`;
const Tweet = ({ username, photo, tweet, userId, id }) => {
const user = auth.currentUser; // 인증한 유저 가져오기(현재 로그인 유저)
//삭제 버튼을 클릭했을 경우 실행되는 함수
const onDelete = async () => {
//확인 누르면 true로 삭제를 실행한다.
const ok = confirm("해당 트윗을 삭제하시겠습니까?");
//현재 로그인 되어있는 유저의 아이디와 트윗 작성자의 id와 동일하지 않으면 리턴한다. (user가 있으면 uid를 가져옴)
if (!ok || user?.uid !== userId) return;
try {
//1. DB의 트윗 삭제
//tweets컬렉션의 id에 해당하는 문서(DB)를 삭제한다.
await deleteDoc(doc(db, "tweets", id));
//2. 이미지 삭제
//만약 사진이 있으면 사진도 스토리지에서 삭제한다.
if (photo) {
const photoRef = ref(storage, `tweets/${user.uid}/${id}`);
await deleteObject(photoRef);
}
} catch (e) {
//오류가 있으면 콘솔로 출력한다.
console.log(e);
} finally {
//
}
};
return (
<Wrapper>
<Column>
<Username>{username}</Username>
<Payload>{tweet}</Payload>
<div>
{/* 현재 인증되어있는 유저가 있으면 게시글 작성자 id와 비교해서 동일할 경우에만 버튼이 출력된다. */}
{user?.uid === userId ? (
<DeleteButton onClick={onDelete}>Delete</DeleteButton>
) : null}
</div>
</Column>
<Column>{photo ? <Photo src={photo} /> : null}</Column>
</Wrapper>
);
};
export default Tweet;
삭제됨을 확인할 수 있다.
그리고 실제로 스토리지에서도 사진이 삭제됐는지 확인