Next.js 캐럿마켓 클론코딩(OPTIMISTIC UPDATES)

짜스의 하루 ·2024년 8월 26일

Introduction

동네 생활 탭 -> 유저가 다른 유저들의 게시물을 읽을 수 있고, 게시물을 클릭하면 전체 게시물을 볼 수 있고, 게시물의 조회수를 추적하고 좋아요를 누르거나 댓글을 남길 수도 있음.
--> 모두 database에서 무언가를 바꾸는 mutation이라는 것 , 이 섹션의 핵심이고, 핵심 내용은 optimistic responses를 의미한다.
(mutation(댓글을 남기거나, 좋아요를 누르거나)이 엄청 빠르게 보이도록 하는 방법을 배우게 될 것임. )

먼저

post -> 글을 올리면, 그 글에 comment를 남길 수 있음
(post에는 여러 comment들이 달릴 수 있음)
-> post에 좋아요를 누를 수 있지만, 한 유저당 한개의 post에는 1개의 좋아요만 달릴 수 있다.

이렇게 기본적인 설정을 하고, model를 생성해보자

model Post {
  id Int @id @default(autoincrement())
  title String
  description String?
  views Int @default(0)//field에서 조회수를 카운트 -> 0부터 시작
  created_at DateTime @default(now())
  updated_at DateTime @updatedAt
  user User @relation(fields: [userId], references: [id],onDelete: Cascade)
  userId Int

  Comments Comment[]
  Like Like[]

}
  • Post가 어떤 Comment들이 자신을 가리키고 있는지를 알게 된다.

  • 이렇게 하면 Post model이 User를 가리키는 relationShip이 있다고 하는 말해주는 것이다. 하지만, Post model이 User를 가리키는 건, User도 자신을 가리키는 모든 Post를 알 수 있어야 한다는 말이기도 한다.

이렇게 User안에 posts를 넣어서 User가 작성한 모든 Post를 가져올 수 있다. --> relationship을 사용해서 해당 User를 가리키는 모든 Post를 가져온다는 것을 의미한다.

model Comment {
  id Int @id @default(autoincrement())
  payload String

  user User @relation(fields: [userId], references: [id],onDelete: Cascade)
  userId Int

  post Post @relation(fields: [postId], references: [id],onDelete: Cascade)
  postId Int
  created_at DateTime @default(now())
  updated_at DateTime @updatedAt

}

comment 도 역시, 한명의 user가 여러 comment를 작성할 수 있고, 하나의 post에 여러 comment를 작성할 수 있다. -> user와 post를 연결시켜 준 것이다.

model Like {
 @@id(name : "id", [userId, postId])

  created_at DateTime @default(now())
  updated_at DateTime @updatedAt

  user User @relation(fields: [userId], references: [id],onDelete: Cascade)
  userId Int

  post Post @relation(fields: [postId], references: [id],onDelete: Cascade)
  postId Int


}

하나의 Post에 한명의 user는 하나의 Like를 누를 수 있다.

좋아요 수에는 Like model를 사용하고 조회수는 그냥 integer로 카운트 하는 이유 -> 유저가 게시물을 여러번 보는 것은 상관이 없지만, 좋아요를 중복으로 누를 수는 없어야 하기 때문이다.

@@id(name: "id", [userId, postId]) 는 Prisma의 모델 정의에서 사용되는 구문으로, 복합(primary) 키를 설정하는 것을 의미한다

@@id : 이 구문은 모델에 복합(primary) 키를 정의할 때 사용된다. @@는 모델 레벨에서 사용하는 어노테이션을 나타낸다.

name: "id" : 복합 키에 이름을 부여하는 부분이다. 여기서는 복합 키의 이름을 "id"로 지정한 것이다. 이 이름은 데이터베이스 스키마에서 해당 키를 참조할 때 사용된다.

[userId, postId] : 복합(primary) 키를 구성하는 필드들을 지정한다. 이 경우, userId와 postId 두 필드를 조합하여 하나의 복합 키를 만든다. 이 복합 키는 Like 모델의 각 레코드를 고유하게 식별하는 데 사용된다.

⭐️중요⭐️

Prisma에서 일대다(1) 관계를 정의할 때, 다(N) 쪽에 외래 키(foreign key) 를 설정하고, 일(1) 쪽에서는 해당 관계를 나타내는 배열을 설정하게 된다.

