좋아요 기능 - Part1 좋아요 버튼 (토글) with 트랜잭션

Project Diary/React + MariaDB (PicShare WebApp)

사용자가 특정 게시물에 좋아요를 눌렀는지 여부를 표시하고, 좋아요 버튼을 눌렀을 때 해당 상태를 토글하는 기능을 구현합니다. 이 기능은 프론트엔드와 백엔드, 데이터베이스 설계를 포함하여 전반적인 구조를 설명합니다.


 

데이터베이스

 

  • userNo와 postId의 조합으로 중복을 방지하고, 특정 사용자가 특정 게시물을 좋아요 했는지 여부를 확인합니다.
  • FOREIGN KEY 제약조건 
CONSTRAINT `FK__posts` FOREIGN KEY (`postId`) REFERENCES `posts` (`postId`) ON DELETE CASCADE,
CONSTRAINT `FK__users` FOREIGN KEY (`userNo`) REFERENCES `users` (`userNo`) ON DELETE CASCADE

 


 

프론트엔드

 

1. LikeButton 컴포넌트

 

1-1 컴포넌트 상태 및 Redux 상태 연결

import React, { useState, useEffect } from "react";
import { useDispatch, useSelector } from "react-redux";
import axios from "axios";
import { FaHeart, FaRegHeart } from "react-icons/fa";
import { fetchLikeList } from "./likeSlice"; // 적절한 경로로 변경

const serverUrl = import.meta.env.VITE_API_URL;

