Post

리액트 간단한 기능 구현해보기

리액트로 게시물 정렬 및 좋아요 기능 구현하기

요구사항

기능

  1. 최근 등록순(최신 게시물부터 오래된 게시물 순으로 정렬), 조회순으로 정렬시키기
  2. 좋아요 기능 (좋아요를 누르면 좋아요 수 변경 및 좋아요 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.