데이터 정렬

✒️ 2025-05-28 10:04 내용 수정


데이터 정렬


1. 오름차순 또는 내림차순만 설정

react blog 9.png

import React from 'react';
import ReactDOM from 'react-dom/client';
import './index.css';
import Movie from './Component/Movie'; // 새로 추가할 Component를 추가한다.
import reportWebVitals from './reportWebVitals';

const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
  <React.StrictMode>
    <Movie /> // Component를 작성한다.
  </React.StrictMode>
);

reportWebVitals();
/* eslint-disable */
import 'bootstrap/dist/css/bootstrap.min.css';
import '../Movie.css';
import {useState} from 'react';
import MovieBox from './MovieBox.js' // 분리한 함수를 import로 가져온다
import MovieDeatil from './MovieDetail.js'
import item from '../data/mock.json' // 외부 json 파일의 데이터를 가져온다

function Movie() {
  let [pick, setPick] = useState(0);
  let [post, setPost] = useState(item);
  let [detail, setDetail] = useState(false);

  // 정렬 state 객체를 만들고, 이를 이용해 정렬 옵션을 설정한다.
  let [order, setOrder] = useState('createdAt');
  // 정렬 기준이 id일 경우에만 오름차순으로 정렬하고, 그 외에는 내림차순 정렬한다.
  // 이 방법을 사용하면 원본 데이터가 바뀐다.
  (order == 'id')
    ? item.sort((a, b) => a[order] - b[order])
    : item.sort((a, b) => b[order] - a[order]);

  // 정렬 옵션은 생성 날짜, 평점, id를 선택할 수 있다.
  const handleNewestClick = () => {setOrder('createdAt');}
  const handleBestClick = () => {setOrder('rating');}
  const handleIdClick = () => {setOrder('id');}

  return (
    <>
      <section className='section sec'>
        <div className='container-lg'>
          <h2 className='title'>영화관</h2>
          <div className='row row-cols-1'>
            <div className='col btn-wrap'>
	            // 각 버튼을 누르면 해당 기준으로 정렬을 수행한다.
                <button className='btn btn-primary' onClick={handleIdClick}>아이디순</button>
                <button className='btn btn-primary' onClick={handleNewestClick}>최신순</button>
                <button className='btn btn-primary' onClick={handleBestClick}>베스트순</button>
              </div>
            {
              post.map((el, i) => {
                return(
                  <MovieBox i={i} post={post} pick={pick} 
                  setPick={setPick}
                  detail={detail} setDetail={setDetail}></MovieBox>
                );
              })
            }
          </div>
          <div className='row'>
            { // detail state 객체가 true일 때만 상세보기 창을 렌더링한다.
              (detail) ? <MovieDeatil i={pick} post={post} detail={detail} setDetail={setDetail}></MovieDeatil> : null
            }
          </div>
        </div>
      </section>
    </>
  );
}

export default Movie;
function MovieBox(props) {
    let {i, post, pick, setPick, detail, setDetail} = props;

	// 영화 제목을 누르면 상세보기 창이 열린다.
	// 상세보기 창은 제목을 누르면 detail state 객체가 true가 되어 렌더링되며
	// 다른 제목을 누르면 해당 제목의 내용이 뜨도록 설정했지만 ::after의 추가로 제거해도 된다.
    function detailOpen(i) {
      if(pick == i) {
        setDetail(!detail);
      } else {
        detail = true;
        setDetail(detail);
      }
      setPick(i);
    }

    return(
      <div className='col box py-3'>
        <div className='gt d-flex'>
          <img className="col-2" src={post[i].imgUrl}></img>
          <div className="text-box">
            <h3 className='post-title' onClick={()=>{detailOpen(i)}}>{post[i].title}</h3>
            <p className="col-2">
              <span className="d-block">아이디 : {post[i].id}</span>
              <span className="d-block">발매일 : {post[i].createdAt}</span>
              <span className="d-block">평점 : {post[i].rating}</span>
            </p>
          </div>
        </div>
      </div>
    )
  }