const LikeButton = ({ postId }) => {
  const dispatch = useDispatch();
  const user = useSelector((state) => state.members.user);
  const likeList = useSelector((state) => state.likes.likeList);
  const [isLiked, setIsLiked] = useState(false);
  const [likedUsers, setLikedUsers] = useState([]);
  const [showModal, setShowModal] = useState(false);
  • useDispatch와 useSelector를 사용하여 Redux 상태와 연결합니다.
  • useState를 사용하여 좋아요 상태와 좋아요한 사용자 목록을 관리합니다.

1-2 컴포넌트 마운트 시 데이터 가져오기

 useEffect(() => {
    if (likeList && likeList.length > 0) {
      setIsLiked(likeList.some((post) => post.postId === postId));
    }
  }, [likeList, postId]);

likeList가 변경될 때마다 isLiked 상태를 업데이트합니다.

 

 const fetchLikedUsers = async () => {
    try {
      const res = await axios.post(`${serverUrl}/other/post/likedUsers`, {
        postId,
      });
      if (res.data) {
        setLikedUsers(res.data);
      }
    } catch (error) {
      console.error("좋아요 유저 목록을 가져오는 중 오류 발생:", error);
    }
  };

현재 게시물을 좋아요한 사용자 목록을 서버에서 가져옵니다.

 

useEffect(() => {
    fetchLikedUsers();
  }, [postId]);

컴포넌트가 마운트되거나 postId가 변경될 때 fetchLikedUsers를 호출하여 좋아요한 사용자 목록을 가져옵니다.

 

1-3 좋아요 토글 함수

  const onToggle = async () => {
    if (user) {
      try {
        const res = await axios.post(`${serverUrl}/other/post/likeToggle`, {
          post: { postId },
          userNo: user.userNo,
        });
        if (res.data) {
          setIsLiked((prev) => !prev);
          dispatch(fetchLikeList(user.userNo));
          await fetchLikedUsers(); // 좋아요 유저 목록을 다시 가져옴
        } else {
          console.log("좋아요 상태 변경 실패");
        }
      } catch (error) {
        console.error("좋아요 상태 변경 중 오류 발생:", error);
      }
    } else {
      alert("로그인해 주세요.");
    }
  };

사용자가 로그인한 경우 서버에 likeToggle 요청을 보내고, 성공 시 isLiked 상태를 토글하고 likeList를 다시 가져옵니다.

 

 

2. 좋아요 버튼 동기화 - Redux Slice: 좋아요 상태 관리

 

2-1 Like Slice 생성

import { createSlice } from "@reduxjs/toolkit";
import axios from "axios";

const serverUrl = import.meta.env.VITE_API_URL;

const likeSlice = createSlice({
  name: "like",
  initialState: {
    likeList: [],
  },
  reducers: {
    initLikeList(state, action) {
      state.likeList = action.payload;
    },
  },
});

export const { initLikeList } = likeSlice.actions;

export default likeSlice.reducer;

 

2-2  좋아요 목록을 가져오는 비동기 액션

export const fetchLikeList = (userNo) => async (dispatch) => {
  try {
    const res = await axios.post(`${serverUrl}/other/post/likeList`, { userNo });
    const data = res.data.map((post) => ({
      ...post,
      imageUrls: post.imageUrls ? post.imageUrls.split(",") : [],
    }));
    console.log("패치라이크리스트", data);
    dispatch(initLikeList(data));
    return data;
  } catch (err) {
    console.log(err);
    throw err;
  }
};
  • 사용자 번호를 받아 해당 사용자가 좋아요한 게시물 목록을 가져오는 비동기 액션입니다.
  • axios.post : 서버에 likeList 요청을 보냅니다
  • dispatch(initLikeList(data)): 서버에서 받은 데이터를 리덕스 상태로 설정합니다.

사용자가 좋아요 버튼을 누를 때마다 fetchLikeList를 호출하여 최신 좋아요 목록을 가져와 likeList를 업데이트하고, 이를 통해 LikeButton 컴포넌트의 상태를 동기화할 수 있습니다.

 


 

백엔드

 

0. 필요 개념 설명

트랜잭션이란?

  • 트랜잭션은 데이터베이스 상태를 변화시키기 위해 수행하는 작업의 단위입니다.
  • 트랜잭션은 하나 이상의 SQL 문으로 구성될 수 있습니다.
  • 모든 작업이 성공적으로 완료되거나, 하나라도 실패하면 모든 작업이 취소됩니다.

트랜잭션의 필요성

트랜잭션의 필요성을 예시로 설명하겠습니다.

계좌 이체 - A 계좌에서 100원 인출
- B 계좌에 100원 입금
- 실패 : 만약 하나라도 실패하면 전체 트랜잭션이 취소되어, A 계좌에서 100원이 인출되지 않고, B 계좌에 입금되지도 않습니다.
-  성공 : 트랜잭션이 완료되면 데이터의 일관성이 유지되며 상태가 업데이트 됩니다.
좋아요 기능 - 사용자가 게시물에 좋아요를 누름
- 좋아요를 추가하거나 이미 존재하는 좋아요의 상태를 반대로 업데이트
- 사용자가 좋아요한 모든 게시물의 정보 조회(상태 동기화를 위해)
- 실패 : 만약 하나라도 실패하면 전체 트랜젝션이 취소되어, 좋아요 데이터 삽입 전체 작업이 취소됩니다.
-  성공 : 트랜잭션이 완료되면 데이터의 일관성이 유지되며 좋아요 상태가 업데이트 됩니다

 

트랜잭션 처리 

  1. 트랜잭션 시작 : beginTransaction 메소드를 사용하여 트랜잭션을 시작합니다.
  2. 트랜잭션 커밋: 트랜잭션이 성공적으로 완료되면 commit 메소드를 호출하여 모든 변경 사항을 데이터베이스에 반영합니다.
  3. 트랜잭션 롤백: 트랜잭션 중 하나라도 실패하면 rollback 메소드를 호출하여 모든 변경 사항을 취소하고 데이터베이스 상태를 트랜잭션 시작 전으로 되돌립니다.

 

1. 좋아요 토글 API엔드포인트

 

1-1 트랜잭션 시작

connection.beginTransaction((err) => {
  if (err) {
    console.error("에러", err);
    res.status(500).send("실패");
    connection.release();
    return;
  }

 

1-2   좋아요 쿼리 실행

  const insertLikeQuery = `
    INSERT INTO postlike (userNo, postId, postPhoto, isLiked, date) 
    VALUES (?, ?, ?, 1, ?)
    ON DUPLICATE KEY UPDATE isLiked = NOT isLiked`;

  connection.query(
    insertLikeQuery,
    [
      userNo,
      post.postId,
      post.postPhoto,
      date.format("YYYY-MM-DD HH:mm:ss"),
    ],
    (err, result) => {
      if (err) {
        return connection.rollback(() => {
          connection.release();
          res.status(500).send(err);
        });
      }
  • 사용자가 게시물에 처음 좋아요를 누르면 postlike 테이블에 새로운 레코드가 삽입됩니다.
  • 사용자가 이미 좋아요를 누른 상태에서 다시 누르면 'isLiked'값이 반대로 토글됩니다.
    >  ON DUPLICATE KEY UPDATE isLiked = NOT isLiked

1-3 쿼리 결과 확인 / 특정 사용자가 좋아요한 게시물 목록을 조회 쿼리 실행

      if (result.affectedRows !== 0) {
        const selectLikeQuery = "SELECT * FROM postlike WHERE userNo=?";
        connection.query(selectLikeQuery, [userNo], (err, result) => {
          if (err) {
            return connection.rollback(() => {
              connection.release();
              res.status(500).send(err);
            });
          }

사용자가 좋아요를 눌렀거나 취소한 후, 해당 사용자가 좋아요한 게시물 목록을 최신 상태로 가져와 클라이언트에 반환하기 위해 사용됩니다.

 

1-4 트랜잭션 커밋

          connection.commit((err) => {
            if (err) {
              return connection.rollback(() => {
                connection.release();
                res.status(500).send(err);
              });
            }

            connection.release();
            res.send(result);
          });

트랜잭션이 성공적으로 완료되면 commit 메소드를 호출하여 모든 변경 사항을 데이터베이스에 반영합니다.

 

1-5 트랜잭션 롤백

        connection.rollback(() => {
          connection.release();
          res.status(500).send("좋아요 업데이트 실패");
        });

트랜잭션 중 하나라도 실패하면 rollback 메소드를 호출하여 모든 변경 사항을 취소하고 데이터베이스 상태를 트랜잭션 시작 전으로 되돌립니다.