React, Next, Redux/⚛ React.JS

좋아요/싫어요 구현하기

DarrenKwonDev 2020. 9. 17. 17:04

로직은 다음과 같습니다.

좋아요(싫어요)가 안 눌려져 있을 때 좋아요를 누르기
=> 좋아요(싫어요) 1 올리기 

좋아요(싫어요)가 이미 클릭되어 있는데 다시 누르기
=> 좋아요(싫어요) 1 내리기 (취소)

싫어요(좋아요) 버튼을 클릭한다면 
=> 좋아요(싫어요) 1 내리고 싫어요(좋아요) 1 올리기

 

이번 프로젝트의 의의에는 싫어요 버튼을 제거하는 게 맞다고 생각하여서 싫어요를 반영하는 로직은 삭제했지만 작성한 코드는 아래에 따로 첨부했습니다.

 

우선 Like 스키마입니다. 

 

좋아요에 해당하는 숫자를 계산하기보다 도큐먼트를 늘리는 방식을 사용하고, 이후 숫자가 필요할 때 조건을 걸어 쿼리를 날리고 length를 계산하도록 합시다.

const mongoose = require("mongoose");

const likeSchema = mongoose.Schema({
  userId: {
    type: mongoose.Schema.Types.ObjectId,
    ref: "User",
  },
  commentId: {
    type: mongoose.Schema.Types.ObjectId,
    ref: "Comment",
  },
  PostId: {
    type: mongoose.Schema.Types.ObjectId,
    ref: "Post",
  },
});

// 인덱스 설정
likeSchema.index({ commentId: 1, userId: 1 });

// 모델의 이름과 스키마를 이용해 모델의 정의함.
const Like = mongoose.model("Like", likeSchema);

module.exports = Like;

 

 

 

antd를 사용중이어서 다음과 같이 작성했습니다.

