본문 바로가기
10. 클론코딩/>> 인스타그램

05 Post 저장은 파이어베이스로

by 블록메타 2022. 12. 22.

5.1 Modal

 

Post 작성하는 Modal을 만들어 보자.  모달의 상태를 저장하는 상태가 필요하다. 호출곳과 모달에서 상태의 값을 공유해야 되기 때문에 recoil 라이브러리를 사용한다. 

 

npm i recoil

 

_app

import { RecoilRoot } from "recoil";


function MyApp({ Component, pageProps: { session, ...pageProps } }) {
 return (
   <SessionProvider session={session}>
     <RecoilRoot>
       <Component {...pageProps} />
     </RecoilRoot>
   </SessionProvider>
 )
}

atoms 디렉토리 생성 후  modalAtom.js 생성한다

import { atom } from "recoil";

export const modalState = atom({
   key: 'modalState',
   default: false,
});

Header.js 에서 modalState를 호출합니다.

import { modalState } from "../atoms/modalAtom";

function Header() {
   const { data: session } = useSession();
   const [open, setOpen] = useRecoilState(modalState);
   const router = useRouter();

Plus 아이콘을 선택시 open값을 true로 변경합니다.

  <PlusCircleIcon
     onClick={() => setOpen(true)}
     className="navBtn" />

components/Modal.js 파일을 생성한다.

import { modalState } from "../atoms/modalAtom";
import { useRecoilState } from "recoil";


function Modal() {
   const [open, setOpen] = useRecoilState(modalState);

 return (
   <div>
       
       {open && <p>THE MODAL IS OPEN</p>}
   </div>
 )
}

모달화면으로 만들기 위해서는 headlessui 라이브러리가 필요합니다. 아래와 같이 인스톨 합니다. 

 

npm i @headlessui/react

 

components/Modal.js

import React from 'react'
import { modalState } from "../atoms/modalAtom";
import { useRecoilState } from "recoil";
import { Dialog, Transition } from "@headlessui/react";
import { Fragment } from "react";


function Modal() {
   const [open, setOpen] = useRecoilState(modalState);

 return (
   <Transition.Root show={open} as={Fragment}>
       <Dialog
           as='div'
           className="fixed z-10 inset-0 overflow-y-auto"
           onClose={setOpen}
       >
           <div
               className="flex items-end justify-center min-h-[800px]
                          sm:min-h-screen pt-4 px-4 pb-20 text-center
                          sm:block sm:p-0"
           >
               <Transition.Child
                   as={Fragment}
                   enter="ease-out duration-300"
                   enterFrom="opacity-0"
                   enterTo="opacity-100"
                   leave="ease-in duration-200"
                   leaveFrom="opacity-100"
                   leaveTo="opacity-0"
               >
                   <Dialog.Overlay
                       className="fixed inset-0 bg-gray-500
                                 bg-opacity-75 transition-opacity"
                   />
               </Transition.Child>

               <span
                   className="hidden sm:inline-block sm:align-middle
                              sm:h-screen"
               >
                   &#8203;
               </span>
               <Transition.Child
                   as={Fragment}
                   enter="ease-out duration-300"
                   enterFrom="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
                   enterTo="opacity-100 translate-y-0 sm:scale-100"
                   leave="ease-in duration-200"
                   leaveFrom="opacity-100 translate-y-0 sm:scale-100"
                   leaveTo="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
               >
                   <div className="inline-block align-bottom bg-white rounded-lg px-4
                                   pt-5 pb-4 text-left overflow-hidden shadow-xl transform
                                   transition-all sm:my-8 sm:align-middle sm:max-w-sm
                                   sm:w-full sm:p-6">
                       <h1>Hello</h1>

                   </div>

               </Transition.Child>


           </div>
       </Dialog>
   </Transition.Root>
 )
}

export default Modal

모달로 hello 찍는데도 이렇게 많은 CSS가 필요하다. 이 부분은 그대로 가져다 사용할 수 있으므로 소스 설명은 생략하겠다. 

input box를 추가해 보자

                                      <div className="mt-3 text-center sm:mt-5">
                                           <Dialog.Title as="h3"
                                               className="text-lg leading-6 font-medium text-gray-900">
                                               Upload a photo
                                           </Dialog.Title>

                                           <div>
                                               <input
                                                   //ref={filePickerRef}
                                                   type="file" hidden
                                                   //onChange={addImageToPost}
                                                   />
                                           </div>

                                           <div className="mt-2">
                                               <input className="border-none focus:ring-0 w-full
                                                   text-center"
                                                   type="text"
                                                   //ref={captionRef}
                                                   placeholder="Please enter a caption..." />
                                           </div>
                                       </div>
                                   </div>

카메라 아이콘 표시하기

import { CameraIcon } from "@heroicons/react/outline";

<div
                                           onClick={() => filePickerRef.current.click()}
                                           className="mx-auto flex items-center justify-center h-12 w-12
                                           rounded-full bg-red-100 cursor-pointer">
                                           <CameraIcon
                                               className="h-6 w-6 text-red-600"
                                               aria-hidden="true"
                                           />

                                       </div>

카메라 버튼 클릭시 파일 다이얼로그

const filePickerRef = useRef(null);

<div
  onClick={() => filePickerRef.current.click()}
  className="mx-auto flex items-center justify-center h-12 w-12
  rounded-full bg-red-100 cursor-pointer">

 <CameraIcon
   className="h-6 w-6 text-red-600"
   aria-hidden="true"
 />

</div>
 
<input
   ref={filePickerRef}
   type="file" hidden
   //onChange={addImageToPost}
 />

 

사용자가 선택한 파일 저장하는 함수

const [ selectedFile, setSelectedFile ] = useState(null);

   const addImageToPost = (e) => {
       const reader = new FileReader();
       if(e.target.files[0]) {
           reader.readAsDataURL(e.target.files[0]);
       }

       reader.onload = (readerEvent) => {
           setSelectedFile(readerEvent.target.result);
       };
   }

파일 선택했을때와 선택하지 않았을때를 구분하여 코딩

{selectedFile ? (
                                       <img
                                           src={selectedFile}
                                           className="w-full object-contain cursor-pointer"
                                           onClick={() => setSelectedFile(null)}
                                           alt="" />
                                   ) : (
                                       <div
                                           onClick={() => filePickerRef.current.click()}
                                           className="mx-auto flex items-center justify-center h-12 w-12
                                           rounded-full bg-red-100 cursor-pointer">
                                           <CameraIcon
                                               className="h-6 w-6 text-red-600"
                                               aria-hidden="true"
                                           />

                                       </div>
 )}

Upload Post 클릭했을때 loading을 true로 셋팅

const uploadPost = async() => {
   if(loading) return;

   setLoading(true);
}

이제 Firebase 로 post 에서 올린 이미지를 올려보자

 

5.2 Firebase

 

Post의 데이타를 올리기 위해서는 Storage의 권한을 변경해 주어야 된다. 

 

console.firebase.google.com 접속해서 우리가 만든 앱을 선택하고 Storage 생성후 Rules 를 변경해 보자

이제 uploadPost를 수정해 보자

const uploadPost = async() => {
   if(loading) return;

   setLoading(true);

   // 1) Create a post and add to firestore 'posts' collection
   // 2) get the post ID for the newly created post
   // 3) upload the image to firebase storage with the post ID
   // 4) get a download URL from fb storage and update the original post with image

   const docRef = await addDoc(collection(db, 'posts'), {
       username: session.user.username,
       caption : captionRef.current.value,
       profileImg : session.user.image,
       timestamp: serverTimestamp()
   })
   console.log("New doc added with ID", docRef.id);

   const imageRef = ref(storage, `posts/${docRef.id}/image`)

   await uploadString(imageRef, selectedFile, "data_url").then(async snapshot => {
       const downloadURL = await getDownloadURL(imageUrl);

       await updateDoc(doc(db, 'posts', docRef.id), {
           image: downloadURL
       })
   });
}

이제 포스팅을 하고, firebase에 올라가는지 확인해 보자.

포스트 올리는것도 배웠으니,  기존에 더미로 표시한 부분은 리얼 포스트로 바꾸어 보자 



그리고, 포스트를 실시간으로 화면에 출력하는 부분도 알아보자 

 

firebase에서 snapshot api를 지원한다. 이 api는 실시간으로 DB의 내용을 받을 수 있다. 

 const [posts, setPosts] = useState([]);

 useEffect(() => {
   onSnapshot(collection(db, 'posts'))
 })

성공적으로 출력되었습니다.

 

Upload Post 클릭하면 실시간으로 반영된걸 확인할 수 있다.

5.4 Comments 기능

 

먼저 state를 만들자, 작성한 comment도 필요하고, comment의 리스트도 필요하다

 const [comment, setComment] = useState("");
 const [comments, setComments] = useState([]);

입력폼을 작성한다. 클릭시에는 sendComment을 호출한다.

 

<form className="flex items-center p-4">
               <EmojiHappyIcon className="h-7" />
               <input
                   type="text"
                   className="border-none flex-1 focus:ring-0 outline-none"
               />
               <button
                   type="submit"
                   disabled={!comment.trim()}
                   onClick={sendComment}
                   className="font-semibold text-blue-400">Post</button>
 </form>

 

comment 가 firebase에 등록된걸 확인할 수 있다.

 

화면에도 표시해 보자

 useEffect(() => onSnapshot(query(collect(db, 'posts', id, "comments" ),
       orderBy("timestamp", "desc")
       ),
       (snapshot) => setComments(snapshot.docs)
       ),
       [db]
 );


{/* comments */}
       {comments.length > 0 && (
           <div className="ml-10 h-20 overflow-y-scroll scrollbar-thumb-black
                           scrollbar-thin">
               {comments.map(comment => (
                   <div key={comment.id}
                   className='flex items-center space-x-2 '>
                       <img
                           className="h-7 rounded-full"
                           src={comment.data().userImage} alt="" />
                       <span className="font-bold">{comment.data().usernam}
                       </span>{" "}
                       <p>{comment.data().comment}</p>
                   </div>
               ))}
           </div>
       )}

 시간부분도 표시해 보자

npm i react-moment
import Moment from "react-moment";
  <Moment fromNow className="pr-5 text-xs">
        {comment.data().timestamp?.toDate()}
  </Moment>

방금 comment을 더 입력해 보았다.   in a few seconds로 출력된걸 확인할 수 있다.

 

5.5 좋아요 구현

실시간으로 좋아요 데이터를 firebase에서 가지고 온다.

const [likes, setLikes] = useState([]);
 
useEffect(
       () =>
         onSnapshot(collection(db, 'posts', id, 'likes'), (snapshot) =>
           setLikes(snapshot.docs)
         ),
      [db, id]
 );

하트 클릭시 처리

  useEffect(() => {
   setHasLiked(
       likes.findIndex((like) => (like.id === session?.user?.uid) !== -1)
   );
}, [likes])


 const likePost = async () => {
   console.log(hasLiked);
   if(hasLiked==0) {
       console.log("deleteDoc");
       await deleteDoc(doc(db, 'posts', id, 'likes', session.user.uid))
   } else {
       console.log("setDoc");
       await setDoc(doc(db, 'posts', id, 'likes', session.user.uid), {
           username: session.user.username
       });
   }
}



{(hasLiked===0) ? (
   <HeartIconFilled onClick={likePost} className="btn text-red-500"/>
) : (
   <HeartIcon onClick={likePost} className="btn"/>
)}

좋아요 갯수도 표시해요

{/* caption */}
       <p className="p-5 truncate">
           {likes.length > 0 && (
               <p className="font-bold mb-1">{likes.length} likes</p>
           )}
           <span className="font-bold mr-1">{username}</span>
           {caption}
       </p>

개발 작업은 끝났습니다. 이제 github에 올리고 배포 하도록 하겠습니다.

 

'10. 클론코딩 > >> 인스타그램' 카테고리의 다른 글

04 소셜로그인 인증하기  (0) 2022.12.22
03 시작하기  (0) 2022.12.21
02 환경설정  (0) 2022.12.21
01 Instagram 클론코딩 소개하기  (1) 2022.12.21

댓글