UI 구현 연구일지

[React 컴포넌트] Like, Dislike (좋아요, 싫어요) 기능 만드는 법

페블_ 2022. 7. 11. 22:58

React (LikeDislikes.js)

Like, Dislike를 누를 때 발생하는 3가지 경우의 수를 생각해보자.

  • 좋아요, 싫어요 둘다 눌리지 않은 상태 -> 둘 중 하나를 누르면 좋아요/싫어요 등록
  • 좋아요가 이미 눌려 있는 상태에서 싫어요를 누른다면 -> 좋아요 삭제 후 싫어요 등록
  • 싫어요가 이미 눌려 있는 상태에서 좋아요를 누른다면 -> 싫어요 삭제 후 좋아요 등록

 

과정

  • Like, Dislike (좋아요, 싫어요 숫자 개수를 저장)와 LIkeAction, DislikeAction(좋아요, 싫어요가 눌렸는지 여부)를 저장하는 state를 준비한다.
  • API 호출을 할 때 정보를 전달할 변수 variables를 준비한다. Like, Dislike를 포스팅과 댓글 모두에 사용할 수 있게 설계했기 때문에 해당 컴포넌트가 어디에 쓰이는지 먼저 검사해야 한다. props에 postId가 존재한다면 variables에 postId 값을 첨부하고, props.comment가 존재한다면 commentId 값을 첨부한다.
  • useEffect()에서 /getLikes와 /getDislikes를 가져온다. 그러면 { userId: 값, postId or commentId: 값 } 형태의 배열을 받을 것이다. 그 배열의 길이가 바로 Like/Dislike 개수이고, 그 중에 현재 로그인한 userId와 일치하는 값이 존재한다면 이미 Like/Dislike를 누른 것이므로 LikeAction/DislikeAction을 true로 설정해준다.
  • onClick 함수를 Like, Dislike 버튼에 각각 만든다. 버튼을 누르면 먼저 Redux에 들어있는 유저 정보를 검사해서 로그인이 되어있는지부터 확인한다.
  • 버튼이 3가지 경우의 수 중 어느 하나에 해당되는지 검사해서 그에 알맞는 동작을 하게 한다. 예를 들어 Like 버튼이 눌린 여부가 null이어도 Dislike가 눌려서 그런 것일 수도 있으므로 검사를 한다든가,
  • 이미 어느 한 개가 눌려 있고 다른 쪽을 누르는 경우, 원래 등록된 state를 null로 만들고 DB 정보를 삭제하는 API를 호출하는 것을 잊지 말자
import React, { useEffect, useState } from 'react';
import { Tooltip, Icon } from 'antd';
import axios from 'axios';
import { useSelector } from 'react-redux';

function LikeDislikes(props) {
  const user = useSelector(state => state.user);

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

  let variable = {};

  if(props.postId) {
    variable = { postId: props. postId, userId: props.userId }
  } else {
    variable = { commentId: props.commentId , userID: props.userId }
  }

  useEffect(() => {
    axios.post('/api/like/getLikes', variable)
      .then(response => {
        if(response.data.success) {
          setLikes(response.data.likes.length);

          response.data.likes.map(like => {
            if(like.userId === props.userId) {
              setLikeAction(true);
            }
          })
        } else {
          alert('Likes 정보를 가져오는데 실패했습니다.')
        }
      });

      axios.post('/api/like/getDislikes', variable)
      .then(response => {
        if(response.data.success) {
          setDislikes(response.data.dislikes.length);

          response.data.dislikes.map(like => {
            if(like.userId === props.userId) {
              setDislikeAction(true);
            }
          })
        } else {
          alert('Dislikes 정보를 가져오는데 실패했습니다.')
        }
      });

  }, [])
  
  const onLike = () => {

    if(user.userData && !user.userData.isAuth) {
      return alert('로그인을 해주세요');
    }

    if(LikeAction === null) {
      axios.post('/api/like/upLike', variable)
        .then(response => {
          if(response.data.success) {
            setLikes(Likes + 1);
            setLikeAction(true);

            // like가 null이어도 dislike가 이미 눌려있어서 그럴 수도 있기 때문에 dislike 여부도 확인한다.
            if(DislikeAction !== null) {
              setDislikeAction(null);
              setDislikes(Dislikes - 1);
            }

          } else {
            alert('Like를 올리는데 실패했습니다.');
          }
        });

    } else {
      axios.post('/api/like/unLike', variable)
        .then(response => {
          if(response.data.success) {
            setLikes(Likes - 1);
            setLikeAction(null);
          } else {
            alert('Like를 취소하는데 실패했습니다.');
          }
        });
    }
  };

  const onDislike = () => {
    
    if(user.userData && !user.userData.isAuth) {
      return alert('로그인을 해주세요');
    }
    
    if(DislikeAction === null) {
      axios.post('/api/like/upDislike', variable)
        .then(response => {
          if(response.data.success) {
            setDislikes(Dislikes + 1);
            setDislikeAction(true);

            // dislike가 null이어도 like가 이미 눌려있어서 그럴 수도 있기 때문에 like 여부도 확인한다.
            if(LikeAction !== null) {
              setLikeAction(null);
              setLikes(Likes - 1);
            }

          } else {
            alert('Dislike를 올리는데 실패했습니다.');
          }
        });

    } else {
      axios.post('/api/like/unDislike', variable)
        .then(response => {
          if(response.data.success) {
            setDislikes(Dislikes - 1);
            setDislikeAction(null);
          } else {
            alert('Like를 취소하는데 실패했습니다.');
          }
        });
    }
  }

  return (
    <div>
      <span key="comment-basic-like" >
        <Tooltip title="Like">
          <Icon type="like"
                theme={LikeAction ? 'filled' : 'outlined'}
                onClick={onLike}
          />
        </Tooltip>
      </span>
      <span style={{ padingLeft: '8px', cursor: 'auto' }}> {Likes} </span>

      <span key="comment-basic-dislike" >
        <Tooltip title="Dislike">
          <Icon type="dislike"
                theme={DislikeAction ? 'filled' : 'outlined'}
                onClick={onDislike}
          />
        </Tooltip>
      </span>
      <span style={{ padingLeft: '8px', cursor: 'auto' }}> {Dislikes} </span>
    </div>
  )
  
}

