나의 개발 기록지

개인 포트폴리오 고민 상담소 #9 본문

개인 프로젝트(고민 상담소)

개인 포트폴리오 고민 상담소 #9

해기97 2023. 9. 7. 18:03

저번에 블로그를 신경 안쓰고 살다가 왕창 만들고 수정한 것에 대해.. 기록을 남기자..

 


일단 저번 포스팅의 마지막이었던 댓글 기능에 대해서

 

일단 댓글 기능은 완벽하게 작동하고 있으며 저번 포스팅에서 처럼 스토어에 추가 및 삭제를 하여 화면에 바로바로 반영될 수 있게 끔 완성을 해주었다. 그리고 그 과정에서 글 쓰기 기능을 담아 둔 커스텀 훅이었던 useWriteBtn 이라는 파일을 useWrite로 변경해줌과 동시에 내부 코드도 수정이 조금 있었는데

 

import { addDoc, collection } from "firebase/firestore/lite";
import { db } from "../api/firebase";

import { userType } from "../types/userTypes";
import { postWriteType } from "../types/writeTypes";

type postWritePropsType = {
    writeValue: postWriteType,
    userState: userType,
    date: number
}

type commentWritePropsType = {
    userState: userType,
    commentValue: string,
    detailCollection: string,
    detailId: string,
    date: number,
    commentName: string,
}
export default function useWrite(){

    const postWrite = async ({
        writeValue,
        userState,
        date
    }:postWritePropsType) =>{
        try{
            const docRef = await addDoc(collection(db,writeValue.category),{
                title: writeValue.title,
                content: writeValue.content,
                category: writeValue.category,
                userUid: userState.userUid,
                date: date
            })

            return docRef
        } catch(error){
            console.error(`${error}에러 발생 했따`)
        }
    }

    const commentWrite = async ({
        userState,
        commentValue,
        detailCollection,
        detailId,
        date,
        commentName
    }:commentWritePropsType) =>{
        try{
            const docRef = await addDoc(collection(db,detailCollection,detailId,'comment'),{
                comment: commentValue,
                userUid: userState.userUid,
                date: date,
                commentName: commentName
            })

            return docRef
        } catch(error){
            console.error(`${error}에러 발생 했따`)

        }
    }

    return{postWrite,commentWrite}
}

이 처럼 useWrite에서 게시글 생성과 댓글 생성 두 함수를 만들어 둔 뒤 리턴값으로 두 함수를 뱉어서 사용하고있다.

 

두 함수는 받아올 파라미터가 다르기에 전 처럼 한 함수로 관리하려하니 옵셔널파라미터를 사용해야했고 그 과정이 조금 별로인 듯 하여 차라리 이런 식으로 나누어주는 걸 택했다.

 

그리고 이제 댓글 작성 버튼의 함수를 한번 살펴보면

 

const handelCreateBtn = async () =>{

        if(commentValue.length === 0){
            alert('댓글을 입력해주세요.')
        }else {
            const userUid = userState.userUid;
            const userCommentData = commentData.find((item) => item.userUid === userUid);
            let commentName;
            if (userCommentData) {
              // 이미 작성한 댓글이 있는 경우
              commentName = userCommentData.commentName;
            } else {
              // 새로운 댓글을 작성하는 경우
              commentName = anonymousAnimal(); // 새로운 commentName 생성
              // Firestore에 새로운 댓글 추가 코드 (필요한 부분 추가)
            }
            createComment.commentWrite({
                detailCollection: category,
                detailId: id,
                commentValue: commentValue,
                userState: userState,
                date: timestamp,
                commentName: commentName,
              });
            const newComment = {
              comment: commentValue,
              userUid: userUid,
              date: timestamp,
              commentName: commentName,
            };
            dispatch(setCommentData([...commentData, newComment])); // 댓글 작성하면 스토어에 추가
            dispatch(onChangeCommentValue('')); // 댓글 작성 시 인풋 초기화
          }
        }

이러한 코드가 생성이 되었는데 중간에 commentName은 추후 아래에서 작성할 것이고

여기서 확인해볼 점은 dispatch 부분을 확인하면 될 것 같다. 댓글 작성 시 스토어에 기존에 댓글 데이터와 새로운 댓글 데이터를 추가해줌으로써 화면에 바로바로 추가가 되는 작업이 완성이 된 것 이다.

 

삭제 부분도 한번 살펴보면

 

import { collection, deleteDoc, doc, getDocs } from "firebase/firestore/lite";
import { db } from "../api/firebase";

type useDeleteCommentBtnProps = {
    category: string,
    id: string,
    commentCollection?: string,
    commentId?: string
}