여기서 확인할 수 있듯이, 하나의 Post에서 여러개의 Like(좋아요) 를 받을 수 있다. 그러기 때문에 Post에서는 Like의 배열을, Like에는 key를 설정해 둔것이다.

postId Int: Like 모델에서 이 필드는 어떤 포스트에 대한 좋아요인지 식별하는 외래 키이다.
즉, Like 모델의 postId 필드는 어떤 Post에 대한 좋아요인지 식별하며, 여러 Like가 같은 postId를 가질 수 있기 때문에, 하나의 Post에 대해 여러 Like를 받을 수 있습니다.


See Posts

이렇게 화면을 꾸며볼 예정이다!

import db from '@/lib/db';
import { formatToTimeAgo } from '@/lib/utils';
import {
  ChatBubbleBottomCenterIcon,
  HandThumbUpIcon,
} from '@heroicons/react/24/outline';
import Link from 'next/link';

async function getPosts() {
  const posts = await db.post.findMany({
    select: {
      id: true,
      title: true,
      description: true,
      views: true,
      created_at: true,
      _count: {
        select: {
          Comments: true,
          Like: true,
        },
      }, 
    },
  });
  return posts;
}

export const metadata = {
  title: '동네생활',
};

export default async function Chat() {
  const posts = await getPosts();
  console.log(posts);
  return (
    <div className="p-5 flex flex-col">
      {posts.map((post) => (
        <Link
          key={post.id}
          href={`/posts/${post.id}`}
          className="pb-5 mb-5 border-b border-neutral-500 text-neutral-400 flex flex-col last:pb-0 last:border-b-0
          gap-2"
        >
          <h2 className="text-white text-lg font-semibold">{post.title}</h2>
          <p>{post.description}</p>
          <div className="flex items-center justify-between text-sm">
            <div className="flex gap-4 items-center">
              <span>{formatToTimeAgo(post.created_at.toString())}</span>
              <span></span>
              <span>조회 {post.views}</span>
            </div>
            <div className="flex gap-4 items-center *:flex *:gap-1 *:items-center">
              <span>
                <HandThumbUpIcon className="size-4" />
                {post._count.Like}
              </span>
              <span>
                <ChatBubbleBottomCenterIcon className="size-4" />
                {post._count.Comments}
              </span>
            </div>
          </div>
        </Link>
      ))}
    </div>
  );
}

여기서 getPosts() 를 잘 살펴보면,

_count: {
       select: {
         Comments: true,
         Like: true,
       },
     }

처음 보는 _count 를 살펴볼 수 있는데, --> reverse relationship을 계산할 수 있는데, _count 를 사용하면 Post를 가리키는 좋아요댓글의 개수를 알 수가 있다.

이미, model Post를 정의할 때, Post 하나당 여러 Comment와 Like를 받을 수 있기 때문에 각각을 배열로 받아왔다. 그럼,Comment가 몇개인지, Like가 몇개인지를 확인할 수 있어야 한다. 이때 사용하는게 _count 라고 생각하면 될 것 같다 !


Likes and Dislikes

이제 /posts/[id] 로 이동할 수 있는 페이지의 UI와 기능을 생성해보려고 한다.

여기서 우리가 생각해봐야 할 것은 -> /posts/[id] 에서
1 . posts의 id를 가져와 상세 내용 보여주기
2 . /posts/[id] 로 접속 할 때, 조회수 1 늘리기
3 . 좋아요 & 좋아요 취소 구현하기

먼저 posts의 id를 가져와 화면에 상세 내용 보여주기 + /posts/[id] 조회수 1 늘리기
먼저, 데이터를 불러올 때 우리는 findUnique를 보통 사용하게 된다.

그럼 여기서, getPost()[데이터를 불러오는 함수임]이 호출될 때 -> 즉 /post/[id]로 페이지가 이동할 때 -> 조회수가 1 올라간다 -> getPost()가 호출될 때마다 조회수가 1씩 증가한다. 라고 이해하면 된다.

async function getPost(id: number) {
  try {
    const post = await db.post.update({
      where: {
        id,
      },
      data: {
        views: {
          increment: 1,
        },
      },
      include: {
        user: {
          select: {
            username: true,
            avatar: true,
          },
        },
        _count: {
          select: {
            Like: true,
            Comments: true,
          },
        },
      },
    });
    return post;
  } catch (e) {}
}

