🎯 목표
1. 데이터의 배열(array) 리스트 동적으로 렌더링하기
대부분의 웹 어플리케이션에서는 얼마나 많은 아이템을 렌더링해야 할지 미리 알 수 없다. 예를들어 사용자들이 얼마나 많이 expenses를 추가할지, 특정 연도가 선택되었을 때 얼마나 많은 expenses가 보여지게 될지 알 수 없다.
예제를 보자. 현재 App.js에 있는 expenses 배열의 데이터를 props인 items={expenses}
을 통해 <Expenses />
로 전달하였다. 그리고 Expenses.js에서는 각각의 아이템을 출력하기 위해 <ExpenseItem />
을 네 번 호출하고 있다.
import Card from "../UI/Card";
import "./Expenses.css";
import ExpenseItem from "./ExpenseItem";
import ExpensesFilter from "./ExpensesFilter";
import { useState } from "react";
const Expenses = (props) => {
const [filteredYear, setFilteredYear] = useState("2020");
const filterChangedYearHandler = (selectedYear) => {
console.log("Expenses.js");
console.log("selectedYear", selectedYear);
setFilteredYear(selectedYear);
};
console.log("filteredYear", filteredYear);
return (
<Card className="expenses">
<ExpensesFilter
onChangeFilterYear={filterChangedYearHandler}
selected={filteredYear}
/>
<ExpenseItem
id={props.items[0].id}
title={props.items[0].title}
amount={props.items[0].amount}
date={props.items[0].date}
/>
<ExpenseItem
id={props.items[1].id}
title={props.items[1].title}
amount={props.items[1].amount}
date={props.items[1].date}
/>
<ExpenseItem
id={props.items[2].id}
title={props.items[2].title}
amount={props.items[2].amount}
date={props.items[2].date}
/>
<ExpenseItem
id={props.items[3].id}
title={props.items[3].title}
amount={props.items[3].amount}
date={props.items[3].date}
/>
</Card>
);
};
export default Expenses;
현재는 네 개의 항목에 대해 개별적으로 <ExpenseItem />
컴포넌트를 네 번 호출하고 있는데, 이런식으로 하드 코딩하는 것은 지양해야 한다. 대신, 데이터 목록(List)을 동적으로 렌더링해야 한다.
배열의 요소별로 하나씩 을 렌더링하기 위해 JS 내장 함수인 map() 메소드
를 사용하면 된다.
- Array.prototype.map()
map()
메서드는 호출한 배열의 모든 요소에 대해, 함수를 호출한 결과로 채워진 새 배열을 만든다.
즉, 호출한 배열을 기반으로 새로운 배열을 생성하는데, 원본 배열에 있는 모든 요소들을 변환(transform)한다.const array1 = [1, 4, 9, 16]; // map()은 매개변수로 인자를 전달하는 함수를 취한다. // 이 함수는 배열에 있는 모든 요소에 실행되고 그 결과값을 담은 배열을 반환한다. const map1 = array1.map(x => x * 2); console.log(map1); // 함수의 결과값: 새로운 값을 담은 새로운 배열을 반환한다. // Array [2, 8, 18, 32]
✅ expenses 배열에 있는 각 요소를 <ExpensesItem />
으로 렌더링하고 싶기 때문에 매핑할 JSX 요소인 <ExpensesItem />
를 map()
의 매개변수로 전달하는 함수의 리턴 값으로 넣어주면 된다.
const expensesArray = props.items;
expensesArray.map((expense) => (/*매핑할 JSX 요소 반환*/);
//...
return (
<Card className="expenses">
<ExpensesFilter
onChangeFilterYear={filterChangedYearHandler}
selected={filteredYear}
/>
{props.items.map((expenses) => (
<ExpenseItem
id={expenses.id}
title={expenses.title}
amount={expenses.amount}
date={expenses.date}
/>
))}
</Card>
);
};
export default Expenses;
❌ Warning: Each child in a list should have a unique "key" prop.
리액트는 데이터의 목록을 렌더링할 때 특별한 개념을 가진다. 이는 리액트가 발생할 수 있는 성능 손실이나 버그 없이 효과적으로 목록을 업데이트하고 렌더링할 수 있도록 보장하기 위해 존재한다.
새로운 데이터가 추가되면 배열의 목록이 추가 되면서 state가 업데이트 되어 화면에 새로운 요소가 렌더링 되는데, 이때 key props 값
을 설정하지 않으면 약간의 문제가 발생한다.
🌀 성능 악화!
개발자모드에서 Elements 탭에 들어가면, 새로운 아이템을 추가할 때 리액트가 얼마나 버벅이고 있는지 알 수 있다. 리액트가 보기에는 아이템도 비슷비슷하고 배열이 길어지고 있기 때문에, 렌더링 할때 새로운 아이템을 div 목록의 마지막 아이템으로 렌더링한 후, 모든 아이템 지나면서 배열에 있는 컨텐츠와 다시 일치시키기 위해 모든 아이템을 업데이트해서 컨텐츠를 교체한다.
댓츠 낫굿.. 댓츠 노노 🥺! 성능면에서 퀄리티가 있다고 할 수 없다.
🌀 버그 발생 위험!
map()
을 통해 목록으로 렌더링되고 있는 <ExpenseItem />
컴포넌트의 내부에 state로 관리되고 있는 것이 있다면 새 아이템이 이전 아이템을 덮어쓰는 현상이 일어날 수도 있다. 잠재적으로 버그가 발생할 수도 있는 것이다.
🥺 💬 "리액트야 왜 그렇게 바보 같이 굴어! 좀 알잘딱깔센하면 안되겠니!" 싶지만, 리액트에게는 다른 방법이 없다. 리액트는 현재 간단히 배열의 길이를 체크하고 이미 렌더링된 아이템의 수만 확인할 수 있을 뿐이다. 리액트에겐 각각의 아이템이 너무나 비슷해 보이기 때문에 새로운 아이템이 어느 위치에 추가되어야 하는지는 알 수가 없다. 그렇기 때문에 리액트에게 key props
값을 줘서 이 아이템은 어디에 들어가야해! 라고 알려줘야 한다.
✅ 아이템 목록이 출력되는 곳에서, map()
을 호출할때, 즉 <ExpenseItem />
컴포넌트를 호출할 때 key props
값을 설정해 주면 된다.
{props.items.map((expenses) => (
<ExpenseItem
key={expenses.id}
id={expenses.id}
title={expenses.title}
amount={expenses.amount}
date={expenses.date}
/>
))}
이렇게 하면 이제 리액트는 모든 아이템을 식별할 수 있고 배열의 길이 뿐만 아니라 아이템이 어디에 위치해야 할지도 인식할 수 있게 된다! 따라서 좀더 효율적인 방법으로 업데이트 할 수 있게 된다. 😎
특별히 key의 값은 유니크해야 한다. 따라서 고유식별자인 id 값을 넣어주는게 가장 좋다.
map((i, index) => (...))
이 전달하는 함수의 두 번째 인자
를 사용할 수도 있는데, 이는 map에 전달하는 함수에서 자동으로 얻어지는 인덱스 값이다.일반적으로 디비에서 가져오는 데이터는 고유 id가 있으므로 key 값은 고유식별자인 id값으로 주도록 하자!