[개인 프로젝트 ShareLife] SNS 만들기 좋아요 기능 구현(게시글)

규갓 God Gyu·2024년 8월 22일
0

프로젝트

목록 보기
65/81

게시글에 대해 무한스크롤을 더 진행해야하지만, 당장 기능 구현이 급해서 쩔수 없이 좋아요 먼저 구현해보려한다.

likes table과 posts table auth users 연동하기

수파베이스의 posts테이블과 연동을 짓는 likes table을 만들었고,
likes의 post_id가 posts의 id와 연동되게 하였고,
실제 좋아요 누른 사용자는 auth에서 인증된 사용자이므로 auth의 users와 연동지었다.


그리고 유저 아이디 같은 경우 유저가 사라지면 좋아요도 사라지게 Cascade로 연동하였다. 굳이 삭제된 아이디의 좋아요까지 남겨놓을 필요는 없을 것 같아서??


그리고 이걸 활용할진 모르겠지만 realtime 기능도 일단 활성화 하였다.

찾아보니
리얼타임 기능 활성화 시,
즉각적인 업데이트 / 데이터 일관성(항상 최신으로 유지)
그렇다면 게시글도 사용했으면 좋지 않았을까? 싶긴하네 나중에 확인해보자

하트 아이콘은 react 아이콘 라이브러리를 사용해보았다
pnpm add react-icons

그리고 useLike.ts를 추가해서 좋아요 관련 hook을 넣어놨고,

useLike.ts 훅 생성

import { useEffect, useState } from "react"
import { supabase } from "@/lib/supabase"

type LikeCounts = {
  [postId: number]: number
}

type UseLikeProps = {
  currentUserId: string | null
}

const useLike = ({ currentUserId }: UseLikeProps) => {
  const [likedPosts, setLikedPosts] = useState<number[]>([])
  const [likeCounts, setLikeCounts] = useState<LikeCounts>({})

  const fetchUserLikes = async () => {
    if (!currentUserId) return

    const { data: likes, error } = await supabase
      .from("likes")
      .select("post_id")
      .eq("user_id", currentUserId)

    if (error) {
      console.error("Error fetching likes:", error)
    } else {
      setLikedPosts(likes ? likes.map((like) => like.post_id) : [])
    }
  }

  const fetchAllLikeCounts = async () => {
    try {
      const { data: posts, error: postError } = await supabase
        .from("posts")
        .select("id")

      if (postError) {
        console.error("Error fetching posts:", postError)
        return
      }

      const postIds = posts.map((post) => post.id)
      const updatedLikeCounts: LikeCounts = {}

      for (const postId of postIds) {
        const { count, error: countError } = await supabase
          .from("likes")
          .select("*", { count: "exact" })
          .eq("post_id", postId)
          .maybeSingle()

        if (countError) {
          console.error("Error fetching like count for post:", countError)
        } else {
          // Ensure count is a number, default to 0 if undefined
          updatedLikeCounts[postId] = count ?? 0
        }
      }

      setLikeCounts(updatedLikeCounts)
    } catch (error) {
      console.error("Error fetching like counts:", error)
    }
  }

  const toggleLike = async (postId: number) => {
    if (!currentUserId) return

    const existingLike = likedPosts.includes(postId)

    if (existingLike) {
      // Remove like
      const { error } = await supabase
        .from("likes")
        .delete()
        .eq("post_id", postId)
        .eq("user_id", currentUserId)

      if (error) {
        console.error("Error removing like:", error)
      } else {
        setLikedPosts(likedPosts.filter((id) => id !== postId))
        fetchAllLikeCounts() // Update like count
      }
    } else {
      // Add like
      const { error } = await supabase
        .from("likes")
        .insert([{ post_id: postId, user_id: currentUserId }])

      if (error) {
        console.error("Error adding like:", error)
      } else {
        setLikedPosts([...likedPosts, postId])
        fetchAllLikeCounts() // Update like count
      }
    }
  }

  useEffect(() => {
    if (currentUserId) {
      fetchUserLikes()
      fetchAllLikeCounts()
    }
  }, [currentUserId])

  const isPostLikedByUser = (postId: number) => likedPosts.includes(postId)
  const getLikeCountForPost = (postId: number) => likeCounts[postId] || 0

  return {
    toggleLike,
    isPostLikedByUser,
    getLikeCountForPost,
  }
}

export default useLike

