Ed Sheeran 명곡들을 유저가 투표할 수 있는 실시간 인기차트 페이지를 구현하려고 한다!
따로 API나 내가 만든 진짜 서버는 없기에, json-server 라이브러리를 이용해 간단한 시뮬레이션 정도 할 수 있는 REST API Mock Server를 이용하려고 한다.
공식문서를 참고해 설치하고,
yarn add json-server
프로젝트 안에 data.json 을 만든다.(공식문서에 있는 예제코드 그대로 복붙)
{
"posts": [
{ "id": 1, "title": "json-server", "author": "typicode" }
],
"comments": [
{ "id": 1, "body": "some comment", "postId": 1 }
],
"profile": { "name": "typicode" }
}
나중에 data.json은 다시 내가 필요한 형식으로 바꿔줬다.
그런 다음엔 아래 명령어를 터미널에 입력해
json-server --watch data.json
JSON SERVER를 실행하면 된다. 매번 저렇게 긴 명령어를 치면 번거롭기 때문package.json에서 수정 가능!
//package.json
{
"name": "app",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "tsc && vite build",
"lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
"preview": "vite preview",
"start": "json-server --watch data.json --port 3000"
},...
}
인기차트에서 구현하고 싶었던 건, 첫째로, 유저가 좋아요를 클릭하면 좋아요 숫자가 1 올라가고, 다시 한 번 클릭하면 해제되는 거였다.
둘째로, 좋아요 숫자에 따라 실시간으로 순위가 변동될 수 있도록 하고 싶었다.
마지막으로, 새로고침해도 상태는 유지되어야 했다.
처음 작성했던 코드는 다음과 같았다.
function Chart() {
const [songs, setSongs] = useState<Song[]>([]);
const [isClicked, setIsClicked] = useState<number | null>(null);
const handleLikeClick = async (
songId: number,
title: string,
img: string,
album: string
) => {
setIsClicked((prevSongId) => (prevSongId === songId ? null : songId));
const updatedSongs = songs.map((song) => {
if (song.id === songId) {
return { ...song, likes: song.likes + 1 };
}
return song;
});
setSongs(updatedSongs);
try {
await updateLikes(
songId,
updatedSongs.find((song) => song.id === songId)?.likes || 0,
title,
img,
album
);
} catch (error) {
console.error('Error updating likes:', error);
}
};
useEffect(() => {
getSongs()
.then((data) =>
setSongs(data)
)
.catch((error) => {
console.error('Error fetching songs:', error);
});
}, []);
return (
<S.StyledTableContainer>
<Table>
<TableHead>
<TableRow hover={true}>
<TableCell>순위</TableCell>
<TableCell align='right'>곡 정보</TableCell>
<TableCell align='right'>앨범</TableCell>
<TableCell align='right'>좋아요</TableCell>
</TableRow>
</TableHead>
<TableBody>
{songs &&
songs.map((song, index) => (
<TableRow key={song.id}>
<TableCell>{index + 1}</TableCell>
{isClicked === song.id ? (
<GoHeartFill
onClick={() =>
handleLikeClick(
song.id,
song.title,
song.img,
song.album
)
}
style={{ cursor: 'pointer' }}
/>
) : (
<GoHeart
onClick={() =>
handleLikeClick(
song.id,
song.title,
song.img,
song.album
)
}
style={{ cursor: 'pointer' }}
/>
)}
{song.likes}
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</S.StyledTableContainer>
);
}
export default Chart;
그러자 곡 A의 좋아요를 누르면, 곡 B의 좋아요가 해제되었다. 모든 곡을 하나의 isClicked에 담아 관리해서 나타나는 문제였다.
그래서 상태를 하나 더 추가해서 각각의 노래에 대한 하트 클릭 상태와 좋아요 수를 관리하기로 했다.
const [clickedSongs, setClickedSongs] = useState<number[]>([]);
const [songLikes, setSongLikes] = useState<number[]>([]);
그리고 handleLikcClick 함수를 수정해줬다.
const handleLikeClick = async (
songId: number,
title: string,
img: string,
album: string
) => {
if (clickedSongs.includes(songId)) {
// 이미 클릭한 노래의 하트를 다시 클릭하면 클릭 상태를 해제
setClickedSongs((prevClickedSongs) =>
prevClickedSongs.filter((id) => id !== songId)
);
// 좋아요 수 -1
setSongLikes((prevSongLikes) =>
prevSongLikes.map((likes, index) =>
index === songId ? likes - 1 : likes
)
);
} else {
// 클릭하지 않았던 노래의 하트를 클릭하면 클릭 상태 추가
setClickedSongs((prevClickedSongs) => [...prevClickedSongs, songId]);
// 좋아요 수 +1
setSongLikes((prevSongLikes) =>
prevSongLikes.map((likes, index) =>
index === songId ? likes + 1 : likes
)
);
}
sort 함수를 이용해 setSongs의 songs를 다시 만들어줬다
getSongs()
.then((data) =>
setSongs(data.slice().sort((a: Song, b: Song) => b.likes - a.likes))
)
그리고 마지막으로 새로고침해도 계속 상태 유지하도록 하는 기능은 생각보다 시간이 좀 걸렸다ㅠㅠ