export default LikeDislikes

 

API

model

Like와 Dislike를 따로 관리해야 하기 때문에 model도 두 개를 만들어준다.

LikeDislikes 컴포넌트를 포스트에서뿐만 아니라 댓글에서도 공통으로 사용할 수 있도록 userId, commentId, postId 세 가지 요소를 갖게 한다. 그러면 find()할 때 포스트는 postId로 찾고, 댓글은 commentId로 찾는 식으로 구별할 수 있다.

Like.js

const mongoose = require('mongoose');
const Schema = mongoose.Schema;

const LikeSchema = mongoose.Schema({
  userId: {
    type: Schema.Types.ObjectId,
    ref: 'User'
  },
  commentId: {
    type: Schema.Types.ObjectId,
    ref: 'Comment'
  },
  postId: {
    type: Schema.Types.ObjectId,
    ref: 'Post'
  }
}, { timestamps: true });

const Like = mongoose.model('Like', LikeSchema);

module.exports = { Like };

Dislike.js

const mongoose = require('mongoose');
const Schema = mongoose.Schema;

const DislikeSchema = mongoose.Schema({
  userId: {
    type: Schema.Types.ObjectId,
    ref: 'User'
  },
  commentId: {
    type: Schema.Types.ObjectId,
    ref: 'Comment'
  },
  postId: {
    type: Schema.Types.ObjectId,
    ref: 'Post'
  }
}, { timestamps: true });

const Dislike = mongoose.model('Dislike', DislikeSchema);

module.exports = { Dislike };

 

router (router/like.js)

  • 조회 - /getLikes, /getDislikes
  • 등록 - /upLike, /upDislike
  • 취소 - /unLike, /unDislike
const express = require('express');
const router = express.Router();

const { Like } = require('../models/Like');
const { Dislike } = require('../models/Dislike');

router.post('/getLikes', (req, res) => {

  let variable = {};

  if(req.body.postId) {
    variable = { postId: req.body.postId }
  } else {
    variable = { commentId: req.body.commentId }
  }

  Like.find(variable)
    .exec((err, likes) => {
      if(err) return res.status(400).send(err);
      res.status(200).json({ success: true, likes });
    })
});

router.post('/getDislikes', (req, res) => {

  let variable = {};

  if(req.body.postId) {
    variable = { postId: req.body.postId }
  } else {
    variable = { commentId: req.body.commentId }
  }

  Dislike.find(variable)
    .exec((err, dislikes) => {
      if(err) return res.status(400).send(err);
      res.status(200).json({ success: true, dislikes });
    })
});

router.post('/upLike', (req, res) => {
  let variable = {};

  if(req.body.postId) {
    variable = { postId: req.body.postId, userId: req.body.userId }
  } else {
    variable = { commentId: req.body.commentId, userId: req.body.userId }
  }

  // 만약 dislike가 이미 클릭되어있다면 like 정보를 저장하면서 dislike 기록 삭제
  const like = new Like(variable);
  like.save((err, likeResult) => {
    if(err) return res.json({ success: false, err })

    Dislike.findOneAndDelete(variable)
      .exec((err, disLikeResult) => {
        if(err) return res.status(400).json({ success: false, err });
        res.status(200).json({ success: true });
      });
  })

});

router.post('/upDislike', (req, res) => {
  let variable = {};

  if(req.body.postId) {
    variable = { postId: req.body.postId, userId: req.body.userId }
  } else {
    variable = { commentId: req.body.commentId, userId: req.body.userId }
  }

  const dislike = new Dislike(variable);
  dislike.save((err, dislikeResult) => {
    if(err) return res.json({ success: false, err })

    Like.findOneAndDelete(variable)
      .exec((err, likeResult) => {
        if(err) return res.status(400).json({ success: false, err });
        res.status(200).json({ success: true });
      });
  })

});

router.post('/unLike', (req, res) => {

  let variable = {};

  if(req.body.postId) {
    variable = { postId: req.body.postId, userId: req.body.userId }
  } else {
    variable = { commentId: req.body.commentId, userId: req.body.userId }
  }

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

});

router.post('/unDislike', (req, res) => {

  let variable = {};

  if(req.body.postId) {
    variable = { postId: req.body.postId, userId: req.body.userId }
  } else {
    variable = { commentId: req.body.commentId, userId: req.body.userId }
  }

  Dislike.findOneAndDelete(variable)
      .exec((err, result) => {
        if(err) return res.status(400).json({ success: false, err });
        res.status(200).json({ success: true });
      });

});

module.exports = router;