post의 [id]를 넘겨 받은 다음, id를 가지고 getPost에서 함수를 Update()를 시킨다.

const post = await db.post.update({...}):

  • await 키워드를 사용해 데이터베이스에서 post 테이블의 레코드를 업데이트하고 그 결과를 변수 post에 저장한다.
  • 이 쿼리는 데이터베이스에서 게시물의 조회수를 1 증가시키고, 게시물의 추가적인 정보를 포함하여 가져오게 된다.

where: { id, } :

  • where 절은 어떤 조건에 맞는 레코드를 업데이트할지 지정한다. 여기서는 id 필드가 함수에 전달된 id 값과 일치하는 게시물을 찾는다.

data: { views: { increment: 1 }, } :
data 객체는 업데이트할 데이터를 지정한다.
--> 여기서는 views 필드increment: 1로 설정하여 현재 views 값을 1 증가시킨다. 즉, 게시물이 조회될 때마다 조회수가 1씩 늘어나게 된다.

include: { ... } :
include 옵션은 업데이트 후 가져올 연관된 데이터 또는 카운트 정보를 지정한다.

user: { select: { username: true, avatar: true } }:
user 필드와 관련된 데이터를 가져온다. 여기서는 username과 avatar만을 선택하여 가져온다.(post를 작성한 user의 정보를 뿌려줄 것이기 때문)
--> 이는 게시물을 작성한 사용자의 사용자명(username)아바타 이미지(avatar) 를 가져오도록 한다.

_count: { select: { Like: true, Comments: true } } :
_count는 연관된 데이터의 개수를 가져온다.
--> 여기서는 Like와 Comments의 개수를 가져온다. 즉, 해당 게시물이 받은 좋아요 수와 댓글 수를 포함시키게 된다.

이렇게 가져온 데이터를 활용해서 화면을 간단하게 꾸며보자면
이렇게 꾸며지게 된다.
조회는 화면이 새로고침 되면서 -> getPost()함수가 호출될 때마다 1씩 증가하게 된다.

그럼 여기서 공감하기 버튼을 한번 눌렀을 때는 숫자가 올라가고, 다시 눌렀을 때는 취소되도록 하는 로직을 짜야한다.


먼저, likePost()함수를 살펴보면, likePost()함수가 실행되면,
db.like.create() 메서드를 사용하여 새로운 "Like" 레코드를 데이터베이스에 생성한다.
postId: id : 현재 페이지나 게시물의 id이다. 이 id는 사용자가 좋아요를 누른 게시물의 고유 식별자이다.
userId: session.id! : 현재 로그인한 사용자의 id이다. 이 id는 좋아요를 누른 사용자의 고유 식별자이다.

요약하자면, 이 함수는 사용자가 특정 게시물에 좋아요를 눌렀을 때 Like 레코드를 데이터베이스에 추가한다.
--> 이 레코드는 postId와 userId를 함께 저장하여 특정 사용자가 특정 게시물에 좋아요를 눌렀다는 정보를 기록한다.

dislikePost()함수를 살펴보면, dislikePost()함수가 실행되면,
db.like.delete() 메서드를 사용하여 "Like" 레코드를 데이터베이스에서 삭제한다.

where: { id: { postId: id, userId: session.id! } } : 여기서 id 객체는 복합 키를 나타내며, 이 키에 해당하는 Like 레코드를 삭제한다. 이 id는 특정 게시물의 postId와 그 게시물에 좋아요를 누른 사용자의 userId로 구성된다.

요약하자면, 이 함수는 사용자가 특정 게시물에서 좋아요를 취소할 때, 해당 postId와 userId를 기반으로 Like 레코드를 삭제한다.
--> 이로써 데이터베이스에서 해당 사용자가 그 게시물에 대해 좋아요를 눌렀다는 기록을 제거한다.

그럼, 여기서 좋아요 상태를 판단하고, isLiked가 되었다면 ? dislikePost를, 아니라면, likePost를 설정해주어서 좋아요, 좋아요 취소 로직을 실행시켜야 한다.
{ isLiked ? dislikePost : likePost } 이런식으로!

그럼, 좋아요를 했는지의 상태를 얻어오기 위해서 getIsLiked() 함수를 생성해야 한다.

async function getIsLiked(postId: number) {
  const session = await getSession();
  const like = await db.like.findUnique({
    where: {
      id: {
        postId,
        userId: session.id!,
      },
    },
  });
  return Boolean(like);
}

