좋아요 기능 - Part2 좋아요 리스트
ㆍProject Diary/React + MariaDB (PicShare WebApp)
특정 피드를 좋아하는 유저 목록과 내가 좋아하는 피드 목록을 보여주는 기능을 구현합니다.
프론트엔드
1. 좋아요 유저 목록 모달 (LikedUsersModal)
// LikedUsersModal 컴포넌트
const LikedUsersModal = ({ likedUsers, onClose }) => {
return (
<ModalOverlay onClick={onClose}>
<ModalContent onClick={(e) => e.stopPropagation()}>
<h3>좋아요 누른 사용자</h3>
<ul>
{likedUsers.map((user) => (
<li key={user.userNo}>
<img
src={`${serverUrl}/uploads/${user.profilePicture}`}
alt={`${user.userNickname} 프로필`}
/>
<span>{user.userNickname}</span>
</li>
))}
</ul>
<button onClick={onClose}>닫기</button>
</ModalContent>
</ModalOverlay>
);
};
export default LikedUsersModal;
좋아요 유저 목록 모달은 특정 게시물을 좋아요한 사용자들의 목록을 보여줍니다.
2. 내가 좋아요한 게시물 목록 (LikeList 컴포넌트)
// LikeList 컴포넌트
const LikeList = () => {
const dispatch = useDispatch();
const likeList = useSelector((state) => state.likes.likeList);
const user = useSelector((state) => state.members.user);
useEffect(() => {
if (user) {
dispatch(fetchLikeList(user.userNo));
console.log("라이크리스트", likeList);
}
}, [user, dispatch]);
const sliderSettings = {
/* 슬라이더 설정 생략 */
};
return (
<LikeListBlock>
<div className="top">
<h2>
<FaHeart />
내가 좋아요한 게시물 목록
</h2>
<div className="num">
<span>게시물 {likeList.length}개</span>
</div>
</div>
{likeList.map((post) => (
<PostBlock key={post.postId}>
<PostHeader>
// 생략
</PostHeader>
// 생략
</PostBlock>
))}
</LikeListBlock>
);
};
export default LikeList;
- likeList: 리덕스 상태에서 좋아요한 게시물 목록을 가져옵니다.
3. Redux Slice: 좋아요 상태 관리
// 좋아요 목록을 가져오는 비동기 액션
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)) : 서버에서 받은 데이터를 리덕스 상태로 설정합니다.
백엔드
1. 좋아요 리스트 API앤드포인트
// 좋아요 리스트 엔드포인트
import express from "express";
import { db } from "../db.js";
const otherRouter = express.Router();
otherRouter.post("/post/likeList", (req, res) => {
const userNo = req.body.userNo;
const query = `
SELECT
p.postId,
p.content,
p.userNo,
u.userNickname,
u.profilePicture,
GROUP_CONCAT(i.imageUrl) AS imageUrls,
GROUP_CONCAT(h.tag) AS hashtags,
p.created_at
FROM postlike AS pl
JOIN posts AS p ON pl.postId = p.postId
JOIN users AS u ON p.userNo = u.userNo
LEFT JOIN images AS i ON p.postId = i.postId
LEFT JOIN post_hashtags AS ph ON p.postId = ph.postId
LEFT JOIN hashtags AS h ON ph.hashtagId = h.hashtagId
WHERE pl.userNo = ? AND pl.isLiked = 1
GROUP BY p.postId
ORDER BY p.created_at ASC;
`;
db.query(query, [userNo], (err, result) => {
if (err) {
console.error("에러", err);
res.status(500).send("실패");
} else {
res.send(result);
}
});
});
export default otherRouter;
sql 쿼리문 - SELECT 절
SELECT
p.postId,
p.content,
p.userNo,
u.userNickname,
u.profilePicture,
GROUP_CONCAT(i.imageUrl) AS imageUrls,
GROUP_CONCAT(h.tag) AS hashtags,
p.created_at
- p.postId : 게시물의 고유 ID를 선택합니다.
- p.content : 게시물의 내용을 선택합니다.
- p.userNo : 게시물을 작성한 사용자의 고유 ID를 선택합니다.
- u.userNickname : 게시물을 작성한 사용자의 닉네임을 선택합니다.
- u.profilePicture : 게시물을 작성한 사용자의 프로필 사진을 선택합니다
- GROUP_CONCAT(i.imageUrl) AS imageUrls :
- 해당 게시물에 첨부된 이미지 URL들을 하나의 문자열로 합쳐서 선택합니다. - GROUP_CONCAT(h.tag) AS hashtags:
- 해당 게시물에 연결된 해시태그들을 하나의 문자열로 합쳐서 선택합니다. - p.created_at: 게시물이 작성된 날짜와 시간을 선택합니다.
sql 쿼리문 - FROM 절
FROM postlike AS pl
JOIN posts AS p ON pl.postId = p.postId
JOIN users AS u ON p.userNo = u.userNo
LEFT JOIN images AS i ON p.postId = i.postId
LEFT JOIN post_hashtags AS ph ON p.postId = ph.postId
LEFT JOIN hashtags AS h ON ph.hashtagId = h.hashtagId
- postlike AS pl : postlike 테이블을 pl로 별칭을 설정합니다.
- JOIN posts AS p ON pl.postId = p.postId :
- postlike 테이블과 posts 테이블을 postId를 기준으로 조인합니다.
(특정 게시물에 대한 좋아요 정보와 해당 게시물의 정보를 결합) - JOIN users AS u ON p.userNo = u.userNo :
- posts 테이블과 users 테이블을 userNo를 기준으로 조인합니다.(게시물을 작성한 사용자의 정보를 결합) - LEFT JOIN images AS i ON p.postId = i.postId :
- posts 테이블과 images 테이블을 postId를 기준으로 조인합니다. (게시물에 첨부된 이미지들을 결합)
- LEFT JOIN을 함으로써, 이미지가 없는 게시물도 결과에 포함됩니다. - LEFT JOIN post_hashtags AS ph ON p.postId = ph.postId :
- posts 테이블과 post_hashtags 테이블을 postId를 기준으로 조인합니다. (게시물과 해시태그 간의 관계를 결합) - LEFT JOIN hashtags AS h ON ph.hashtagId = h.hashtagId :
- post_hashtags 테이블과 hashtags 테이블을 hashtagId를 기준으로 조인합니다. (게시물에 연결된 해시태그 정보를 결합)
sql 쿼리문 - WHERE 절
WHERE pl.userNo = ? AND pl.isLiked = 1
- pl.userNo = ? AND pl.isLiked = 1 :
- postlike 테이블에서 userNo가 특정 사용자 번호(?는 실제 값으로 바뀜)이고, isLiked가 1인, 즉 해당 사용자가 좋아요한 게시물만 필터링합니다.
sql 쿼리문 - GROUP BY 절
GROUP BY p.postId
- p.postId :
- 게시물 ID를 기준으로 결과를 그룹화합니다.
- 각 게시물에 대해 한 행씩 결과를 반환하며, GROUP_CONCAT 함수를 통해 각 게시물에 대한 이미지 URL과 해시태그를 문자열로 합칩니다.
sql 쿼리문 - ORDER BY 절
ORDER BY p.created_at ASC;
- p.created_at ASC :
게시물이 작성된 날짜와 시간을 기준으로 오름차순으로 정렬합니다.( 오래된 게시물부터 최신 게시물 순으로 결과를 정렬 )
좋아요 기능을 구현하면서 느낀 점
좋아요 리스트 기능을 구현하는 과정에서 처음으로 복잡한 쿼리문을 작성해보면서 SQL 문법에 대해 더 깊게 이해할 수 있었습니다. 그리고 트랜잭션을 통해 여러 데이터베이스 상태를 수정하면서 그 중요성을 느낄 수 있었습니다.
이렇게 프로젝트를 진행할수록 이론으로만 배웠던 개념들을 실제로 구현하면서 단순히 외우기만 했던 개념들이 피부로 와닿았고, 그만큼 재밌고 신기했습니다.
여러 컴포넌트에서 상태를 공유하고 동기화해야 했기 때문에 중앙 집중식 상태 관리가 필수적이었는데, 이를 리덕스로 해결하면서 리덕스의 강력함과 중요성을 느꼈습니다.
컴포넌트를 재사용할 수 있게 설계하는 것이 얼마나 중요한지 깨달았습니다. 재사용 가능한 컴포넌트를 만들고, 이들의 상태를 리덕스를 통해 상태를 동기화하며 더 효율적이고 모듈화된 코드를 작성하는 방법을 배울 수 있었던거 같아 흥미롭고 굉장히 보람있었던 파트였습니다.