리액트 간단한 기능 구현해보기
리액트로 게시물 정렬 및 좋아요 기능 구현하기
요구사항
기능
- 최근 등록순(최신 게시물부터 오래된 게시물 순으로 정렬), 조회순으로 정렬시키기
- 좋아요 기능 (좋아요를 누르면 좋아요 수 변경 및 좋아요 UI 변경)
조건
- mock data를 사용
구현
1. 최근 등록순, 조회순으로 정렬시키기
주어진 html 코드
1
2
3
4
5
6
<div>
<select id="sort_type">
<option value="1">최근등록순</option>
<option value="2">조회순</option>
</select>
</div>
#변경되는 정렬 타입(최근 등록순, 조회순)을 담을 변수 만들기
1
const [sortType, setSortType] = useState<number>(1);
#select에 onChange 적용하기
1
2
3
4
5
6
<div>
<select id="sort_type" onChange={(e) => setSortType(Number(e.target.value))}>
<option value="1">최근등록순</option>
<option value="2">조회순</option>
</select>
</div>
#정렬하는 함수 만들기
switch문을 사용해서 id별로 정렬하는 로직 작성
1
2
3
4
5
6
7
8
9
10
11
12
13
14
const sortedData = (data: ICard[]) => {
const copyData = [...data];
switch (sortType) {
case 1:
return copyData.sort(
(a, b) =>
new Date(b.created_at).getTime() - new Date(a.created_at).getTime()
);
case 2:
return copyData.sort((a, b) => b.views - a.views);
default:
return data;
}
};
#게시글을 표시할 때 정렬 함수를 사용하여, 정렬된 게시글을 표시할 수 있도록 하기
1
2
3
4
5
<div className="posts-container">
{sortedData(sortData).map((post) => (
<Card {...post} key={post.id} handleToggleLiked={handleToggleLiked} />
))}
</div>
2. 좋아요 기능 (좋아요를 누르면 좋아요 수 변경 및 좋아요 UI 변경)
#좋아요 여부에 따라 다른 UI 표시하기
1
<p className="card-liked">{liked ? "👍" : "👎"}</p>
#좋아요 기능 만들기
mock data를 사용하기 때문에, mock data를 반복해서 돌면서 상태 변경
1
2
3
4
5
6
7
8
9
10
11
12
13
const handleToggleLiked = (id: number) => {
const newData = sortData.map((post) =>
post.id === id
? {
...post,
liked: !post.liked,
likes: !post.liked ? post.likes + 1 : post.likes - 1
}
: post
);
setSortData(newData);
};
생각해보면 좋은 포인트
정렬된 데이터 최적화
- useMemo를 사용하여 정렬된 데이터를 메모
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
const [sortType, setSortType] = useState<number>(1);
const sortedData = useMemo(() => {
const copyData = [...sortData];
switch (sortType) {
case 1:
return copyData.sort(
(a, b) =>
new Date(b.created_at).getTime() - new Date(a.created_at).getTime()
);
case 2:
return copyData.sort((a, b) => b.views - a.views);
default:
return copyData;
}
}, [sortData, sortType]);
- useEffect를 사용해서 sortType가 변경되었을 때만 재렌더하기
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
const [sortedData, setSortedData] = useState<ICard[]>([]);
const [sortType, setSortType] = useState<number>(1);
useEffect(() => {
const handleSortData = (data: ICard[]) => {
const copyData = [...data];
switch (sortType) {
case 1:
return copyData.sort(
(a, b) =>
new Date(b.created_at).getTime() - new Date(a.created_at).getTime()
);
case 2:
return copyData.sort((a, b) => b.views - a.views);
default:
return data;
}
};
setSortedData(handleSortData(dbData));
}, [sortType]);
단순한 정렬 작업이나 데이터가 메모리에 로드된 경우에는 useMemo
, 데이터가 비동기로 로드되거나 정렬과 관련해서 추가 로직들을 작성해야한다면 useEffect
로 최적화 하는 것이 좋음!
코드
전체 코드
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
// App.tsx
import { useState } from "react";
import "./App.css";
import Card, { ICard } from "./components/Card";
import dbData from "./data/data.json";
function App() {
const [sortData, setSortData] = useState<ICard[]>(dbData ?? []);
const [sortType, setSortType] = useState<number>(1);
const sortedData = (data: ICard[]) => {
const copyData = [...data];
switch (sortType) {
case 1:
return copyData.sort(
(a, b) =>
new Date(b.created_at).getTime() - new Date(a.created_at).getTime()
);
case 2:
return copyData.sort((a, b) => b.views - a.views);
default:
return data;
}
};
const handleToggleLiked = (id: number) => {
const newData = sortData.map((post) =>
post.id === id
? {
...post,
liked: !post.liked,
likes: !post.liked ? post.likes + 1 : post.likes - 1
}
: post
);
setSortData(newData);
};
return (
<div className="container">
<div>
<select
id="sort_type"
onChange={(e) => setSortType(Number(e.target.value))}
>
<option value="1">최근등록순</option>
<option value="2">조회순</option>
</select>
</div>
<div className="posts-container">
{sortedData(sortData).map((post) => (
<Card {...post} key={post.id} handleToggleLiked={handleToggleLiked} />
))}
</div>
</div>
);
}
export default App;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
// Card.tsx
import "./Card.css";
export interface ICard {
id: number;
title: string;
likes: number;
created_at: string;
liked: boolean;
views: number;
}
interface CardProps extends ICard {
handleToggleLiked: (id: number) => void;
}
function Card(props: CardProps) {
const { title, views, created_at, liked, likes, id, handleToggleLiked } =
props;
return (
<li className="card-container" id={`card${id}`}>
<h2>{title}</h2>
<p className="card-liked" onClick={() => handleToggleLiked(id)}>
{liked ? "👍" : "👎"}
</p>
<p>{new Date(created_at).toLocaleString()}</p>
<div className="card-view-likes">
<p>좋아요 {likes}</p>
<p>조회수 {views}</p>
</div>
</li>
);
}
export default Card;
mock data
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
[
{
id: 1,
title: "첫 번째 게시글",
likes: 10,
created_at: "2024-07-15T10:00:00Z",
liked: false,
views: 150
},
{
id: 2,
title: "두 번째 게시글",
likes: 5,
created_at: "2024-07-14T09:30:00Z",
liked: true,
views: 200
},
{
id: 3,
title: "세 번째 게시글",
likes: 20,
created_at: "2024-07-13T08:20:00Z",
liked: false,
views: 250
},
{
id: 4,
title: "네 번째 게시글",
likes: 8,
created_at: "2024-07-12T07:15:00Z",
liked: true,
views: 180
},
{
id: 5,
title: "다섯 번째 게시글",
likes: 15,
created_at: "2024-07-11T06:10:00Z",
liked: false,
views: 300
},
{
id: 6,
title: "여섯 번째 게시글",
likes: 3,
created_at: "2024-07-10T05:05:00Z",
liked: true,
views: 90
},
{
id: 7,
title: "일곱 번째 게시글",
likes: 12,
created_at: "2024-07-09T04:00:00Z",
liked: false,
views: 240
},
{
id: 8,
title: "여덟 번째 게시글",
likes: 7,
created_at: "2024-07-08T03:50:00Z",
liked: true,
views: 160
},
{
id: 9,
title: "아홉 번째 게시글",
likes: 25,
created_at: "2024-07-07T02:40:00Z",
liked: false,
views: 310
},
{
id: 10,
title: "열 번째 게시글",
likes: 30,
created_at: "2024-07-06T01:30:00Z",
liked: true,
views: 500
}
];
This post is licensed under CC BY 4.0 by the author.