개발일지/React

[노마드 코더] ReactJS 트위터 클론 코딩 - Realtime Tweets & Delete

OH!Lee 2025. 2. 6. 22:49
반응형

이 포스팅은 노마드 코더님의 트위터 클론코딩을 학습 후 작성하였습니다. (무료!!)

https://nomadcoders.co/nwitter

 

트위터 클론코딩 – 노마드 코더 Nomad Coders

React Firebase for Beginners

nomadcoders.co


 이번에는 Firestore DB에서 실시간으로 쿼리의 변경 사항을 수신하는 방법을 적용해 보자. timeline.tsx의 fetchTweets()에서 기존에 사용하였던 getDeocs() 구문을 주석 처리 하고 onSnapshot()으로 대체할 것이다.

const fetchTweets = async() => {
    const tweetsQuery = query(
        collection(db, "tweets"),
        orderBy("createdAt", "desc")
    )
    // const snapshot = await getDocs(tweetsQuery);
    // 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);

    await onSnapshot(tweetsQuery, (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);
    });
}

이제 데이터 베이스의 항목이 추가, 변경, 삭제되면 새로고침을 하지 않아도 즉각적으로 바로 반영이 된다!

하지만, 이러한 실시간 이벤트 리스너는 지속적으로 사용 시 많은 비용을 발생시킬 수 있기 때문에 unsubscribe함수를 통해 현재 페이지를 떠나거나 추적이 불필요할 때에는 이벤트 리스너를 끄는 것이 중요하다. 

 fetchTweets()을 useEffect() 안으로 옮겨주고, unsubscribe 변수 하나를 생성한다. 그리고 onSnapshot()을 unsubscribe 변수로 저장한다. 변수는 기본적으로 null로 초기화시켜 준다. 

useEffect(() => {
        let unsubscribe : Unsubscribe | null = null;
        const fetchTweets = async() => {
            const tweetsQuery = query(
                collection(db, "tweets"),
                orderBy("createdAt", "desc"),
            )
            // const snapshot = await getDocs(tweetsQuery);
            // 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);
   
            unsubscribe = await onSnapshot(tweetsQuery, (snapshot) => {
                const tweets = snapshot.docs.map((doc) => {
                    const {tweet, createdAt, userId, username, photo} = doc.data();
                    return {tweet, createdAt, userId, username, photo, id:doc.id}
                });
                console.log("receive real time");
                setTweet(tweets);
            });
        }
        fetchTweets();
        return () => {
            unsubscribe && unsubscribe();
        }
    }, []);

 useEffect()에서 fetchTweets()이 실행이 되고, onSnapshot() 함수가 실행되면서 만들어둔 unsubscribe에 정보들이 담기게 된다. 이후 페이지를 벗어날 때 useEffect()의 return 함수에 unsubscribe이 null이 아니라면 unsubscribe()를 반환하고 clean up기능을 사용해 onSnapshot()을 멈추게 한다.

 그리고 한 번에 엄청난 데이터의 양을 읽어오는 것을 방지하기 위해 tweetsQuery()에 limit() 함수를 이용해 불러오는 게시글의 제한도 두도록 한다.

 

timeline.tsx 전체 코드

import { collection, getDocs, limit, onSnapshot, orderBy, query } from "firebase/firestore";
import { useEffect, useState } from "react";
import styled from "styled-components";
import { db } from "../firebase";
import Tweet from "./tweet";
import { Unsubscribe } from "firebase/auth";

export interface ITweet {
    id: string;
    photo? : string;
    tweet: string;
    userId: string;
    username: string;
    createdAt: number;
}

const Wrapper = styled.div`
    display: flex;
    gap: 10px;
    flex-direction: column;
`;

export default function Timeline() {
    const [tweets, setTweet] = useState<ITweet[]>([]);  
    useEffect(() => {
        let unsubscribe : Unsubscribe | null = null;
        const fetchTweets = async() => {
            const tweetsQuery = query(
                collection(db, "tweets"),
                orderBy("createdAt", "desc"),
                limit(20),
            )
            // const snapshot = await getDocs(tweetsQuery);
            // 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);
   
            unsubscribe = await onSnapshot(tweetsQuery, (snapshot) => {
                const tweets = snapshot.docs.map((doc) => {
                    const {tweet, createdAt, userId, username, photo} = doc.data();
                    return {tweet, createdAt, userId, username, photo, id:doc.id}
                });
                console.log("receive real time");
                setTweet(tweets);
            });
        }
        fetchTweets();
        return () => {
            unsubscribe && unsubscribe();
        }
    }, []);
    return (<Wrapper>
        {tweets.map(tweet => <Tweet key={tweet.id} {...tweet} />)}
    </Wrapper>
    )
}

 이제 글을 작성 후 새로고침을 하지 않더라도 즉각적인 업데이트를 받을 수 있다.


Deleteing Tweets

 이제 트윗을 삭제할 수 있는 버튼을 만들어 보자. 트윗은 트윗을 작성한 작성자만 삭제가 가능하게 할 것이다. tweet.tsx에 버튼 하나를 생성한다.