function SingleComment({ post_id, comment, userData, Comments, setComments }) {

  .. 중략
  
  const actions = [
    <LikeDislike comment={comment} userData={userData} />,
    <span key="comment-basic-reply-to" onClick={onReplyClick}>
      Reply to
    </span>,
  ];

  return (
    <>
      <Comment
        key={comment._id}
        actions={actions}
        author={<a>{comment.writer_nickname}</a>}
        avatar={
          <Avatar src={comment.writer_image} alt={comment.writer_nickname} />
        }
        content={<ContentsWrapper>{comment.contents}</ContentsWrapper>}
        datetime={<DateRefining dateString={comment.createdAt} />}
      />
     
     ... 중략

 

 

좋아요/싫어요 버튼을 렌더하는 컴포넌트입니다.

 

likes state는 좋아요의 갯수를 의미하고

action state는 현재 좋아요 버튼을 누른 상태인지, 아무것도 안 누른 상태인지, 싫어요 버튼을 누른 상태인지 등을 저장하는 state입니다.

function LikeDislike({ comment, userData }) {
  const [likes, setLikes] = useState(0);
  const [action, setAction] = useState(null);

  const onLikeClick = () => {
    const body = {
      commentId: comment._id,
      userId: userData._id,
    };

     // 아무것도 누르지 않은 상태일 때
    if (action === null) {
    
      axios.post("/api/like/uplike", body).then((res) => {
        if (res.data.upLike) {
          setAction("liked");
          setLikes((prev) => prev + 1);
        } else {
          alert(res.data.err);
        }
      });
    } else if (action === "liked") {
      // 이미 좋아요를 눌렀는 데 다시 눌렀을 때이므로 취소 로직
      axios.post("/api/like/unlike", body).then((res) => {
        if (res.data.unLike) {
          setAction(null);
          setLikes((prev) => prev - 1);
        } else {
          alert(res.data.err);
        }
      });
    }
  };

  // 맨 처음 로딩할 때 해당 comment가 몇 개의 좋아요를 눌렀는지
  // 로그인한 유저각 이미 좋아요를 눌렀는지를 판단합니다.
  useEffect(() => {
    const body = {
      commentId: comment._id,
    };

    axios.post("/api/like/getLike", body).then((res) => {
      if (res.data.getLike) {
      
        // 얼마나 많은 좋아요를 받았는가
        setLikes(res.data.likes.length);

        // 내가 이미 좋아요를 눌렀는가
        res.data.likes.map((like) => {
          if (like.userId === userData._id) {
            setAction("liked");
          }
        });
      } else {
        alert("좋아요 데이터를 가져오는데 실패했습니다.");
      }
    });
  }, []);

  return (
    <div style={{ display: "flex" }}>
      <div
        key="comment-basic-like"
        title="Like"
        style={{ marginRight: "16px", cursor: "pointer" }}
      >
        <span onClick={onLikeClick}>
        
          // action이 like면 색을 채운 아이콘 아니면 색이 빈 아이콘을 넣습니다.
          {createElement(action === "liked" ? LikeFilled : LikeOutlined)}
          <span className="comment-action" style={{ marginLeft: "8px" }}>
            // likes 도큐먼트의 갯수(lenght)를 넣습니다.
            {likes}
          </span>
        </span>
      </div>
    </div>
  );
}

export default LikeDislike;

 

node로 백엔드 로직을 짜게 된다면 다음과 같이 되겠군요.

// ================================================================
// 좋아요를 눌렀을 때
// ================================================================
apiRouter.post("/like/uplike", (req, res) => {
  let { commentId, userId } = req.body;

  const LikeIns = new Like({ userId, commentId });

  LikeIns.save((err, result) => {
    if (err) return res.status(400).json({ upLike: false, err });
    return res.status(200).json({ upLike: true });
  });
});

// ================================================================
// 좋아요를 다시 눌러 취소할 때
// ================================================================
apiRouter.post("/like/unlike", (req, res) => {
  let { commentId, userId } = req.body;

  console.log(commentId, userId);

  Like.findOneAndDelete({ commentId: commentId, userId: userId }).exec(
    (err, result) => {
      if (err) return res.status(400).json({ unLike: false, err });
      return res.status(200).json({ unLike: true });
    }
  );
});

 

만약 싫어요도 같이 한다면?

 

조언하건데, action state하나로 하지 마시고 반드시 여러 개의 action을 두세요. 이게 훨씬 편합니다.

function LikeDislikes(props) {

    const [Likes, setLikes] = useState(0)
    const [Dislikes, setDislikes] = useState(0)
    const [LikeAction, setLikeAction] = useState(null)
    const [DislikeAction, setDislikeAction] = useState(null)
    
 ... 중략

 

좋아요, 싫어요를 가져올 때도 두 번을 날려야합니다.

  useEffect(() => {
    const body = {
      commentId: comment._id,
    };

    axios.post("/api/like/getLike", body).then((res) => {
      if (res.data.getLike) {
        // 얼마나 많은 좋아요를 받았는가
        setLikes(res.data.likes.length);

        // 내가 이미 좋아요를 눌렀는가
        res.data.likes.map((like) => {
          if (like.userId === userData._id) {
            setAction("liked");
          }
        });
      } else {
        alert("좋아요 데이터를 가져오는데 실패했습니다.");
      }
    });

    axios.post("/api/like/getDislike", body).then((res) => {
      if (res.data.getDisLike) {
        // 얼마나 많은 싫어요를 받았는가
        setLikes(res.data.dislikes.length);

        // 내가 이미 싫어요를 눌렀는가
        res.data.dislikes.map((dislike) => {
          if (dislike.userId === userData._id) {
            setAction("disliked");
          }
        });
      } else {
        alert("싫어요 데이터를 가져오는데 실패했습니다.");
      }
    });
  }, []);

 

좋아요를 누를 때는 이미 싫어요를 누른 상태라면 싫어요를 지워줘야 합니다.

역으로, 싫어요를 누를 때 이미 좋아요를 누른 상태라면 좋아요도 지워줘야겠죠.

apiRouter.post("/like/uplike", (req, res) => {
  let { commentId, userId } = req.body;

  const LikeIns = new Like({ userId, commentId });

  LikeIns.save((err, result) => {
    if (err) return res.status(400).json({ upLike: false, err });

    // 만약 이미 싫어요를 누른 상태라면, 싫어요를 취소야 합니다.
    Dislike.findByIdAndDelete(result._id).exec((err, dislikeResult) => {
      if (err) return res.status(400).json({ upLike: false, err });
      return res.status(200).json({ upLike: true });
    });
  });
});