데이터 정렬
✒️ 2025-05-28 10:04 내용 수정
데이터 정렬
- 데이터를 Array 객체의 sort() 메소드를 사용하여 특정 기준으로 정렬한다.
- 간단한 블로그 형태 만들기#2. 데이터를 객체 형태로 작성했을 때와는 조금 다른 방법으로 버튼을 누르면 정렬이 되도록 설정했다.
- 위 글에서는
<select>태그를 사용해서 정렬 옵션을 설정했다
- 위 글에서는
- 또한 함수가 많아지면서 함수를 파일로 분리하여 관리하는 방법을 작성했다.
- 샘플 데이터는 codeit에서 제공하는 데이터를 사용했고, json 파일로 저장되어 있어 json 파일로부터 데이터를 가져와 사용하는 방법으로 진행했다.
- API : http://learn.codeit.kr/api/film-reviews
- 데이터를 가져올 때 해당 데이터가 배열로 되어 있는지, 객체로 되어있는지에 따라 가져온 후에 처리 방식이 조금씩 다르기 때문에 유념해야 한다.
- 특히 다른 함수로 데이터를 넘겨주는 경우 props 객체 안에 여러 property가 들어갈 수 있기에 해당 내용 안에서 필요한 내용을 분리할 수 있어야 한다.
1. 오름차순 또는 내림차순만 설정
- 작성하기에 앞서 먼저 태그를 반환하는 함수들을 js파일로 분리시킨다.
- 분리시킨 후엔 import와 export를 작성해주고, index.js파일에도 추가/수정한다.

- index.js 파일 : App대신 Book의 내용만 출력할 예정이므로 App을 주석처리 하거나 제거한다.
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();
- Movie.js : index.js에 내보낼 태그를 작성한다.
- 여러 태그 함수들을 분리하고 데이터를 json 파일로부터 가져와서 길이가 매우 짧아졌다.
- 정렬 시에 state 배열을 그대로 정렬할 경우에 원본 데이터가 변경되기 때문에 배열을 복사하고 setState로 적용하는 것이 좋다.
/* 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;
- MovieBox.js : Movie 함수에서 전체 데이터가 출력될 내용을 작성한 함수다.
- 영화의 아이디, 발매일, 사진, 평점을 간단하게 확인할 수 있다.
- 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;
- MovieDetail.js : 선택한 영화의 상세보기 창을 출력하는 함수다.
- MovieBox에서 영화의 제목을 누르면 상세보기 창이 렌더링되며, 상세보기 표시 상태를 결정하는 detail state 객체가 false일 경우엔 렌더링 되지 않는다.
- 렌더링 여부는 Movie 함수에서 결정된다.
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;
- Movie.css : css를 설정하였으며, 상세보기 창의 경우 창이 뜨면 주변이 어두워지도록 ::after를 추가했다.
*{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;}
- 발매일을 최신순으로 정렬하는 것이 기본 옵션이라 최신순 정렬이 되어 있다.
- id순 정렬을 누르면 id를 오름차순으로 정렬한 결과가 출력된다.
- 베스트순 버튼을 누르면 평점 기준 내림차순으로 정렬된다.
- 영화 제목을 누르면 해당 영화의 상세 내용을 확인할 수 있다.
2. flag에 따른 정렬 옵션 설정
- 새 함수와 파일을 출력하기 위해 index.js를 수정한다.
- Food라는 이름의 태그 결과를 출력할 예정이다.
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();
- Food.js : 전체 항목을 index.html에 출력하기 위한 태그를 작성하는 함수다.
- 이번엔 정렬 순서를 배열로 저장해서 하나의 함수에 매개변수 2개를 받아 정렬을 수행한다.
- 한글 이름을 기준으로 정렬할 때 localeCompare 함수를 사용해야 정렬이 적용되어서 index를 사용해 정렬 수행 코드를 분리했다.
- 배열#Array 객체의 메소드의 sort 항목 참고.
- 정렬 시 원본 배열을 정렬하는 것이 아닌 복사한 배열을 정렬해서 setState로 적용하는 방식을 적용했다.
- 정렬 순서를 배열로 만들었고, 이를 이용해 정렬을 수행할 버튼도 쉽게 만들 수 있다.
- 여러 정렬 기준을 적용하여 경우에 따라 분리하면서 함수 수정이 매우 잦았는데, 처음엔 길더라도 항목별로 따로 onClick 함수를 작성하고, 이후 합칠 수 있는 부분들을 찾아 합치고 조건별 분리를 시키는 방법으로 작성하는 것이 구조 파악에도 도움이 되었다.
/* 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;
- FoodBox.js : 음식 데이터의 세부 내용을 출력하기 위한 함수다.
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;
- Food.css : css 파일로, bootstrap 적용을 기준으로 작성하여 내용이 길지 않다.
*{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;}
- id를 기준으로 오름차순(왼쪽), 내림차순(오른쪽) 정렬
- 이름을 기준으로 오름차순(왼쪽), 내림차순(오른쪽) 정렬
- 칼로리를 기준으로 오름차순(왼쪽), 내림차순(오른쪽) 정렬
- 생성날짜를 기준으로 오름차순(왼쪽), 내림차순(오른쪽) 정렬