const DeleteButton = styled.button``;

export default function Tweet({username, photo, tweet}:ITweet) {
    return (<Wrapper>
        <Column>
            <Username>{username}</Username>
            <Payload>{tweet}</Payload>
            <DeleteButton>Delete</DeleteButton>  <--버튼은 Payload밑에 넣어준다.
        </Column>
        {photo === "" ? null :<Column>
            <Photo src={photo} />
        </Column>}
    </Wrapper>

    )
}

버튼의 css를 꾸며보자.

const DeleteButton = styled.button`
    background-color: dodgerblue;
    color: white;
    font-weight: 600;
    font-size: 12px;
    padding: 5px 10px;
    text-transform: uppercase;
    border-radius: 5px;
    cursor: pointer;
`;

 이제 이 버튼은 작성자 아이디와 현재 로그인된 사용자가 같을 때에만 보이게 만든다. 먼저 user변수를 만든다.

const user = auth.currentUser;

firebase의 auth를 이용해 현재 유저를 확인할 수 있다. Tweet을 받아올 때 props에 추가로 userId와 id도 함께 받아 온다.

export default function Tweet({username, photo, tweet, userId, id}:ITweet)

그리고 user와 userId가 같을 때에만 버튼을 보이도록 설정한다.

{user?.uid === userId ? <DeleteButton>Delete</DeleteButton> : null}

다른 사용자로 로그인 시 버튼이 보이지 않게 해 준다.

 삭제를 위한 onDelete() 함수를 만들고 onClick event 등록을 해준다.

const onDelete = async() => {
        const ok = confirm("Are you sure you want to delete this tweet?");
        if(!ok || user?.uid !== userId) return;
        try {
            await deleteDoc(doc(db, "tweets", id));
        } catch (e) {
            console.log(e);
        } finally {

        }
    }
{user?.uid === userId ? <DeleteButton onClick={onDelete}>Delete</DeleteButton> : null}

 이제 삭제 버튼을 누르면 사제를 원하는지 확인 문구가 나오고, 확인을 누르면 삭제를 진행한다. 하지만 이미지 파일의 경우 S3를 통한 업로드를 했기 때문에 이미지가 있다면 따로 S3에 있는 image도 삭제시켜야 한다.


Delete S3 image

 예전에 image를 update 해 줄 때 url의 location만 업로드를 해 주었었다. 여기에 추가로 url key도 추가로 업데이트해 줄 것이다. post-tweet-form.tsx로 가서 onSubmit() 함수 안의 updateDoc()의 항목을 추가로 업데이트해 준다.

if(file) {
    //S3 upload
    const params = {
        ACL : "public-read",
        Body : file,
        Bucket : S3_BUKKET,
        ContentType: file.type,
        Key : "upload/" + file.name,
    };
    const url = await myBuket.upload(params, (error:any)=>{
        console.log(error);
    }).promise()
    await updateDoc(doc, {
        photo: url.Location,
        photoKey : url.Key  <-- key를 추가로 넣어준다.
    })
}

timeline.tsx에 만들어둔 interface에도 key 항목을 추가해 준다. 

export interface ITweet {
    id: string;
    photo? : string;
    photoKey?: string;  <-- 추가
    tweet: string;
    userId: string;
    username: string;
    createdAt: number;
}

tweet.tsx의 props에 photoKey 항목을 추가해 주고, timelin.tsx의 unsubscribe의 onSnapshot() 코드도 수정해 준다.

unsubscribe = await onSnapshot(tweetsQuery, (snapshot) => {
    const tweets = snapshot.docs.map((doc) => {
        const {tweet, createdAt, userId, username, photo, photoKey} = doc.data();
        return {tweet, createdAt, userId, username, photo, photoKey, id:doc.id}
    });
    console.log("receive real time");
    setTweet(tweets);
});

photoKey를 넘겨받을 수 있다면 이제 삭제코드를 추가해 준다.

if(photo !== "") {
    if(!photoKey) return;
    const params = {
        Bucket: S3_BUKKET,
        Key: photoKey,
    };
    myBuket.deleteObject(params, (error:any)=>{console.log(error)});
}

deleteObject()를 이용해 image를 삭제하며, 파라미터로 버킷과, Key가 필요하다.

이제 삭제를 해 보자.

S3의 이미지도 잘 삭제되었다.

 

이전 글 :

[노마드 코더] ReactJS 트위터 클론 코딩 - Router
[노마드 코더] ReactJS 트위터 클론 코딩 - Authentication
[노마드 코더] ReactJS 트위터 클론 코딩 - Github Login
[노마드 코더] ReactJS 트위터 클론 코딩 - Tweeting

[노마드 코더] ReactJS 트위터 클론 코딩 - Uploading Images with S3
[노마드 코더] ReactJS 트위터 클론 코딩 - Fetching Timeline

 

다음 글 :

[노마드 코더] ReactJS 트위터 클론 코딩 - Edit tweet(code challenge)

[노마드 코더] ReactJS 트위터 클론 코딩 - Pfofile page

반응형