ㆍ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 계좌에 입금되지도 않습니다. - 성공 : 트랜잭션이 완료되면 데이터의 일관성이 유지되며 상태가 업데이트 됩니다. |
좋아요 기능 | - 사용자가 게시물에 좋아요를 누름 - 좋아요를 추가하거나 이미 존재하는 좋아요의 상태를 반대로 업데이트 - 사용자가 좋아요한 모든 게시물의 정보 조회(상태 동기화를 위해) |
- 실패 : 만약 하나라도 실패하면 전체 트랜젝션이 취소되어, 좋아요 데이터 삽입 전체 작업이 취소됩니다. - 성공 : 트랜잭션이 완료되면 데이터의 일관성이 유지되며 좋아요 상태가 업데이트 됩니다 |
트랜잭션 처리
- 트랜잭션 시작 : beginTransaction 메소드를 사용하여 트랜잭션을 시작합니다.
- 트랜잭션 커밋: 트랜잭션이 성공적으로 완료되면 commit 메소드를 호출하여 모든 변경 사항을 데이터베이스에 반영합니다.
- 트랜잭션 롤백: 트랜잭션 중 하나라도 실패하면 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 메소드를 호출하여 모든 변경 사항을 취소하고 데이터베이스 상태를 트랜잭션 시작 전으로 되돌립니다.