type useDeletePostBtnProps = {
    category: string,
    id: string,
}

export default function useDeleteBtn(){

        const commentDelete = async ({
            category,
            id,
            commentCollection,
            commentId}: useDeleteCommentBtnProps) => {
                //댓글 삭제 함수
            const commentRef = `${category}/${id}/${commentCollection}/${commentId}`
            const commentDocRef = doc(db,commentRef)

            try{
                await deleteDoc(commentDocRef)
            }catch(error){
                console.error(error)
            }
        }

        const postDelete = async ({category,id}:useDeletePostBtnProps) =>{
            // 게시글 삭제 함수
            const docRef = doc(db,category,id);
            try {
                // 게시글 내의 댓글 컬렉션 참조
                const commentsCollectionRef = collection(db, `${category}/${id}/comment`);
                const commentsQuerySnapshot = await getDocs(commentsCollectionRef);

                // 댓글 삭제 comment 컬렉션 내부의 댓글들을 모두 삭제해야 컬렉션이 삭제됨
                commentsQuerySnapshot.forEach(async commentDoc => {
                  await deleteDoc(commentDoc.ref);
                });

                // 문서 삭제 시 컬렉션도 같이 삭제가 되는 듯
                await deleteDoc(docRef);
              } catch (error) {
                console.error(error);
              }
        }

        return {commentDelete,postDelete}
}

일단 파이어베이스의 삭제 기능을 담고있는 커스텀 훅 내부도 댓글과 게시글 두 함수를 나누어 작성을 해주었고

 

이 부분에서 살짝 어려움을 겪었었는데 나의 파이어베이스 구조는

 

이러한 구조로 데이터베이스가 구성이 되어있는데 이 과정에서 나는 게시글을 삭제하면 댓글 컬렉션까지 통째로 사라지는 줄 알았는데 그게 아니었다.

 

댓글 컬렉션 내 문서를 모두 삭제를 해야 댓글 컬렉션도 사라지고 게시글 문서도 사라지는 것이었고 댓글 컬렉션 내 문서를

모두 삭제하지 않으면 파이어베이스에 덩그러니 게시글(문서)가 남아있으며 그 내부에 댓글(컬렉션)과 댓글(문서)가 그대로 남아있는 상황이 발생했었다.

 

이러한 작업을 해주기 위해 댓글 커스텀 훅 내부를 수정해준 부분도 있으며 위 코드를 확인해보면 그에 대한 코드가 작성이 되어있는 걸 볼 수 있다.

 

이제 삭제 버튼에 달아 준 함수 내부도 한번 살펴보면

 

const removeComment = (commentId: string,dataUid: string) =>{

        if(userUid === dataUid){
        setRemoveCommentPopup(true)
        setRemoveCommentId(commentId)
        }else{
            alert('본인의 댓글이 아닙니다.')
        }
    }

    const deleteBtn = useDeleteBtn();

    const deleteCommentDoc = async () =>{
        await deleteBtn.commentDelete({
            category,
            id,
            commentCollection:'comment',
            commentId:removeCommentId})
        setRemoveCommentId('')
        setRemoveCommentPopup(false)
        dispatch(removeCommentData(removeCommentId))
    }
<ul>
            {sortedCommentData.map((data: commentItemType)=>{
                return(
                    <li key={data.id}>
                        <div>

                            <span>{data.commentName} {getDate(data.date)}</span>

                            <p>{data.comment}</p>

                            {userUid === data.userUid ?
                                <button
                                onClick={()=>removeComment(data.id,data.userUid)}>
                                <FontAwesomeIcon icon={faXmark}/> </button> : null}

                        </div>
                    </li>
                )
            })}
            {removeCommentPopup ?
                <Popup
                text="삭제할거임?"
                buttonText="삭제"
                setState={setRemoveCommentPopup}
                button={deleteCommentDoc} /> : null}
        </ul>

위 처럼 removeComment를 눌렀을 때 커스텀 팝업을 띄워주고 커스텀 팝업에 deleteCommentDoc 함수를 보내주었다.

 

deleteCommentDoc 함수를 살펴보면 dispatch를 사용하여 store 내부에 작성 되어 있는 아래의 코드를 사용하여

removeCommentData:(state,action)=>{
            const removePostId = action.payload
            state.commentData = state.commentData.filter(comment => comment.id !== removePostId );
        }

같은 아이디 값을 비교한 후 삭제해주는 기능을 완성해주었다.

 

이로써 댓글에 관한 기능은 모두 끝이났고.. (사실 끝 아님)

 


