언어/Next.js

[Next 고캠핑] 파이어베이스 좋아요 기능 추가

홍시_코딩기록 2024. 8. 24. 23:16

 

 

 

 

캠핑장 검색화면/ 내 캠핑장/ 캠핑장 상세

 

 


좋아요... 좋아요 기능을 추가해야한다...

어떻게 해야하는지 엄청 찾아봤지만 기본 로컬데이터를 이용해서 관리하기는 어렵고

파이어베이스를 이용한김에 데이터 관리도 로컬이 아닌 파이어베이스로 변경했다.

 

https://hhyj0000.tistory.com/184

 

[Next 고캠핑] 로컬 데이터 파이어베이스로 변경하기

캠핑장 좋아요 기능을 추가하기 위해서 로컬로 데이터를 불러왔던것을 firebase로 바꿨다.오른쪽 상단 더보기를 눌러서 json 가져오기를 누르면 내 데이터가 잘 들어와진 것을 확인할 수 있다.  

hhyj0000.tistory.com

 

 

 

likeList 컬렉션을 만들었다.

여기에 userId를 추가해서 유저의 좋아요 상태 관리를 할 것.

 

https://firebase.google.com/docs/firestore/query-data/get-data?hl=ko

 

Cloud Firestore로 데이터 가져오기  |  Firebase

의견 보내기 Cloud Firestore로 데이터 가져오기 컬렉션을 사용해 정리하기 내 환경설정을 기준으로 콘텐츠를 저장하고 분류하세요. 세 가지 방법으로 Cloud Firestore에 저장된 데이터를 검색할 수 있

firebase.google.com

 

 

const likeListItem = collection(db, "likeList");

- firestore 데이터베이스에 있는 likeList 컬렉션을 참조한다.

* 파이어베이스 데이터 추가

// 좋아요 추가
export const addLike = async (campingItem: ICampingList, userId: string) => {
  try {
    await addDoc(likeListItem, {
      userId,
      campingItem: {
        facltNm: campingItem.facltNm,
        lineIntro: campingItem.lineIntro,
        intro: campingItem.intro,
        addr1: campingItem.addr1,
        firstImageUrl: campingItem.firstImageUrl,
        themaEnvrnCl: campingItem.themaEnvrnCl,
        tel: campingItem.tel,
        contentId: campingItem.contentId,
        lctCl: campingItem.lctCl,
        induty: campingItem.induty,
        doNm: campingItem.doNm,
        sigunguNm: campingItem.sigunguNm,
        direction: campingItem.direction,
        brazierCl: campingItem.brazierCl,
        sbrsCl: campingItem.sbrsCl,
        sbrsEtc: campingItem.sbrsEtc,
        homepage: campingItem.homepage,
        animalCmgCl: campingItem.animalCmgCl,
        tooltip: campingItem.tooltip,
        mapX: campingItem.mapX,
        mapY: campingItem.mapY,
      },
    });
  } catch (error) {
    console.log(error);
  }
};

- addDoc() likeList에 추가할 데이터를 적는다. 

 해당 유저의 데이터만 보여줘야하니까 비교할 수 있게 userId 그리고 들어갈 캠핑 데이터


* 파이어베이스 데이터 삭제

// 좋아요 삭제
export const removeLike = async (docId: string) => {
  try {
    await deleteDoc(doc(db, "likeList", docId));
  } catch (error) {
    console.log(error);
  }
};

 

공식 문서에 나와있는 삭제 방법

await deleteDoc(doc(db, "cities", "DC"));

- 아주 간단! deleteDoc(doc(데이터베이스, "컬렉션 이름", 문서id));

- docId를 매개변수로 불러주고 사용할 때 docId를 불러와서 넣어줄 것이다.

 

 

* 파이어베이스 데이터 가져오기

// 좋아요 리스트
export const getLikeList = async (userId: string) => {
  try {
    const q = query(likeListItem, where("userId", "==", userId));
    const snapshot = await getDocs(q);
    return snapshot.docs.map((doc) => doc.data().campingItem);
  } catch (error) {
    console.log(error);
    return [];
  }
};

- query()로그인 한 유저와 데이터베이스의 userId를 비교해서 해당유저의 데이터만 가져온다.

- getDoc() 쿼리의 결과에 일치하는 문서들을 가져온다.

- 해당 문서의 campingItem 데이터를 추출하여 배열로 반환한다.

- 좋아요 한 캠핑장 리스트를 확인하고 리스트 안에서는 좋아요를 취소해도 새로고침 전까지는 유지하고 싶었기때문에 getDocs()를 사용했다.

 실시간으로 가져오려면 onSnapshot()을 이용해야한다.

 

* 파이어베이스 데이터 실시간으로 가져오기

// 좋아요 상태
export const likeState = (
  userId: string,
  likeUpdate: (likeItems: Array<{ contentId: string; docId: string }>) => void,
): (() => void) | undefined => {
  if (!userId) return;
  try {
    const q = query(likeListItem, where("userId", "==", userId));
    // 실시간 조회로 바꿈
    const unsubscribe = onSnapshot(q, (snapshot) => {
      const updatedLike =
        snapshot.docs.map((doc) => ({
          contentId: doc.data().campingItem.contentId as string,
          docId: doc.id, // Firestore 문서 ID
        })) || [];
      likeUpdate(updatedLike);
    });
    return unsubscribe;
  } catch (error) {
    console.log(error);
  }
};