export default MovieBox;
function MovieDetail(props) {
    let {i, post, detail, setDetail} = props;
  
    return(
      <div className='detail'>
        <div className='gt item'>
          <h3 className='post-title'>{post[i].title}</h3>
          <button className="btn btn-dark close-btn" onClick={()=>setDetail(!detail)}>닫기</button>
          <div className="info d-flex">
            <img className="col-3" src={post[i].imgUrl}></img>
            <div className="text-box">
                <span className="d-block">아이디 : {post[i].id}</span>
                <span className="d-block">발매일 : {post[i].createdAt}</span>
                <span className="d-block">평점 : {post[i].rating}</span>
                <p>{post[i].content}</p>
            </div>
          </div>
        </div>
      </div>
    )
}

export default MovieDetail;
*{margin:0; padding:0; box-sizing: border-box;}
ul, ol, li{list-style: none;}
a{text-decoration: none;}

body{background-color: #d4d4d4;}

.header{
  width: 100%; 
  padding: 30px 0;
  background-color: #444;
}

.header a{color:#fff;}

.sec{ width: 100%; padding:50px 0;}
.sec .row{padding: 40px; background-color: #fff;}
.sec .box{border-bottom: 1px solid #ddd; padding: 5px 0;}
.sec .box h3{font-size: 16px;}
.sec .box p{margin: 0; width: 100%;}

.sec .gt{
  padding: 20px;
  background-color: #fff;
  border-radius: 10px;
}

.sec .btn-wrap{margin:0; padding:0;}
.sec .btn{margin:0 10px;}

/* 영화 제목을 누를 때 드래그를 방지한다 */
.sec .post-title{
  -ms-user-select: none;
  -moz-user-select: none;
  -khtml-user-select: none;
  -webkit-user-select: none;
  user-select: none;
  cursor: pointer;
}

.sec .post-title:hover{
  color:blue;
}

/* 상세보기 창의 위치를 현재 화면의 위치로 고정한다 */
.detail{
  width: 80%; max-width: 1000px;
  position:fixed; left:10%; top:10%;
}

/* 상세보기 창이 뜨면 주변이 어두워지도록 설정한다 */ 
.detail::after{
  content:''; width: 100vw; height: 100vh;
  background-color: rgba(68, 68, 68, 90%);
  position:fixed; left: 0; top: 0;
  z-index: -1; /* z-index 설정을 안 해주면 상세보기 창의 닫기 버튼을 못 누른다 */
}

/* 화면이 특정 크기 이상일 때 상세보기 창의 배치 위치를 설정한다 */
@media screen and (min-width:1200px) {
  .detail{
    position:fixed; left:calc((100% - 1000px) / 2); top:10%;
  }
}

@media screen and (max-width:1000px) {
  .info{flex-direction: column; justify-content: center; align-items: center;}
}

.gt.item{position: relative;}

.btn.close-btn{position: absolute; right: 20px; top:20px;}

.text-box{margin: 0 20px;}

react sort 1.png

react sort 2.png

react sort 3.png

react sort 4.png

react sort 5.png


2. flag에 따른 정렬 옵션 설정

import React from 'react';
import ReactDOM from 'react-dom/client';
import './index.css';
import Food from './Component/Food';
import reportWebVitals from './reportWebVitals';

const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
  <React.StrictMode>
    <Food />
  </React.StrictMode>
);

reportWebVitals();
/* eslint-disable */
import 'bootstrap/dist/css/bootstrap.min.css';
import '../Food.css';
import {useState} from 'react';
import FoodBox from './FoodBox.js'
import item from '../data/sample.json';

function Food() {
  
  let [food, setFood] = useState(item);
  
  // 오름차순/내림차순 여부를 저장하는 flag
  let [flag, setFlag] = useState([true, true, true, true]);

  // 정렬 기준을 저장한 배열과 한글 이름을 저장한 배열
  let order = ['id', 'title', 'calorie', 'createdAt'];
  let orderName = ['아이디', '이름', '칼로리', '생성날짜'];

  // 매개변수에 따른 정렬을 수행하는 함수
  let orderList = (f, i) => { // 플래그와 index를 매개변수로 받는다.
    let copy = [...flag]; // flag를 미리 복사한다.
    
    if (i == 1) { // title 기준 정렬 시 한글 비교를 위해 localeCompare를 사용해야 정확하다
      if (f) { // 오름차순 flag일 때
	    // [...food]로 원본 배열을 복사한 뒤 sort()로 정렬한 결과를 setFood로 state를 바꿔준다.
        setFood([...food].sort((a, b) => {return a[order[i]].localeCompare(b[order[i]])}));
      } else { // 내림차순 flag일 때
        setFood([...food].sort((a, b) => {return b[order[i]].localeCompare(a[order[i]])}));
      }
    } else { // id, calorie, createdAt 기준으로 정렬 시
      if (f) { // 오름차순 flag일 때
        setFood([...food].sort((a, b) => (a[order[i]] - b[order[i]])));
      } else { // 내림차순 flag일 때
        setFood([...food].sort((a, b) => (b[order[i]] - a[order[i]])));
      }
    }

    copy[i] = !copy[i]; // flag를 바꿔준다
    setFlag(copy); // 바꿔준 flag를 setFlag로 적용시켜준다.
  };

  return (
    <>
      <section className='section sec'>
        <div className='container-lg'>
          <h2 className='title'>Food List</h2>
          <div className='btn-wrap'>
            {
              order.map((el, i)=>{
                return( // 정렬 순서 배열을 사용해서 버튼을 4개 만들고, 버튼 이름을 flag에 따라 바뀌도록 설정
                  <button key={i} className='btn btn-primary' onClick={()=>{orderList(flag[i], i)}} >{
                    (flag[i]) ? orderName[i] + ' 오름차순' : orderName[i] + ' 내림차순'
                  } 정렬</button>
                )
              })
            }
          </div>
          <!-- 음식 데이터가 들어가는 ul 태그 -->
          <ul className='row row-cols-1'>
            {
              food.map((el, i) => {
                return( // key 값을 데이터의 id 값으로 설정
                  <FoodBox key={el.id} food={el}></FoodBox>
                );
              })
            }
          </ul>
        </div>
      </section>
    </>
  );
}

export default Food;
function FoodBox(props) {
    let {food} = props; // 전달받은 객체
    let {id, imgUrl, title, content, calorie, createdAt} = food; // 객체 내의 데이터 추출
    let date = new Date(createdAt).toISOString().substring(0, 10); // 생성 날짜를 보기 편한 형식으로 변경

    return(
      <li className='col box'>
        <div className='gt'>
          <img src={imgUrl} alt={title}></img>
          <div className="text-box">
            <ul className="info">
              <li><h3>{title}</h3></li>
              <li>id : {id}</li>
              <li>용량 : {content}</li>
              <li>칼로리 : {calorie}</li>
              <li>생성 날짜 : {date}</li>
            </ul>
          </div>
        </div>
      </li>
    )
  }

export default FoodBox;
*{margin:0; padding:0; box-sizing: border-box;}
ul, ol, li{list-style: none;}
a{text-decoration: none;}

body{background-color: #d4d4d4;}

.sec{ width: 100%; padding: 30px 0;}
.sec .row{padding: 10px 20px;}
.sec .box{padding: 5px 0;}

.sec .btn-wrap .btn{margin:5px;}

.sec .gt{
  padding: 30px;
  display:flex; justify-content: flex-start;
  background-color: #fff;
  border-radius: 10px;
}

.sec .gt img{ width: 50%; max-width: 500px;}

react sort2 1.png react sort2 2.png

react sort2 3.png react sort2 4.png

react sort2 5.png react sort2 6.png

react sort2 7.png react sort2 8.png