다음으로는.. 익명 게시판 답게 댓글 작성자들의 이름을 익명으로 동물이름으로 나타나게 끔 만들어 주었다.

 

이 부분에서는 GPT의 도움을 꽤나 많이 받았다. (GPT 최고,,)

 

어떤 식으로 작동해야 유저의 이름이 동물 이름으로 나타날지는 충분히 작성할 순 있지만

댓글 내에서는 처음 작성할 때 랜덤으로 동물 이름을 부여받고 두번, 세번, 네번 작성할 때에도 첫번째 부여받은 동물이름이 계속 나타나야한다.

 

이 부분에서 어떤 식으로 할지 꽤나 고민했었는데 여러 방법들이 생각이 났었다.

 

첫번째 방법으로 파이어베이스에 데이터베이스 중 댓글(컬렉션)의 모든 곳을 돌아서 userUid를 찾고 로그인한 유저의 Uid와 같은 댓글(문서) 가 있다면 그곳에 저장되어 있는 동물이름을 사용하기로 하자. 만약 없다면 새로운 동물이름을 부여해주자 였고

 

두번째 방법으로는 결국 댓글 데이터는 store에 저장이 되니까 그곳에 있는 데이터를 비교해보면 되는 것이었다.

첫번째 방법과 유사하게 작동이 되며 store에 있는 데이터를 훑은 뒤 userUid와 같은 것이 있다면 그곳에 저장된 동물이름을 사용하면 되는 것이다.

 

이처럼 구현할 방법은 생각났지만 구현 단계에서는 GPT의 도움을 받았다.

 

export default function anonymousAnimal(){
    const animals = ['원숭이','코끼리','고양이','강아지',
    '앵무새','해파리','거북이','고라니','두더지','구렁이',
'독수리','얼룩말','호랑이','도마뱀','코브라','살모사','병아리'
,'오징어','고등어','송사리','메추리','오골계','도롱뇽']

    const randomAnimalName = animals[Math.floor(Math.random() * animals.length)];

    return randomAnimalName
}

일단 랜덤한 동물 이름을 뱉어 줄 함수를 하나 미리 만들어주고

 

아까 본 댓글 작성 함수에서

 

const handelCreateBtn = async () =>{

        if(commentValue.trim().length === 0){
            alert('댓글을 입력해주세요.')
        }else {
            const userUid = userState.userUid;
            const userCommentData = commentData.find((item) => item.userUid === userUid);
            let commentName;
            if (userCommentData) {
              // 이미 작성한 댓글이 있는 경우
              commentName = userCommentData.commentName;
            } else {
              // 새로운 댓글을 작성하는 경우
              commentName = anonymousAnimal(); // 새로운 commentName 생성
              // Firestore에 새로운 댓글 추가 코드 (필요한 부분 추가)
            }
            createComment.commentWrite({
                detailCollection: category,
                detailId: id,
                commentValue: commentValue,
                userState: userState,
                date: timestamp,
                commentName: commentName,
              });
            const newComment = {
              comment: commentValue,
              userUid: userUid,
              date: timestamp,
              commentName: commentName,
            };
            dispatch(setCommentData([...commentData, newComment])); // 댓글 작성하면 스토어에 추가
            dispatch(onChangeCommentValue('')); // 댓글 작성 시 인풋 초기화
          }
        }

댓글을 작성할 때 조건문을 통해 여러 조건을 거치는데 일단 가장 처음 거치게 될 조건문인

 

댓글 input의 value.length가 0이라면 (공백임을 알기위해, 스페이스바로 통과하는 것을 막기위해 trim() 도 사용)

alert을 사용하여 경고창을 띄워주고

 

그게아니라면 댓글을 작성해주는데

이곳에서 find함수를 통해 스토어에있는 commentData를 한번 전체를 훑은 뒤 로그인 한 유저의 uid와 일치하는 아이템이 있는지 찾아주고

 

일치하는 아이템이 있다 라는 조건을 거친 뒤

일치하는 아이템 내에 있는 commentName을 사용할 것이고

 

일치하는 아이템이 없다면? 새롭게 랜덤한 이름을 부여받게 될 것이다.

 

이로써 이름이 랜덤한 동물로 일정하게 나타나는 코드는 완성이 되었다.

 

이름 테스트하다가 발견한 버그

이름은 정상적으로 한번 정해진 이름으로 계속 나타나는걸 볼 수 있는데 여기서 에러를 발견했다.

 

추후에 새로고침 하고나서 삭제하면 원하는 댓글만 제대로 삭제가 되는데 처음 여러개를 작성하고 하나만 삭제하면