post(id) 를 넘겨주어서, db.like에 id가 postId와 userId가 저장이 되어있는지

where: { id: { postId, userId: session.id! } } :
postId와 userId로 구성된 복합 키(id) 를 사용하여 해당 Like 레코드를 찾는다.
postId는 함수에 전달된 게시물의 고유 식별자이고, userId는 현재 로그인한 사용자의 id이다.

이 쿼리는 주어진 postId와 userId 조합이 Like 테이블에 존재하는지 확인한다. 만약 존재한다면, 이 사용자가 해당 게시물에 좋아요를 눌렀음을 의미한다.

like가 null 또는 undefined일 경우, Boolean(like)는 false를 반환합니다. 즉, 사용자가 이 게시물에 대해 좋아요를 누르지 않은 경우를 의미 반대는 눌렀다는 의미!


form에 action을 더해주었는데, 위에서 언급했듯이,
const isLiked = await getIsLiked()를 통해 반환 값을 isLiked로 저장해두고,
isLiked라면 ? dislikePost를, (좋아요가 이미 있으니까 좋아요 취소),
isLike가 아니라면, likePost를 (좋아요가 없으니까 좋아요!)

이렇게 진행된다!

Tips
update 를 사용 -> database안의 record를 수정하고 나서, 수정된 record를 return 해주게 된다. --> post를 update해서 조회수를 올리고, update method가 그 post를 return 해주게 된다

update method는 업데이트 할 post를 찾지 못하면 에러를 발생시키게 된다. --> 즉 id를 넘겼을 때, 존재하지 않는 id라면 post가 존재하지 않기 때문에 update되지 않고, 에러가 발생하게 된다. -> 그렇기 때문에 전체가 터지는 것을 방지하기 위해서 try 문 사용


Cache Tags

이제, cache에 저장을 해볼 시간이다.
여기를 살펴보면, 현재 데이터가

1 . posts/[id]에 대해서 데이터 불러오기
2 . like(좋아요 -> 좋아요 했는지 여부, 좋아요 수) 에 대해서 불러오기

이렇게 두 데이터가 불러와지고 있다.

getCachePost

  • getCachePost를 처음 호출하면, getPost 함수가 실행되고 결과가 'post-detail'에 저장되게 된다.
  • 캐시된 데이터 사용: 이후 30초 동안 동일한 post-detail 키를 사용하는 요청이 오면, 캐시된 데이터를 반환하게 된다.
  • 재검증 시간 도래: 30초가 지나면 캐시가 만료되고, getPost가 다시 호출되어 데이터를 갱신하게 된다.
  • 수동 무효화: 언제든지 revalidateTag('post-detail') 를 호출하면, 해당 태그에 연결된 캐시가 무효화되고, getPost 함수가 다시 실행된다.

이렇게 지정을 해두었다.

getLikeStatus

getLikeStatus의 함수에서 postId와 userId를 받아온 뒤, 해당 유저가 postId에 해당하는 특정 게시물에 좋아요를 눌렀는지 여부와, 그 게시물의 좋아요 수를 가져오게 된다.

좋아요 상태 확인 (isLiked):

  • db.like.findUnique 를 호출하여, 주어진 postId와 userId로 해당 유저가 그 게시물에 '좋아요'를 눌렀는지 확인한다.
  • 반환된 결과가 존재하면 isLiked를 true, 그렇지 않으면 false로 설정한다.

좋아요 개수 확인 (likeCount):

  • db.like.count 를 호출하여, 특정 postId에 대한 '좋아요'의 총 개수를 계산한다.

getLikeStatus -> post가 이미 like되어있는지,like의 갯수는 몇개인지
이게 URL에서 얻은 id를 가진 post에 대해 생성된 like의 개수를 알려주게 될 것이다.

getCachedLikeStatus
처음 보는 형태로 되어있는데,
getCachedLikeStatus()함수 안에서 cachedOperation을 호출하고 있다.

getCachedLikeStatus()함수가 postId, userId를 각각 받은 뒤, cachedOperation에 tag에 like-status-${postId} 를 넘겨주었다.

이렇게 postId를 넘겨준 이유는 ? --> 특정 게시물의 대한 like 데이터를 불러오려고 하기 때문이다.