- 좋아요 상태를 확인하는 코드는 실시간으로 업데이트되어야하니까 (하트 누르기) 실시간으로 업데이트를 해줬다.

- 데이터 삭제를 위해 문서 id와 좋아요를 누른 캠핑장을 구분하기 위해 캠핑장의 contentId를 넘겨줬다.

 

 

사용하기

 

더보기
export default function LikeBtn({
  className,
  onClick,
  campingItem,
  like,
  docId,
}: LikeBtnProps) {
  const { user } = useAuth();

  const onClickLike = async (e: React.MouseEvent<HTMLButtonElement>) => {
    e.stopPropagation();
    if (!user) {
      onClick(e);
    } else {
      if (!like) {
        void addLike(campingItem, user.uid);
      } else {
        void removeLike(docId);
      }
    }
  };

- 로그인을 한 상태가 아닐때는 로그인 페이지로 이동시켜야하기 때문에 onClick이벤트를 props로 넘겨줬다.  

 

interface IPropsCampingList {
  list: ICampingList[];
  className?: string;
  onClick: (item: ICampingList) => void;
}

export default function CampingCard({
  list,
  className,
  onClick,
}: IPropsCampingList) {
  const [likeList, setLikeList] = useState<
    Array<{ contentId: string; docId: string }>
  >([]);
  const { currentModal, openModal } = useModal();
  const router = useRouter();
  const { user } = useAuth();

  const isMounted = useRef(true);

  useEffect(() => {
    isMounted.current = true;
    if (user) {
    // 좋아요 상태 불러오기
      const unsubscribe = likeState(user.uid, (updatedLikes) => {
        setLikeList(updatedLikes);
      });

      // 컴포넌트 언마운트 시 구독 해제
      return () => {
        isMounted.current = false;
        if (unsubscribe) {
          unsubscribe();
        }
      };
    }
  }, []);

  const onClickLike = (e: React.MouseEvent<HTMLButtonElement>) => {
    if (!user) {
      openModal("likeAlert");
    }
  };

  const closeModal = () => {
    router.back();
    setTimeout(() => {
      void router.push("/login");
    }, 100);
  };

  return (
    <>
      {list.map((item: ICampingList) => {
        // 입지 구분 아이콘 리스트
        const icons = item.lctCl ? item.lctCl.split(",") : [];
        const iconList = icons
          .slice(0, 3)
          .concat(
            Array(3 - icons.length > 0 ? 3 - icons.length : 0).fill("없음"),
          );

        // 좋아요 리스트에서 contentId가 캠핑장 contentId하고 같으면 문서의 id를 반환
        const key =
          likeList.find((like) => like.contentId === item.contentId)?.docId ??
          item.contentId;
        // 좋아요 리스트의 contetnId와 캠핑장 contetnId가 같으면 true 반환
        const isLiked = likeList.some(
          (like) => like.contentId === item.contentId,
        );
        return (
          <CardWrap
            key={key}
            className={className}
            onClick={() => {
              onClick(item);
            }}
          >
            <CardInner>
              <ImgBox>
                <LikeBtn
                  className="like"
                  onClick={onClickLike}
                  campingItem={item}
                  like={isLiked}
                  docId={key}
                />
                
                
                ......

- 좋아요 상태를 불러오는데

Can't perform a React state update on an unmounted component. This is a no-op, but it indicates a memory leak in your application. To fix, cancel all subscriptions and asynchronous tasks in a useEffect cleanup function.
오류가 나왔다. 찾아보니 컴포넌트가 언마운트된 상태에서 상태 업데이트를 시도할 때 발생한다고 한다.

https://stackoverflow.com/questions/56442582/react-hooks-cant-perform-a-react-state-update-on-an-unmounted-component
 

React-hooks. Can't perform a React state update on an unmounted component

I get this error: Can't perform a React state update on an unmounted component. This is a no-op, but it indicates a memory leak in your application. To fix, cancel all subscriptions and

stackoverflow.com

- 해결하는 가장 쉬운 방법은 마운트 되었는지 상태를 추적하는 것.

 useRef를 사용해서 isMounted 변수를 선언했고 컴포넌트의 마운트 상태를 추적한다.

 

 


++ 내 캠핑장 페이지네이션을 추가해야한다.

+++ 캠핑 후기게시판 만들기...

 

 

https://nomadcoders.co/nwitter/lectures/4527
https://stackoverflow.com/questions/56442582/react-hooks-cant-perform-a-react-state-update-on-an-unmounted-component
https://velog.io/@cjw020607/Firebase-Cloud-Firestore-Storage-%EC%95%88%EC%9D%98-%EB%8D%B0%EC%9D%B4%ED%84%B0-%EC%82%AD%EC%A0%9C%ED%95%98%EA%B8%B0-deleteDoc-deleteObject
https://velog.io/@phjjj/%ED%8C%8C%EC%9D%B4%EC%96%B4%EB%B2%A0%EC%9D%B4%EC%8A%A4%EB%A5%BC-%EC%9D%B4%EC%9A%A9%ED%95%98%EC%97%AC-%EB%8D%B0%EC%9D%B4%ED%84%B0-%EB%B6%88%EB%9F%AC%EC%98%A4%EA%B8%B0