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"
>
​
</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 |
댓글