왜? 단순히 like-status만 tag로 넣어주게 되면, 좋아요를 누르고 좋아요를 취소할 때, 모든 like-status에 관한 데이터가 다 불러와지기 때문이다. (해당 게시물에 관한 like 데이터에 대해서만 불러오고 싶다)

왜 postId를 태그에 포함시키는가?

  • 목적: 특정 게시물에 대한 '좋아요' 데이터를 캐싱하고, 효율적으로 불러오기 위해서이다.
  • 이유: 단순히 like-status 만 태그로 사용하면, revalidateTag('like-status')를 호출할 때 모든 게시물의 '좋아요' 데이터가 무효화된다.
    --> 이렇게 되면, 전체 게시물에 대해 불필요한 데이터 재로딩이 발생할 수 있다.
  • 해결 방법: like-status-${postId} 형태로 태그를 지정하면, revalidateTag를 호출할 때 특정 게시물(postId)의 '좋아요' 상태만 무효화하거나 갱신할 수 있다.
    이렇게 하면 특정 게시물에 대한 '좋아요' 데이터만 갱신되고, 나머지 게시물의 데이터는 캐시된 상태로 유지된다.

useOptimistic

유저가 데이터를 우리에게 보내주면, 우리는 거기에 맞게 database를 수정한다.
-> 그게 mutation을 의미한다. 유저가 우리 백엔드에 있는 리소스를 변형하는 것을 의미한다고 생각하면 된다.

유저가 mutation을 trigger하면, 우리에겐 두가지 옵션이 있다.
-> 하나는 mutation이 완전히 끝날때까지 그냥 기다리는 것, 그리고 유저에게 mutation 진행 사항을 알려주는 것 (ex 회원가입 처리 중 )

하지만 게시물 like/dislike mutation의 경우, 굳이 백엔드에서 mutation이 다 끝날때까지 기다릴 필요가 없다. 대신 유저에게 optimistic response를 줄 수 있다.

optimistic response는 마지 백엔드에서 mutation이 성공한 것처럼 UI를 수정하는 것을 의미한다
--> 실제 백엔드에서 일어나는 mutation에는 관심도 없고, 기다릴 필요도 없다
--> 유저에게 그냥 mutation이 성공하면 UI가 어떻게 보일 지 보여주는 것이다 라고 생각하면 될 것이다.

이때 사용하는 메서드는 useOptimistic이다.
--> UI를 낙관적으로 업데이트할 수 있는 React Hook -> 유저에게 action 수행 결과를 즉시 보여주는데 사용되기 때문이다.

현재 우리가 useOptimistic()을 사용해보려고 하는 버튼이다.
이 버튼을 누르는데 만약 10초라는 시간이 걸릴 때, 사용자가 10초를 기다려야만 할까?
아니다! 데이터에 불러오는 시간을 기다리는 대신에, 화면에 보여질 것을 미리 사용자에게 보여주면 된다.

useOptimistic() 함수는 -> "use client"에서 사용이 되어야 한다.
왜 ? useOptimistic는 사용자 인터페이스(UI)를 빠르게 업데이트하여 사용자 경험을 개선하기 위해 사용된다. 이 훅은 사용자가 어떤 작업을 수행했을 때, 실제 서버 응답이 오기 전에 예상되는 결과를 UI에 즉시 반영할 수 있게 해준다.
--> 따라서 클라이언트 사이드에서 실행되는 코드를 포함한 파일에서 사용되어야 하며, 이러한 파일들은 use client로 지정되어야 한다.

고로, components/like-button.tsx 폴더를 생성해서 따로 빼주었다.

이제, uesOptimistic()을 사용해보도록 하겠다!

useOptimistic -> 두가지 인자를 받는다.

초기 상태 ({ isLiked, likeCount }):

  • useOptimistic의 첫 번째 인수로 주어지는 객체는 초기 상태를 정의한다.
  • 여기서 isLiked는 사용자가 현재 해당 게시물에 '좋아요' 를 눌렀는지 여부를 나타내고, likeCount는 해당 게시물의 '좋아요' 수를 나타낸다.

상태와 리듀서 함수 ([state, reducerFn]):

  • useOptimistic 훅은 [state, reducerFn] 배열을 반환한다.
    state는 현재 상태를 나타내며, 초기에는 { isLiked, likeCount } 객체로 설정된다.
    reducerFn은 상태를 업데이트하는 함수로, useReducer 훅의 리듀서 함수와 유사하게 동작한다.

