React 공식 튜토리얼을 바탕으로, 필요한 개념을 보충하여 학습한 기록입니다.
map()
메서드의 쓰임자바스크립트에서 리스트를 수정 및 조작하기 위해서는 map()
메서드를 자주 사용하게 된다.
map()
메서드는, 배열 내의 모든 요소 각각에 대하여 주어진 함수를 호출한 결과를 모아 새로운 배열을 반환한다.
arr.map(callback(currentValue[, index[, array]])[, thisArg])
callback
currentValue
index
array
map()
을 호출한 배열.thisArg
callback
을 실행할 때 this
로 사용되는 값.callback
의 결과를 모은 새로운 배열.이 map()
메서드는 원본을 수정하지 않고 새로운 배열을 대신 리턴한다는 점에서 유용하다.
리액트에서 map()
메서드를 사용하면, 일반 데이터 배열을 원본으로 둔 상태에서, 데이터를 리액트 엘리먼트로 변환한 배열을 새롭게 리턴받을 수 있다. 이 때 원래의 일반 데이터 배열은 수정이나 변경 없이 원본 상태로 유지된다. 따라서 불변성을 지키며 동적인 배열을 렌더링하는 데 유리하다.
리액트에서는 map()
메서드를 사용해 데이터가 담긴 배열을 반복 실행하여, 각 항목에 대해 새로운 리액트 엘리먼트를 반환하여 엘리먼트 배열을 생성할 수 있다.
이러한 과정은 독립적인 컴포넌트로 리팩토링 될 수도 있다.
const numbers = [1, 2, 3, 4, 5]
const listItems = numbers.map((number) =>
<li>{number}</li>
);
ReactDOM.render(
<ul>{listItems}</ul>,
document.getElementById('root')
);
numbers
배열을 만들었다.map()
메서드를 사용하여 numbers
배열을 반복 실행한다.<li>
엘리먼트를 반환하고, 그 결과로 5개의 <li>
엘리먼트를 갖는 배열을 생성해 listItems
변수에 저장하였다.number
와 listItems
모두 자바스크립트 표현식 이므로, 엘리먼트 내부에서 사용하기 위해 반드시 중괄호 {}
로 감싸서 사용해야 한다는 점에 유의해야 한다.리액트에서는, 일반적으로 컴포넌트 안에서 리스트를 렌더링한다. 예를 들어, 앞서 본 예제 코드를 하나의 컴포넌트로 리팩토링해볼 수 있다.
function NumberList(props) {
const numbers = props.numbers;
const listItems = numbers.map((number) =>
<li>{number}</li>
);
return (
<ul>{listItems}</ul>
);
}
const numbers = [1, 2, 3, 4, 5];
ReactDOM.render(
<NumberList numbers={numbers} />,
document.getElementById('root')
);
map
함수를 사용하여 배열의 각 요소에 대해<li>
엘리먼트를 반환하고,<ul>
엘리먼트로 리턴하는 컴포넌트를 작성하였다.여기서 위 코드를 실행하면 리스트의 각 항목에 key
를 넣어야 한다 라는 경고가 표시된다. key
란, 엘리먼트 리스트를 만들 때 포함해야 하는 특수한 문자열 어트리뷰트다.
<li key={number.toString()}>
{number}
</li>
key
는 리액트가 어떤 항목을 변경, 추가 또는 삭제할지 식별하는 것을 돕는다. 즉, 컴포넌트 리스트를 렌더링 했을 때 어떤 원소에 변동이 있었는지 알아내기 위해 사용한다.
유동적인 데이터를 다룰 때, 리스트의 중간에 새로운 컴포넌트가 추가될 수도, 삭제될 수도 있다. key
가 없는 경우에는, 변경된 부분 이하 리스트 요소에 전부 영향을 준다. (참고: 리액트의 Key를 알아보자)
하지만 key
를 사용하면 배열이 업데이트될 때마다 변경되지 않은 값들은 그대로 두고, 원하는 내용을 삽입하거나 삭제할 수 있다. 엘리먼트에 고유성이 부여되기 때문이다. 따라서 key
는 엘리먼트에 안정적인 고유성을 부여하기 위해 배열 내부의 엘리먼트에 지정해야 한다.
const numbers = [1, 2, 3, 4, 5];
const listItems = numbers.map((number) =>
<li key={number.toString()}>
{number}
</li>
);
key
를 선택하는 방법으로는 데이터의 ID와 같이 리스트의 다른 항목들 사이에서 해당 항목을 고유하게 식별할 수 있는 문자열을 사용하는 것이 권장된다.
렌더링한 항목에 대한 안정적인 ID가 없다면 프로그래머의 재량에 따라 항목의 인덱스를 key
로 사용할 수도 있다. 다만 항목의 순서가 바뀔 수 있는 경우 key
에 인덱스를 사용하는 것은 권장되지 않는다.
key
로 인덱스를 사용하고 있던 배열에서, 항목의 순서가 바뀌는 경우를 생각해 보자:
todos.map((todo, index) => (
<Todo {...todo} key={index} />
));
}
위의 코드에서 만약 todos
배열의 중간에서 다른 항목이 삽입되거나 기존 항목이 삭제되는 일이 일어나면 어떻게 될까?
컴포넌트는 key
를 보고 갱신되고 재사용된다. 즉 key
란 리액트가 DOM 엘리먼트를 식별하기 위한 값이다. 따라서 어떤 요소가 삭제되었다면 해당 요소의 key
도 함께 사라져야 하고, 추가되었다면 기존 요소들의 key
와 중복되지 않는, 새로운 값을 key
로 가져야 한다.
그러나 key
로 인덱스를 사용하면 중간에 어떤 요소가 삽입 또는 삭제되었을 때 기존 요소가 쓰던 key
를 새 요소가 사용하게 되거나 (삽입된 경우), 삭제되어 없어져야 하는 key
를 다른 요소가 받아서 사용하는(삭제된 경우) 일이 생길 수 있다.
이와 같은 식으로 성능이 저하되거나 컴포넌트의 state와 관련된 문제가 발생할 수 있으므로 인덱스를 key
로 사용하는 것은 권장되지 않는다. 다만 리스트 항목에 명시적으로 key
를 지정하지 않으면 리액트는 기본적으로 인덱스를 key
로 사용한다.
"키는 주변 배열의 context 에서만 의미가 있다". 아래 코드를 보자.
function ListItem(props) {
return <li>{props.value}</li>;
}
function NumberList(props) {
const numbers = props.numbers;
const listItems = numbers.map((number) =>
<ListItem key={number.toString()} value={number} />
);
return (
<ul>
{listItems}
</ul>
);
}
const numbers = [1, 2, 3, 4, 5];
ReactDOM.render(
<NumberList numbers={numbers} />,
document.getElementById('root')
);
ListItem
: props로 value
를 받아서 <li>
엘리먼트를 반환하는 컴포넌트NumberList
: props로 배열 numbers
를 받아서, 각 요소에 대해 반환된 <ListItem>
엘리먼트들로 이루어진 배열 listItems
를 반환하는 컴포넌트위 코드는 배열의 각 요소를 각각 엘리먼트로 반환하는 부분만 ListItem
컴포넌트로 따로 추출한 경우다. 여기서 ListItem
안에 있는 <li>
엘리먼트가 아니라 배열의 <ListItem />
엘리먼트가 key
를 가진 것을 알 수 있다.
즉 배열이 만들어지는 (여기서는 map()
메서드 내부) 맥락에 해당하는 부분에 적절히 key
를 부여해야 한다. 여기서 배열을 이루는 것은 <li>
엘리먼트가 아니라 <Listitem>
이다.
앞서 말했듯, key
는 한 배열 내부에서 요소들을 식별하기 위해 사용된다. 따라서 동일한 배열 내부의 형제 사이에서 고유해야 하지만, 전체 범위에서 고유할 필요는 없다. 즉 두 개의 다른 배열을 만들 때 동일한 key
를 사용할 수 있다.
리액트에서 key
는 요소를 구분할 수 있도록 일종의 힌트 를 제공하는 셈이지만, 그 값을 컴포넌트로 전달하지는 않는다. 컴포넌트에서 key
와 동일한 값이 필요한 경우, 다른 이름의 prop으로 명시적으로 전달해야 한다.
const content = posts.map((post) =>
<Post
key={post.id}
id={post.id}
title={post.title} />
);
위 예시에서 Post
컴포넌트는 props.id
를 읽을 수 있지만 props.key
는 읽을 수 없다.