일단 처음 선언한 타입부터 매우 생소하였는데

type LikeCounts = {
  [postId: number]: number
}

좋아요 수에 대한 정의 즉, 숫자에 대한 타입을 정의해주는데 이 경우 그냥 좋아요 수만 체크해주는게 아닌, 어떤 게시물에 대한 좋아요 수이기 때문에, postId 자체도 number인데 이러한 키(postId) 와 연관된 값 조차도 number라는 key value 둘다 type을 정의하기 위해 이렇게 표현하였다.
그런데 []: number 이렇게 표현한 이유는 예제를 들면 바로 이해할 수 있다.

const likes: LikeCounts = {
  101: 5,
  102: 10,
  103: 3,
};

// Accessing the number of likes for a specific postconsole.log(likes[101]); // Output: 5

likes[101] 에 접근하면 5가 나오듯이 어떤 객체의 어떤 키값에 접근하기 위해선 []로 접근할 수 있기 때문에 이런식으로 표현해준 것이다.
이렇게 type은 선언해준다면 모든 키를 미리 알지 못해도 항목을 동적으로 추가,업데이트 또는 제거할 수 있는 장점이 있음

type UseLikeProps = {
  currentUserId: string | null
}

두번째 타입은 UseLike의 props에 대한 type명시인데,
여기서 currentUserId는 유저의 로그인한 아이디이다.
근데 어차피 로그인상태가 아니면 메인페이지를 접근조차 못하게 해놨으므로 굳이 null까지 정의해줄 필요는 없을 것 같아
currentUserId : string으로만 수정하였다.