리듀서 함수 ((previousState, payload) => { ... }):

  • useOptimistic 의 두 번째 인수로 제공된 함수는 상태를 업데이트하는 리듀서 함수이다.
  • 이 함수는 두 개의 매개변수를 받는다:
    1 . previousState: 이전 상태 객체 ({ isLiked, likeCount }).
    2 . payload: 상태를 업데이트하는 데 필요한 추가 데이터.

상태 업데이트 로직:

  • 리듀서 함수는 isLiked와 likeCount 값을 기반으로 새로운 상태를 반환한다.
  • isLiked는 previousState.isLiked값을 반전시킨다.
    --> 즉, '좋아요' 상태를 토글한다.
  • likeCount는 isLiked 상태에 따라 변경된다 :
    만약 previousState.isLiked 가 true였다면, 사용자가 '좋아요'를 취소한 것이므로 likeCount가 1 감소한다.
    반대로 previousState.isLiked가 false였다면, 사용자가 '좋아요'를 누른 것이므로 likeCount가 1 증가한다.

이제 button에 연결할 onClick함수를 설정해주어야 한다.

const onClick = async () => {
    reducerFn(undefined);
    if (isLiked) {
      await dislikePost(postId);
    } else [await likePost(postId)];
  };

reducerFn(undefined); 호출:

  • reducerFn 을 호출하여 낙관적(Optimistic) UI 업데이트를 수행한다.

  • undefined를 인수로 전달했기 때문에, reducerFn은 useOptimistic에서 정의된 리듀서 함수의 previousState만을 사용하여 상태를 업데이트한다.

  • 이 호출로 인해 UI는 즉시 업데이트되며, 사용자가 "좋아요"를 눌렀다면 "좋아요" 아이콘이 활성화되고, "좋아요"를 취소했다면 비활성화된다.

이렇게 정의한 onClick과, useOptimistic()에서 정의한 state를 가져와 사용해 보았다.

정리
useOptimistic 훅 사용:
useOptimistic 훅은 초기 상태와 리듀서 함수를 인수로 받아, [state, reducerFn] 배열을 반환한다.
state는 현재 상태를 나타내고, reducerFn은 상태를 업데이트하는 함수이다.

reducerFn 호출:
reducerFn을 호출할 때, 이 함수는 useOptimistic에서 지정한 리듀서 함수를 실행하여 상태를 업데이트한다.
reducerFn에 인수를 전달하면, 그 인수가 리듀서 함수의 payload로 전달된다.

라고 이해하면 된다.

이렇게 useOptimistic()함수를 사용하게 되면, 얼마나 걸리든 화면에 UI를 보여줄 수 있게 되었다.

'use client';

import { HandThumbUpIcon } from '@heroicons/react/24/solid';
import { HandThumbUpIcon as OutlineHandThumbIcon } from '@heroicons/react/24/outline';
import { useOptimistic } from 'react';
import { dislikePost, likePost } from '@/app/posts/[id]/actions';

interface LikeButtonProps {
  isLiked: boolean;
  likeCount: number;
  postId: number;
}

export default function LikeButton({
  isLiked,
  likeCount,
  postId,
}: LikeButtonProps) {
  const [state, reducerFn] = useOptimistic(
    { isLiked, likeCount },
    (previousState, payload) => {
      return {
        isLiked: !previousState.isLiked,
        likeCount: previousState.isLiked ? previousState.likeCount - 1 : +1,
      };
    }
  );

  const onClick = async () => {
    reducerFn(undefined);
    if (isLiked) {
      await dislikePost(postId);
    } else [await likePost(postId)];
  };
  return (
    <>
      <button
        onClick={onClick}
        className={`flex items-center gap-2 text-neutral-400 text-sm border border-neutral-400 rounded-full p-2  ${
          state.isLiked ? 'bg-orange-500 text-white border-orange-500' : ''
        }`}
      >
        {state.isLiked ? (
          <HandThumbUpIcon className="size-5" />
        ) : (
          <OutlineHandThumbIcon className="size-5" />
        )}
        {state.isLiked ? (
          <span> {state.likeCount}</span>
        ) : (
          <span>공감하기 ({state.likeCount})</span>
        )}
      </button>
    </>
  );
}
profile
2024. 01. 02 ~ 백앤드 공부 시작, 2024. 04.01 ~ 프론트 공부 시작

0개의 댓글