2023.02.01 리스트와 키
리스트를 위해 사용하는 자료구조가 바로 배열(Array)입니다.
배열은 자바스크립트의 변수나 객체를 하나의 변수로 묶어놓은 것입니다.
const numbers = [1,2,3,4,5];
먼저 JavaScript에서 리스트를 어떻게 변환하는지 살펴봅시다.
아래는 map()함수를 이용하여 numbers 배열의 값을 두배로 만든 후
map()에서 반환하는 새 배열을 doubled 변수에 할당하고 로그를 확인하는 코드입니다.
const numbers = [1, 2, 3, 4, 5];
const doubled = numbers.map((number) => number * 2);
console.log(doubled);
이 코드는 콘솔에 [2, 4, 6, 8, 10]을 출력합니다.
React에서 배열을 엘리먼트 리스트로 만드는 방식은 이와 거의 동일 합니다.
프로그래밍에서 키는 각 객체나 아이템을 구분할 수 있는 고유한 값을 의미합니다.
리액트에서는 위에서 나온 배열과 키를 사용하여 반복되는
다수의 엘리먼트를 쉽게 렌더링할 수 있습니다.
같은 컴포넌트를 화면에 반복적으로 나타내야 할 경우에 이를 코드상에 하나씩
직접 넣는 것은 같은 코드가 반복되기 때문에 굉장히 비효율적입니다.
이러한 경우에 사용하는 것이 바로 자바스크립트 배열의 map( ) 함수입니다.
map( ) 함수는 배열에 들어있는 각 변수에 어떤 처리를 한 뒤 리턴하는 것입니다.
엘리먼트 모음을 만들고 중괄호 {}를 이용하여 JSX에 포함 시킬 수 있습니다.
예제 코드를 봅시다.
const doubles = numbers.map((number) => number * 2);
위 코드는 map()함수를 사용하여 numbers라는 배열에 들어있는 각 숫자에 2를 곱한 값이 들어간 doubled라는 배열을 생성하는 코드입니다.
이처럼 map() 함수는 배열의 첫 번째 아이템부터 순서대로 각 아이템에 어떠한 연산을 수행한 뒤에 최종 결과를 배열로 만들어서 리턴해준다고 보면 됩니다.
그렇다면 실제로 리액트에서는 이 map()함수를 어떻게 사용하여 엘리먼트를 렌더링하는지 예제 코드를 보도록 합시다.
아래의 JavaScript map() 함수를 사용하여 numbers 배열을 반복 실행합니다.
각 항목에 대해 <li> 엘리먼트를 반환하고 엘리먼트 배열의 결과를 listItems에 저장합니다.
const numbers = [1, 2, 3, 4, 5];
const listItems = numbers.map((number) =>
<li>{number}</li>
);
그러면 <ul> 엘리먼트 안에 전체 listItems 배열을 포함할 수 있습니다.
ReactDOM.render(
<ul>{listItems}</ul>,
document.getElementById('root')
);
이 코드는 1부터 5까지의 숫자로 이루어진 리스트를 보여줍니다.
결과적으로 렌더링되는 코드는 다음과 같습니다.
ReactDOM.render(
<ul>
<li>{1}</li>
<li>{2}</li>
<li>{3}</li>
<li>{4}</li>
<li>{5}</li>
</ul>,
document.getElementById('root')
);
일반적으로 컴포넌트 안에서 리스트를 렌더링합니다.
이전 예시를 numbers 배열을 받아서 순서 없는 엘리먼트 리스트를 출력하는 컴포넌트로 리팩토링할 수 있습니다.
function NumberList(props) {
const {numbers} = props
const {listItems} = numbers.map((number) =>
<li>{number}</li>
);
return (
<ul>{listItems}</ul>
);
}
const numbers = [1, 2, 3, 4, 5];
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(<NumberList numbers={numbers} />);
NumberList 컴포넌트는 props로 숫자가 들어있는 배열인 numbers를 받아서 이를 목록으로 출력합니다.
이 NumberList 컴포넌트를 사용하면 numbers 배열에 숫자가 수십 개 또는 수백개가 되어도 별도의 코드를 작성할 필요 없이 화면에 렌더링할 수가 있습니다.
그런데 이 코드를 실행하면 리스트의 각 항목에 key를 넣어야 한다는 경고가 표시됩니다.
Each child in a list should have a unique "key" prop
리스트 아이템에는 무조건 키가 있어야 한다.
“key”는 엘리먼트 리스트를 만들 때 포함해야 하는 특수한 문자열 어트리뷰트입니다.
다음 섹션에서 key의 중요성에 대해서 더 설명하겠습니다.
이제 numbers.map() 안에서 리스트의 각 항목에 key를 할당하여 키 누락 문제를 해결하겠습니다.
function NumberList(props) {
const numbers = props.numbers;
const listItems = numbers.map((number) =>
<li key={number.toString()}>
{number}
</li>
);
return (
<ul>{listItems}</ul>
);
}
리액트에서의 키는 리스트에서 아이템을 구분하기 위한 고유한 문자열이라고 이해하면 된다.
Key는 React가 어떤 항목을 변경, 추가 또는 삭제할지 식별하는 것을 돕습니다.
리액트에서의 키의 값을 같은 리스트에 있는 엘리먼트 사이에서만 고유한 값이면 됩니다.
key는 엘리먼트에 안정적인 고유성을 부여하기 위해 배열 내부의 엘리먼트에 지정해야 합니다.
const numbers = [1, 2, 3, 4, 5];
const listItems = numbers.map((number) =>
<li key={number.toString()}>
{number}
</li>
);
위에 코드는 키값으로 숫자의 값을 사용한 것입니다.
만약 numbers 배열에 중복된 숫자가 들어있다면 키값도 중복되기 때문에 고유해야 한다는 키값의 조건이 충족되지 않습니다.
Key를 선택하는 가장 좋은 방법은 리스트의 다른 항목들 사이에서 해당 항목을 고유하게 식별할 수 있는 문자열을 사용하는 것입니다.
대부분의 경우 데이터의 ID를 key로 사용합니다.
const todoItems = todos.map((todo) =>
<li key={todo.id}>
{todo.text}
</li>
);
렌더링 한 항목에 대한 안정적인 ID가 없다면 최후의 수단으로 항목의 인덱스를 key로 사용할 수 있습니다.
const todoItems = todos.map((todo, index) =>
// Only do this if items have no stable IDs
<li key={index}>
{todo.text}
</li>
);
항목의 순서가 바뀔 수 있는 경우 key에 인덱스를 사용하는 것은 권장하지 않습니다.
이로 인해 성능이 저하되거나 컴포넌트의 state와 관련된 문제가 발생할 수 있습니다.
참고로 리스트 항목에 명시적으로 key를 지정하지 않으면 React는 기본적으로 인덱스를 key로 사용합니다.
여기서 꼭 기억해야 할 점은
map( ) 함수 안에 있는 엘리먼트는 꼭 key가 필요하다는 것입니다.
키는 주변 배열의 context에서만 의미가 있습니다.
예를 들어 ListItem 컴포넌트를 추출 한 경우 ListItem 안에 있는 <li> 엘리먼트가 아니라 배열의 <ListItem /> 엘리먼트가 key를 가져야 합니다.
예시: 잘못된 Key 사용법
function ListItem(props) {
const value = props.value;
return (
// 틀렸습니다! 여기에는 key를 지정할 필요가 없습니다.
<li key={value.toString()}>
{value}
</li>
);
}
function NumberList(props) {
const numbers = props.numbers;
const listItems = numbers.map((number) =>
// 틀렸습니다! 여기에 key를 지정해야 합니다.
<ListItem value={number} />
);
return (
<ul>
{listItems}
</ul>
);
}
예시: 올바른 Key 사용법
function ListItem(props) {
// 맞습니다! 여기에는 key를 지정할 필요가 없습니다.
return <li>{props.value}</li>;
}
function NumberList(props) {
const numbers = props.numbers;
const listItems = numbers.map((number) =>
// 맞습니다! 배열 안에 key를 지정해야 합니다.
<ListItem key={number.toString()} value={number} />
);
return (
<ul>
{listItems}
</ul>
);
}
경험상 map() 함수 내부에 있는 엘리먼트에 key를 넣어 주는 게 좋습니다.
Key는 배열 안에서 형제 사이에서 고유해야 하고 전체 범위에서 고유할 필요는 없습니다. 두 개의 다른 배열을 만들 때 동일한 key를 사용할 수 있습니다.
function Blog(props) {
const sidebar = (
<ul>
{props.posts.map((post) =>
<li key={post.id}>
{post.title}
</li>
)}
</ul>
);
const content = props.posts.map((post) =>
<div key={post.id}>
<h3>{post.title}</h3>
<p>{post.content}</p>
</div>
);
return (
<div>
{sidebar}
<hr />
{content}
</div>
);
}
const posts = [
{id: 1, title: 'Hello World', content: 'Welcome to learning React!'},
{id: 2, title: 'Installation', content: 'You can install React from npm.'}
];
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(<Blog posts={posts} />);
React에서 key는 힌트를 제공하지만 컴포넌트로 전달하지는 않습니다. 컴포넌트에서 key와 동일한 값이 필요하면 다른 이름의 prop으로 명시적으로 전달합니다.
const content = posts.map((post) =>
<Post
key={post.id}
id={post.id}
title={post.title} />
);
위 예시에서 Post 컴포넌트는 props.id를 읽을 수 있지만 props.key는 읽을 수 없습니다.
위 예시에서 별도의 listItems 변수를 선언하고 이를 JSX에 포함했습니다.
function NumberList(props) {
const numbers = props.numbers;
const listItems = numbers.map((number) =>
<ListItem key={number.toString()}
value={number} />
);
return (
<ul>
{listItems}
</ul>
);
}
JSX를 사용하면 중괄호 안에 모든 표현식을 포함 시킬 수 있으므로 map() 함수의 결과를 인라인으로 처리할 수 있습니다.
function NumberList(props) {
const numbers = props.numbers;
return (
<ul>
{numbers.map((number) =>
<ListItem key={number.toString()}
value={number} />
)}
</ul>
);
}
이 방식을 사용하면 코드가 더 깔끔해 지지만, 이 방식을 남발하는 것은 좋지 않습니다. JavaScript와 마찬가지로 가독성을 위해 변수로 추출해야 할지 아니면 인라인으로 넣을지는 개발자가 직접 판단해야 합니다.
map() 함수가 너무 중첩된다면 컴포넌트로 추출 하는 것이 좋습니다.
import React from "react";
const students = [
{
name: "Inje"
},
{
name: "Steve"
},
{
name: "Biil"
},
{
name: "Jeff"
}
];
function AttendanceBook(props) {
return (
<ul>
{students.map((student) => {
return <li>{student.name}</li>
})}
</ul>
);
}
export default AttendanceBook;
AttendanceBook 컴포넌트는 students라는 배열로부터 학생 정보가 담긴
객체를 받아 학생들의 이름을 목록 형태로 출력하는 컴포넌트입니다.
여기에서 배열을 렌더링하기 위해 map( ) 함수를 사용한 것을 볼 수 있습니다.
수정 후 브라우저를 확인해 보면 목록은 나오지만 경고 문구가 나오는 것을 확인할 수 있다.
이를 고치기 위해서 각 학생 객체에 고유한 값을 가진 id를 추가해 주고
map( )함수의 엘리먼트에 key={student.id} 를 넣어줍니다.
이렇게 하면 학생의 아이디가 키 값으로 사용됩니다.
AttendanceBook.jsx에 id 추가
import React from "react";
const students = [
{
id: 1,
name: "Inje"
},
{
id: 2,
name: "Steve"
},
{
id: 3,
name: "Biil"
},
{
id: 4,
name: "Jeff"
}
];
function AttendanceBook(props) {
return (
<ul>
{students.map((student) => {
return <li key={student.id}>{student.name}</li>
})}
</ul>
);
}
export default AttendanceBook;
위에서 쓴 key를 id말고 포맷팅 된 문자열로 변경
{students.map((student) => {
return <li key={`student-id-${student.id}`}>{student.name}</li>
})}
key를 index로 변경할 수도있다.
{students.map((student, index) => {
return <li key={index}>{student.name}</li>
})}
숫자 값을 사용
id를 사용
인덱스를 사용