화면에 처음 여러개 작성한 댓글들이 모두 사라지고 새로고침하면 삭제했던 댓글도 지워지지않고 그대로 남아있는..

 

말 그대로 삭제 버튼 함수가 고장난 것 같았다..

 

이 부분을 고치기위해 하나씩 뜯어보기로 했다.

 

뜯어보니 문제가 있는 곳이 몇 군데가 더 있었다. 처음으로는

 

const sortedCommentData = [...commentData];

    sortedCommentData.sort((a, b) => b.date - a.date);


    return(
        <ul>
            {sortedCommentData.map((data: commentItemType)=>{
                return(
                    <li key={data.id}>
                        <div>

                            <span>{data.commentName} {getDate(data.date)}</span>

                            <p>{data.comment}</p>

                            {userUid === data.userUid ?
                                <button
                                onClick={()=>removeComment(data.id,data.userUid)}>
                                <FontAwesomeIcon icon={faXmark}/> </button> : null}

                        </div>
                    </li>
                )
            })}
            {removeCommentPopup ?
                <Popup
                text="삭제할거임?"
                buttonText="삭제"
                setState={setRemoveCommentPopup}
                button={deleteCommentDoc} /> : null}
        </ul>
    )

이 부분에서 댓글을 날짜 순으로 정렬해주는 코드가 useEffect 없이 사용이되어 input에 글을 입력할 때 마다 계속 호출이 되는 것이었다. 이 경우는 파이어베이스에서 읽기 데이터를 계속 사용하는 경우가 발생하니 좋지않은 상태였다..

 

이 부분을 수정해주면서 또 useEffect를 잘못 사용해서 파이어베이스 할당량을 다 써버리는 사고가 발생했다..

 

또 한번 코드를 잘 사용해야.. 이러한 대참사를,, 불러오지않는다는 걸 뼈저리게 느끼게됐다.

 

거의 작업이 막바지 단계인데 여기서 이러한 에러가 발생해버렸으니 잠시 작업이 모두 중단이 되어버린 상태이다.

분명 일일 할당량 자정에 초기화된다고 했던거같은데 초기화가 되지않고있다.. 시간이 별로 없는데 ㅠ..

 


할당량 문제가 아니었나보다

 

할당량 문제가 아니라 아마 노트북으로 작업을 했었는데 전에 작업하던 파이어베이스 프로젝트의 캐시값이 남아있었는지 그로 인해 자꾸 옛날 프로젝트에서 데이터를 불러오려 해 에러가 발생했던 것이었다.

 

지푸라기라도 잡는 심정으로.. 깃에 업로드 해놓았으니 컴퓨터로 그걸 받아와서 작업을 해보았는데 바로 정상적으로 작동을 했다..

 

지원하고싶은 회사가 내일이 마감이라 일단 후다닥 작업을 끝냈다..

 

하루만에 거의 모든걸 끝낸 것 같다.

 

메인 페이지에 최근 글을 5개씩 보여주는 것과 미완성이었던 잡담소 카테고리를 완성해주고

 

익명게시판과 비밀게시판 그리고 잡담소 각각 성격이 다른 카테고리라 받아오는 정보도 달라야했기에 그걸 수정해주었다.

 

익명게시판과 비밀게시판의 경우는 작성자의 이름을 비공개로 해주어야하기에 익명의 작성자로 나타나게끔 해주었고

잡담소는 유저가 설정한 닉네임으로 나타나게 되어있다.

 

그리고 익명게시판의 댓글 또한 모두가 익명이기에 동물이름으로 표시가 되어야하고 잡담소의 경우는 유저의 닉네임으로 표시가 되게끔 만들어주었다.

 

유저의 닉네임 그리고 프로필 사진을 수정할 수 있게 해주었고 댓글 및 게시글 작성 시 로그인 상태가 아니라면 경고창을 띄워주게 만들어 주었고..

 

그 외 자잘하게 수정한 것들이 꽤나 많다.

 

사실 아직 100% 완성은 아니다.. 자잘한 에러가 많이 존재하며 타입 또한 급해서 any로 잡아준 것이 꽤 남아있다.

 

이력서를 냈다해서.. 끝낼건 아닌 것 같고 이건 끝까지 마무리를 제대로 지어보고싶다.

 

포스팅을 중간에 쉬어버리는 바람에 모든 과정을 담아두진 못했지만.. 다음에 기회가 되면 다시 작성을 하고싶다.

 

또한 이번에 만들어 보며 새롭게 배웠던 부분이나 파이어베이스에 대한 기록은 또 따로 작성해두면 좋을 것 같다.