const useLike = ({ currentUserId }: UseLikeProps) => {

그 다음 실제 사용할 useLike에서 인자로 받는 currentUserId를 UseLikeProps 타입으로 정의해준다

const [likedPosts, setLikedPosts] = useState<number[]>([])
const [likeCounts, setLikeCounts] = useState<LikeCounts>({})

이후로 좋아요 누른 게시물 ID 배열, 각 게시물 좋아요 수를 저장하는 객체를 useState로 선언해준다

  const fetchUserLikes = async () => {
    if (!currentUserId) return

    const { data: likes, error } = await supabase
      .from("likes")
      .select("post_id")
      .eq("user_id", currentUserId)

    if (error) {
      console.error("Error fetching likes:", error)
    } else {
      setLikedPosts(likes ? likes.map((like) => like.post_id) : [])
    }
  }

이후 likes에 대한 패치를 하는 비동기 함수인데,
혹시나 사용중인 아이디가 없을 경우에 대한 조건도 넣어줘서 그럴 경우엔 함수를 곧바로 종료 시키고,
수파베이스의 likes테이블에서 post_id 즉 posts의 아이디와 동일시한 게시물의 아이디를 찾아서 user_id가 현재 사용자인 currentUserId와 일치하는지에 대한 조건을 eq에서 찾는다. 그렇게 하면 현재 사용자의 좋아요만 검색할 수 있다.
이 부분 때문에 에러가 뜨고 있는데 이건 나중에 다뤄볼 예정이고, 어쨋든 구현된 내용만 정리해보자면,
그렇게 쿼리에서 문제가 발생되면 console.error에 에러 내용 띄우고, 에러가 없다면 setLikedPosts에서 likes를 변수로 넣어줬는데 이 변수는 쿼리에서 반환된 데이터가 포함되어 있다.
쿼리는 각 개체에 post_id가 있는 개체 배열.
그 likes 배열을 mapping시켜서 각 객체마다 post_id를 추출하면, 그 결과 사용자가 좋아요를 누른 게시물을 나타내는 post_id배열이 나옴. 근데 코드 정리 전에, 구현 중 에러가 많이 떴었는데, 지금껏 나온 트러블 정리 후에 다시 정리해서 내용 적어보겠음.

좋아요 누르면 게시글 카운트에 대한 패칭 에러가 뜸

useLike.ts:53 Error fetching like count for post: 
{code: 'PGRST116', details: 'The result contains 2 rows', hint: null, message: 'JSON object requested, multiple (or no) rows returned'}

이러한 에러가 떴었는데 코드를 살펴보면

  const fetchAllLikeCounts = async () => {
    try {
      const { data: posts, error: postError } = await supabase
        .from("posts")
        .select("id")

      if (postError) {
        console.error("Error fetching posts:", postError)
        return
      }

      const postIds = posts.map((post) => post.id)
      const updatedLikeCounts: LikeCounts = {}

      for (const postId of postIds) {
        const { count, error: countError } = await supabase
          .from("likes")
          .select("*", { count: "exact" })
          .eq("post_id", postId)
          .maybeSingle()

        if (countError) {
          console.error("Error fetching like count for post:", countError)
        } else {
          // Ensure count is a number, default to 0 if undefined
          updatedLikeCounts[postId] = count ?? 0
        }
      }

      setLikeCounts(updatedLikeCounts)
    } catch (error) {
      console.error("Error fetching like counts:", error)
    }
  }

이 부분에서 if(countError)에 대한 에러였다.
지금처럼 .maybeSingle()을 넣어주기 전 .single()을 넣었기에 발생했던 에러였는데, 수파베이스에서 쿼리 결과가 0개 또는 1개의 결과만 있을 때 maybeSingle()을 사용한다고 나와있다.
그냥 single()만 사용했다면 좋아요가 전혀없을 때 null이 아닌 에러가 뜨는데 maybeSingle()을 사용하면 좋아요가 없을 시 null을 반환해준다.
근데? 오히려 에러는 잡았으나 이후에 나올 트러블에 대한 원인이기도 한 코드라고 생각되는데,

다양한 사용자의 좋아요 수가 중복되어 추가되지 않고 토글만 되는 현상

좋아요 디테일한 에러

빈하트의 0likes에 하트를 누르면 1likes가 되면서 하트가 채워져야하는데, 어떤 게시글은 빨간하트 + 0likes에 하트 누르면 흰색하트 + 1likes로 되는 현상이 있었다.
위의 두 트러블이 같은 이유로 진행된다고 해석되는데, 앞서 설명한 maybeSingle() 즉 결과값이 0이나 1밖에 나오지 않게 애초에 세팅을 해놨기 때문에 카운트가 1이상 늘지 않고, 빈하트인데 1likes로 되어 있던지 토글모드로만 되는 현상이 발생하는 것 같다.

결국 정확하게 판단하자면
1. 사용자 전체 좋아요 수가 업데이트 되지 않고 0 혹은1만됨
2. 좋아요를 누르지 않은 하트 아이콘과 실제 좋아요 카운트 숫자와 동기화가 되어 있지 않음

이전에는 좋아요 개수를 가져올 때 대규모로 혹은 여러 동시 업데이트를 처리할 때 안정적이지 않아서 부정확하기도 하였다.

결국 maybeSingle() 혹은 Single() 메서드로 모든 좋아요를 계산하지 않고 한 행에 대해서만 계산하도록 수파베이스에서 제한을 시켰기 때문에 발생했던 문제였고,

   // Fetch like counts for each post
      const likeCountsPromises = postIds.map(async (postId) => {
        const { count, error: countError } = await supabase
          .from("likes")
          .select("*", { count: "exact" })
          .eq("post_id", postId)

여기서 count: 'exact'를 추가해서 정확한 좋아요 전체 개수를 파악할 수 있게 코드를 수정하고 single()메서드는 삭제해서 좋아요가 0이나 1에 머물지 않고 전체 좋아요를 반영할 수 있게 하였다.

낙관적 UI 업데이트 / supabase realtime 구독

좋아요를 구현할 때 자주 겪는? 혹은 이론으로는 알고 있던 부분을 적용을 해줘야하는데,
이전에는 UI가 서버에서 최신 데이터만 가져와서 업데이트해서 표시가 지연 혹은 잘못될 가능성이 있음.
그렇다면?
낙관적 UI 업데이트 접근 방식을 도입해야하는데,
사용자가 좋아요 전환하자마자 UI가 예측해서 작업 성공하겠지 하고 미리 변경사항을 반영시키는 것이다. 결국 맨 처음엔 완벽하게 서버와 값이 일치하지 않더라도 미리 패칭한 값을 보여주는 느낌이다.
그러면서 이번에 수파베이스 like 테이블에서 활성화시킨 realtime도 적용을 좀 해보려고 하는데,
realtime을 적용하면 서로 같은 홈페이지를 바라보면서 다른 사람이 좋아요를 누른것도 바로 확인할 수 있는 정말 실시간 반응을 구현할 수 있는 기능이라 할 수 있다.

낙관적 UI 업데이트
서버 응답 기다리지 않고 즉시 UI 업데이트
UI먼저 업데이트 하고 -> 서버로 전송
혹시나 요청 실패하면 변경 사항 롤백해야할 수도 있음

realtime(실시간 구독)
UI가 서버와 자동 동기화되서 다른 사용자 변경한 내용 실시간 반영
다중 사용자가 일관성 있게 최신 상태를 볼 수 있게 됨
수동 새로고침 혹은 추가로 정보 가져오지 않아도 UI가 업데이트 된 상태로 유지됨

일단 낙관적 UI 업데이트 수정 위해 toggleLike 함수 수정하기

const toggleLike = async (postId: number) => {
    if (!currentUserId) return

    const existingLike = likedPosts.includes(postId)

    // Optimistic UI update
    setLikedPosts((prevLikedPosts) =>
      existingLike
        ? prevLikedPosts.filter((id) => id !== postId)
        : [...prevLikedPosts, postId],
    )

    let error
    if (existingLike) {
      const { error: deleteError } = await supabase
        .from("likes")
        .delete()
        .eq("post_id", postId)
        .eq("user_id", currentUserId)
      error = deleteError
    } else {
      const { error: insertError } = await supabase
        .from("likes")
        .insert([{ post_id: postId, user_id: currentUserId }])
      error = insertError
    }

    if (error) {
      setError(`Error ${existingLike ? "removing" : "adding"} like`)
      console.error(
        `Error ${existingLike ? "removing" : "adding"} like:`,
        error,
      )
      // Revert optimistic update on error
      setLikedPosts((prevLikedPosts) =>
        existingLike
          ? [...prevLikedPosts, postId]
          : prevLikedPosts.filter((id) => id !== postId),
      )
    } else {
      fetchAllLikeCounts() // Update like count
    }
  }

이 부분에서 낙관적 UI 업데이트가 일어나는데,
좀 더 디테일하게 뜯어보면

const toggleLike = async (postId: number) => {
  if (!currentUserId) return

  const existingLike = likedPosts.includes(postId)

  // Optimistic UI update
  setLikedPosts((prevLikedPosts) =>
    existingLike
      ? prevLikedPosts.filter((id) => id !== postId)
      : [...prevLikedPosts, postId],
  )

toggleLike - 특정 게시물에 대해 좋아요 추가 및 제거해주는 함수
if (!currentUserId) return-일단 현재 사용중인 사용자 아이디 없으면 좋아요도 누를 수 없게 조건 제시
existingLike - 현재 사용자가 이미 좋아요를 눌렀는지 확인해줌.

likedPosts

  • useState로 상태관리를 하고 있음
  • 현재 사용자가 좋아요를 누른 게시물의 ID를 담은 상태변수
  • 사용자가 게시물에 좋아요를 누를때마다 업데이트됨

postId

  • 현재 확인하고 있는 게시물의 ID
  • 좋아요 버튼과 상호작용을 위해 postId 매개변수로 받아옴
    includes(postId)
  • includes 메소드는 likedPosts배열 내 특정 요소(postId)가 존재하는지 확인하는 데 사용
  • 만약 이미 postId가 likedPosts 배열에 있으면 true반환해서 이미 좋아요 표시했음을 나타냄
  • postId가 배열에 없으면false 반환, 사용자가 아직 게시물에 좋아요 표시하지 않았음 의미

그래서 이 함수를 작용하면 existing이 true나 false상태가 되므로 좋아요를 제거하거나 좋아요 추가를 할 수 있음

  // Optimistic UI update
  setLikedPosts((prevLikedPosts) =>
    existingLike
      ? prevLikedPosts.filter((id) => id !== postId)
      : [...prevLikedPosts, postId],
  )

이 부분이 낙관적 UI 업데이트 핵심이라고 볼 수 있는데,

setLikedPosts

  • useState로 관리하는 상태 업데이트 기능. 앞서 좋아요 누른 게시물 id 배열 보유한 likedPosts상태업데이트
    prevLikedPosts
  • setState((prevState)=> newState)형식으로 리엑트에서 상태를 업데이트하는 방법, 비동기적으로 처리되며 이전 상태 기반해서 새로운 상태 계산할 때 사용됨
    existingLike
    사용자가 현재 게시물에 좋아요 표시했는지 확인하는 boolean
    그래서 true이면 이미 좋아요를 눌렀으니,
prevLikedPosts.filter((id) => id !== postId)

이 부분을 통해 해당 postId 제거 시킴 - 좋아요 취소
false면 아직 좋아요를 누르지 않았으므로

[...prevLikedPosts, postId]

이전 배열에서 해당 게시글의 postId를 추가시켜줌 - 좋아요 추가됨

즉, 저 부분때문에 딱히 서버와 연동없이도 바로 클라이언트 측에서 변경상태를 바로 변경해줄 수 있는데,

이후 코드를 통해 서버와의 연동까지 진행한다.

그 후 요청 시 문제가 발생되면

      // Revert optimistic update on error
      setLikedPosts((prevLikedPosts) =>
        existingLike
          ? [...prevLikedPosts, postId]
          : prevLikedPosts.filter((id) => id !== postId),
      )

이 코드로 롤백시켜준다.

realtime 구독하기

웹사이트 접속 시 다른 사람이 좋아요를 눌러도 실시간으로 반응할 수 있도록 supabase realtime을 구독하였는데,


이러한 형식을 따라서 채널 구독이 가능하다고 한다.
그러나 실제로 해보려하니 양식이 맞지 않았었고,

이 중에 실제 채택한건 Listen to a specific table인데,
변경 사항을 수신하는 이벤트 핸들러라고 한다.
이걸 적용했어야 했다.

useEffect(() => {
  if (!currentUserId) return

  const fetchInitialData = async () => {
    await fetchUserLikes()
    await fetchAllLikeCounts()
  }

  fetchInitialData()

  // Subscribe to real-time updates
  const channel = supabase
    .channel("likes")
    .on(
      "postgres_changes",
      { event: "*", schema: "public", table: "likes" },
      () => fetchAllLikeCounts(),
    )
    .subscribe()

  return () => {
    channel.unsubscribe()
  }
}, [currentUserId])

동일하게 접속중인 아이디가 있을때, currentUserId가 없으면 바로 리턴시키고, like와 좋아요 개수를 적용시켜준 초기 값을 불러온 이후 코드가 realtime 구독인데,
실제로 채널 구독하는건 메뉴가 아주 다양했었다.

supabase.channel("likes")

이건 코드를 통해 likes 채널을 생성한다는 의미였다. 나는 likes라는 table을 참고하는 줄 알았는데, 실제 likes 테이블을 참조하는게 아닌, 해당 테이블의 변경 사항을 수신하기 위한 특정한 통신 경로를 설정하는 것이다.

.on("postgres_changes",..)

이 부분에서 .on은 메서드이고, 특정 이벤트를 수신하도록 채널을 설정함. 죽 뒤에 있는 postgres_changes라는 이벤트를 수신하도록 되어있음. 이 의미는, 이 likes라는 새로 만든 채널이 PostgreSQL 데이터베이스의 변경사항을 수신한다는 의미.

{ event: "*", schema: "public", table: "likes" }

여기에서 보면 내가 생성했던 table을 적혀있는데,
event:'*' 모든 이벤트(삽입,업데이트,삭제)를 수신한다는 의미, 즉 likes 테이블! 에서 발생하는 모든 변경 사항을 감지함
schema:'public' 애초에 likes가 public 스키마에 있는 테이블임
table:'likes' 좋아요를 담아 둘 테이블

() => fetchAllLikeCounts()

공식문서에서는 payload와 console이 찍혀있었지만, 실제로 내가 구현해야하는건 좋아요 관련된 실시간 변동사항이므로,
콜백함수가 들어가야 되구나를 알면되고,
변경 사항이 감지되면 fetchAllLikeCounts()를 호출해서 현재 좋아요 수를 다시 가져오도록 구현한 채널임.
아직 혼자 하고있어서 이게 실제로 적용되는지는 사실 아직 모르겠음. 나중에 테스트 해볼 예정
.subscribe() 그리고 이걸 꼭 넣어줘야 이 프로젝트가 이 채널에 대해 구독을 시작한다는 의미여서 이게 없으면 이벤트 리스닝이 실제로 시작되지 않음!
결국 변경 사항 발생되면 likes채널에서 fetchAllLikeCounts()를 콜백함수로 호출함!!

update or delete on table 'posts' violates foreign key constraint 'likes_post_id_fkey' on table 'likes'

좋아요 한 게시글을 수정 및 삭제를 진행하려고하니

update or delete on table 'posts' violates foreign key constraint 'likes_post_id_fkey' on table 'likes'

라는 에러가 떴다.

이건 likes 테이블에서 post_id에 대한 외래 키 제약 조건을 on delete cascade로 설정해놔야 한다는 의미인데,
즉, 게시글이 삭제되면 좋아요도 자동으로 삭제되도록 세팅을 했어야 한다는 의미이다.

게시글이 삭제되면 좋아요도 연동되게 삭제되도록 세팅

좋아요 누른 게시글에 대해 수정 및 삭제가 정상적으로 이루어진다 야호~!!

좋아요가 눌러도 오류가 뜨는 이유 - likes 테이블 policy 비활성화

코드와 post_id 외래 키 cascade를 설정한 이후, 오류가 또 떴었다. 그치만 이전에 많은 오류를 범했던 이슈여서, 이번에는 좋아요 에러가 뜨자마자 policy 체크를 못해줬구나 바로 깨달을 수 있었다.
어떤 권한을 줘야할까 고민하다가 모든 유저가 볼 수 있고, 좋아요 넣을 수 있고, 좋아요를 뺄 수 있도록 select,insert,delete에 대한 권한만 넣어주자

좋아요 기능 코드 총 정리

리펙토링은 거치긴 해야겠지만, 그래도 현재까지 잘 구현되는 좋아요 기능에 대해 코드를 상세히 리뷰하는 겸 분석해보면,

import { useEffect, useState } from "react"
import { supabase } from "@/lib/supabase"

type LikeCounts = {
  [postId: number]: number
}

type UseLikeProps = {
  currentUserId: string
}

일단 useLike 훅에 대한 파일이고,
좋아요 갯수에 대한 상태를 여기서 관리할 예정이므로 LikeCounts로 타입 관리를 진행하였는데, 객체 안에는 어떤 게시글에 대한 키 값도 number 그리고 그 결과 값도 좋아요 갯수이므로 number로 처리하기 위해 [postId:number]:number로 넣어줬음.

그리고 UseLikeProps는 실제로 props 받아오는 데이터의 타입을 정해준건데, 이 훅에서는 실제 사용중인 아이디인 currentUserId에 대해 받아오고 있다. 그 값은 string 처리

const useLike = ({ currentUserId }: UseLikeProps) => {
  const [likedPosts, setLikedPosts] = useState<number[]>([])
  const [likeCounts, setLikeCounts] = useState<LikeCounts>({})
  const [error, setError] = useState<string | null>(null)

useLike 훅 안에 모든 좋아요 관련 기능을 담아냈고,
위에서 정의한 currentUserId를 받아오고 있다.(page.tsx에서)

likedPosts - 현재 사용자가 좋아요를 누른 게시물 ID의 배열(여러개일 수 있으니)
likeCounts - 각 게시물에 대한 좋아요 수를 보유하고 있는 객체
오류에 대해선 리펙토링하면서 없어지겠지만 일단은 상태 관리처리

사용자가 좋아요 누른 게시물!을 가져오기

  const fetchUserLikes = async () => {
    if (!currentUserId) return

    const { data: likes, error } = await supabase
      .from("likes")
      .select("post_id")
      .eq("user_id", currentUserId)

    if (error) {
      setError("Error fetching likes")
      console.error("Error fetching likes:", error)
    } else {
      setLikedPosts(likes ? likes.map((like) => like.post_id) : [])
    }
  }

전체 fetch된 좋아요 누른 게시물 목록을 비동기 처리로 가져옴
if (!currentUserId) return 사용중인 유저가 없으면 못가져오게 return

    const { data: likes, error } = await supabase
      .from("likes")
      .select("post_id")
      .eq("user_id", currentUserId)

수파베이스의 likes테이블에 접근해서 post_id 즉 posts 테이블에 연결된 실제 게시글의 id = 게시글을 찾아내서 이 게시글과 관련되어 user_id가 있는지 없는지 즉 내가 좋아요를 눌렀는지 안눌렀는지를 가져옴
다른 사용자의 좋아요 데이터는 신경쓰지 않음

    if (error) {
      setError("Error fetching likes")
      console.error("Error fetching likes:", error)
    } else {
      setLikedPosts(likes ? likes.map((like) => like.post_id) : [])
    }

이후 에러가 있으면 에러된걸 띄워주고,
잘 가져와졌으면 내가 좋아요 누른 게시글에 업데이트 시켜줌
즉, 본인의 좋아요에 대한 것만 처리해주는 함수

모든 게시물에 대한 좋아요 수 가져오기

  const fetchAllLikeCounts = async () => {
    try {
      const { data: posts, error: postError } = await supabase
        .from("posts")
        .select("id")

      if (postError) {
        setError("Error fetching posts")
        console.error("Error fetching posts:", postError)
        return
      }

      const postIds = posts.map((post) => post.id)
      const updatedLikeCounts: LikeCounts = {}

      // Fetch like counts for each post
      const likeCountsPromises = postIds.map(async (postId) => {
        const { count, error: countError } = await supabase
          .from("likes")
          .select("*", { count: "exact" })
          .eq("post_id", postId)

        if (countError) {
          console.error("Error fetching like count for post:", countError)
          return { postId, count: 0 } // Default to 0 on error
        }
        return { postId, count: count || 0 }
      })

      const likeCountsResults = await Promise.all(likeCountsPromises)
      likeCountsResults.forEach(({ postId, count }) => {
        updatedLikeCounts[postId] = count
      })

      setLikeCounts(updatedLikeCounts)
    } catch (error) {
      setError("Error fetching like counts")
      console.error("Error fetching like counts:", error)
    }
  }

다른 사용자가 어떤 게시물을 바라봐도 좋아요 수를 전체적으로 확인할 수 있기 위한 로직이고,

      const { data: posts, error: postError } = await supabase
        .from("posts")
        .select("id")

일단 수파베이스의 게시글에 대한 아이디를 전부 불러온다.

      if (postError) {
        setError("Error fetching posts")
        console.error("Error fetching posts:", postError)
        return
      }

혹여나 가져오는데 에러가 뜰 경우에 대한 에러 처리 해주고,

      const postIds = posts.map((post) => post.id)
      const updatedLikeCounts: LikeCounts = {}

가져온 모든 게시글에 대해 맵핑해서 하나하나 뽑아준걸 postIds에 담아준다
updatedLikeCounts는 각각 게시글에 대한 좋아요 수를 저장하기 위해 빈 객체 선언

      // Fetch like counts for each post
      const likeCountsPromises = postIds.map(async (postId) => {
        const { count, error: countError } = await supabase
          .from("likes")
          .select("*", { count: "exact" })
          .eq("post_id", postId)

postIds는 각각 모든 게시물의 id를 담은 배열인데 그걸 다시한번 비동기로 새로운 mapping을 시켜주는데, 수파베이스의 likes테이블에서 모든 행의 count를 정확하게 가져오는데 조건은 postId와 동일한 post_id를 가져온다.
이 의미는 즉, 전체 게시글 중 좋아요를 가지고 있는 게시글을 불러온다고 볼 수 있다.

        if (countError) {
          console.error("Error fetching like count for post:", countError)
          return { postId, count: 0 } // Default to 0 on error
        }
        return { postId, count: count || 0 }
      })

      const likeCountsResults = await Promise.all(likeCountsPromises)
      likeCountsResults.forEach(({ postId, count }) => {
        updatedLikeCounts[postId] = count
      })

      setLikeCounts(updatedLikeCounts)

이후 혹여나 에러가 있을 경우 에러표시를 해주면서, 기존의 postId와 count를 0으로 반환한다.
return { postId, count: count || 0 }
근데 좋아요가 존재한다면, 해당 게시물 postId와 그 count수 만약 없으면 0을 반환한다.

      const likeCountsResults = await Promise.all(likeCountsPromises)
      likeCountsResults.forEach(({ postId, count }) => {
        updatedLikeCounts[postId] = count
      })

그리고 Promise.all을 사용하면 모든 Promise가 완료될 때까지 기다린 후 likeCountsResults에 결과를 저장하는데,
앞서 likeCountsPromises 결과에 대해 모든 비동기 처리가 끝난 그 결과 값을 저장한다.

      likeCountsResults.forEach(({ postId, count }) => {
        updatedLikeCounts[postId] = count
      })

이후 forEach메서드를 통해 게시글 id와 count수를 추출해서 빈객체였던 updatedLikeCounts에 key value 게시글:카운트 수 이렇게 저장시켜준다
이후

      setLikeCounts(updatedLikeCounts)

전체 카운트 수에 대해 업데이트해서 상태변화를 진행시켜준다

catch (error) {
      setError("Error fetching like counts")
      console.error("Error fetching like counts:", error)
    }
  }

에러가 떴을 땐 에러처리!

좋아요 기능 전환

 const toggleLike = async (postId: number) => {
    if (!currentUserId) return

    const existingLike = likedPosts.includes(postId)

    // Optimistic UI update
    setLikedPosts((prevLikedPosts) =>
      existingLike
        ? prevLikedPosts.filter((id) => id !== postId)
        : [...prevLikedPosts, postId],
    )

    let error
    if (existingLike) {
      const { error: deleteError } = await supabase
        .from("likes")
        .delete()
        .eq("post_id", postId)
        .eq("user_id", currentUserId)
      error = deleteError
    } else {
      const { error: insertError } = await supabase
        .from("likes")
        .insert([{ post_id: postId, user_id: currentUserId }])
      error = insertError
    }

    if (error) {
      setError(`Error ${existingLike ? "removing" : "adding"} like`)
      console.error(
        `Error ${existingLike ? "removing" : "adding"} like:`,
        error,
      )
      // Revert optimistic update on error
      setLikedPosts((prevLikedPosts) =>
        existingLike
          ? [...prevLikedPosts, postId]
          : prevLikedPosts.filter((id) => id !== postId),
      )
    } else {
      fetchAllLikeCounts() // Update like count
    }
  }

특정 게시물에 대한 좋아요 전환을 처리해주는 비동기 함수를 정의

낙관적 UI로 미리 업데이트 시켜놓고~(앞서 설명했음 코드는)

코드를 보다보니 toast를 활용하지 못하고 있어서

  const toggleLike = async (postId: number) => {
    if (!currentUserId) return

    const existingLike = likedPosts.includes(postId)

    // Optimistic UI update
    setLikedPosts((prevLikedPosts) =>
      existingLike
        ? prevLikedPosts.filter((id) => id !== postId)
        : [...prevLikedPosts, postId],
    )

    if (existingLike) {
      const { error: deleteError } = await supabase
        .from("likes")
        .delete()
        .eq("post_id", postId)
        .eq("user_id", currentUserId)
      if (deleteError) {
        toast({
          title: "좋아요 삭제 중 오류가 발생했습니다.",
          description: deleteError.message,
        })
      } else {
        fetchAllLikeCounts() // Update like count
      }
    } else {
      const { error: insertError } = await supabase
        .from("likes")
        .insert([{ post_id: postId, user_id: currentUserId }])
      if (insertError) {
        toast({
          title: "좋아요 추가 중 오류가 발생했습니다.",
          description: insertError.message,
        })
      } else {
        fetchAllLikeCounts() // Update like count
      }
    }
  }

이렇게 수정하였다.
그래서 결국 이 함수를 실행하면 좋아요가 추가하고 삭제하는 기능을 담게 된다. 물론 낙관적 UI 업데이트로 먼저 선실행!

초기 데이터 가져오기 및 실시간 업데이트

useEffect(() => {
    if (!currentUserId) return

    const fetchInitialData = async () => {
      await fetchUserLikes()
      await fetchAllLikeCounts()
    }

    fetchInitialData()

    // Subscribe to real-time updates
    const channel = supabase
      .channel("likes")
      .on(
        "postgres_changes",
        { event: "*", schema: "public", table: "likes" },
        () => fetchAllLikeCounts(),
      )
      .subscribe()

    return () => {
      channel.unsubscribe()
    }
  }, [currentUserId])

useEffect를 활용해 currentUserId가 있으면 마운트되며 실행되는데,

    const fetchInitialData = async () => {
      await fetchUserLikes()
      await fetchAllLikeCounts()
    }
        fetchInitialData()

사용자가 좋아요한걸 나타내는 함수와 모든 좋아요수를 보여주는 함수를 실행시킨다.

이후는 realtime 활용한 코드고

    return () => {
      channel.unsubscribe()
    }

언마운트 될 때 구독도 해제한다.

const isPostLikedByUser = (postId: number) => likedPosts.includes(postId)
  const getLikeCountForPost = (postId: number) => likeCounts[postId] || 0

마지막으로 두 함수는 게시물의 ID가 likedPosts에 있는지 체크해서 하트 아이콘을 채워야하는지 표시 위해 UI 렌더링을 위해 있고, 현재 좋아요 수를 UI로 보여주기 위해 아래에서 선언

profile
웹 개발자 되고 시포용

0개의 댓글