[React][공식문서] 리스트와 Key

Gyuwon Lee·2022년 6월 21일
0
post-thumbnail

React 공식 튜토리얼을 바탕으로, 필요한 개념을 보충하여 학습한 기록입니다.

1. map() 메서드의 쓰임

자바스크립트에서 리스트를 수정 및 조작하기 위해서는 map() 메서드를 자주 사용하게 된다.

map() 메서드는, 배열 내의 모든 요소 각각에 대하여 주어진 함수를 호출한 결과를 모아 새로운 배열을 반환한다.

arr.map(callback(currentValue[, index[, array]])[, thisArg])
  • callback
    • 새로운 배열 요소를 생성하는 함수로, 아래의 세 인자를 갖는다:
    • currentValue
      • 처리할 현재 요소.
    • index
      • 처리할 현재 요소의 인덱스.
    • array
      • map() 을 호출한 배열.
  • thisArg
    • callback 을 실행할 때 this 로 사용되는 값.
  • 리턴값:
    • 배열의 각 요소에 대해 실행한 callback 의 결과를 모은 새로운 배열.

map() 메서드는 원본을 수정하지 않고 새로운 배열을 대신 리턴한다는 점에서 유용하다.

리액트에서 map() 메서드를 사용하면, 일반 데이터 배열을 원본으로 둔 상태에서, 데이터를 리액트 엘리먼트로 변환한 배열을 새롭게 리턴받을 수 있다. 이 때 원래의 일반 데이터 배열은 수정이나 변경 없이 원본 상태로 유지된다. 따라서 불변성을 지키며 동적인 배열을 렌더링하는 데 유리하다.


2. 리스트 컴포넌트 in React

리액트에서는 map() 메서드를 사용해 데이터가 담긴 배열을 반복 실행하여, 각 항목에 대해 새로운 리액트 엘리먼트를 반환하여 엘리먼트 배열을 생성할 수 있다.

이러한 과정은 독립적인 컴포넌트로 리팩토링 될 수도 있다.

⚙️ 여러개의 컴포넌트 렌더링하기

const numbers = [1, 2, 3, 4, 5]
const listItems = numbers.map((number) => 
  <li>{number}</li>
);

ReactDOM.render(
  <ul>{listItems}</ul>,
  document.getElementById('root')
);
  • 정수 데이터 5개로 이루어진 numbers 배열을 만들었다.
  • map() 메서드를 사용하여 numbers 배열을 반복 실행한다.
    • 즉, 배열의 각 요소에 대해 <li> 엘리먼트를 반환하고, 그 결과로 5개의 <li> 엘리먼트를 갖는 배열을 생성해 listItems 변수에 저장하였다.
  • 이 때, 배열 numberlistItems 모두 자바스크립트 표현식 이므로, 엘리먼트 내부에서 사용하기 위해 반드시 중괄호 {} 로 감싸서 사용해야 한다는 점에 유의해야 한다.

⚙️ 기본 리스트 컴포넌트

리액트에서는, 일반적으로 컴포넌트 안에서 리스트를 렌더링한다. 예를 들어, 앞서 본 예제 코드를 하나의 컴포넌트로 리팩토링해볼 수 있다.

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')
);
  • props로 배열을 받아,
    1. map 함수를 사용하여 배열의 각 요소에 대해<li> 엘리먼트를 반환하고,
    2. 해당 엘리먼트들로 구성된 새로운 배열을 만들어,
    3. 이를 <ul> 엘리먼트로 리턴하는 컴포넌트를 작성하였다.

여기서 위 코드를 실행하면 리스트의 각 항목에 key 를 넣어야 한다 라는 경고가 표시된다. key 란, 엘리먼트 리스트를 만들 때 포함해야 하는 특수한 문자열 어트리뷰트다.

<li key={number.toString()}>
  {number}
</li>

3. Key

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 로 사용한다.

🔑 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 는 요소를 구분할 수 있도록 일종의 힌트 를 제공하는 셈이지만, 그 값을 컴포넌트로 전달하지는 않는다. 컴포넌트에서 key 와 동일한 값이 필요한 경우, 다른 이름의 prop으로 명시적으로 전달해야 한다.

const content = posts.map((post) =>
  <Post
    key={post.id}
    id={post.id}
    title={post.title} />
);

위 예시에서 Post 컴포넌트는 props.id 를 읽을 수 있지만 props.key 는 읽을 수 없다.


참고:
Index as a key is an anti-pattern

profile
하루가 모여 역사가 된다

0개의 댓글