React 리스트와 Key

0

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

기존의 JavaScript에서는 다음 코드와 같이 map() 함수를 이용하여 numbers 배열의 값을 두배로 만든 후 map()에서 반환하는 새 배열을 doubled 변수에 할당하고 로그를 확인하는 방식으로 리스트를 변환합니다.

기존 Javascript 방식의 리스트 변환
const numbers = [1, 2, 3, 4, 5];
const doubled = numbers.map((number) => number * 2);
console.log(doubled); // [2, 4, 6, 8, 10]

React에서 배열을 엘리먼트 리스트로 만드는 방식도 map() 함수를 이용하는데, 위 코드의 Javascript 방식과 거의 동일합니다.

React는 엘리먼트 모음을 만들고 중괄호{}를 이용하여 JSX에 포함시킬 수 있는데, 다음 코드와 같이 JavaScript의 map() 함수를 사용하여 numbers 배열을 반복 실행하면 각 항목에 대해 <li> 엘리먼트를 반환하고 엘리먼트 배열의 결과를 listItems에 저장할 수 있습니다.

엘리먼트 배열의 결과 반환 후 저장하기
const numbers = [1, 2, 3, 4, 5];
const listItems = numbers.map((number) =>
  <li>{number}</li>
);

그 다음 listItems 배열을 <ul>엘리먼트 안에 포함하고 DOM에 렌더링하면, 1부터 5까지의 숫자로 이루어진 리스트가 출력되는 것을 확인할 수 있습니다.

저장된 엘리먼트 출력 후 렌더링하기
ReactDOM.render(
  <ul>{listItems}</ul>,
  document.getElementById('root')
);

기본 리스트 컴포넌트

일반적으로는 컴포넌트 안에서 리스트를 렌더링하는데, 앞의 예제를 다음과 같이 numbers 배열을 받아 순서 없는 엘리먼트 리스트를 출력하는 컴포넌트로 리팩토링할 수 있습니다.

배열을 받아 순서없는 엘리먼트 리스트 출력하기
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')
);

위 코드를 실행하면 리스트의 각 항목에 key를 넣어야 한다는 경고가 표시됩니다. key는 엘리먼트 리스트를 만들 때 포함해야 하는 특수한 문자열 어트리뷰트라고 할 수 있는데, 다음과 같이 numbers.map() 안에 있는 리스트의 각 항목에 key를 할당하면 key 누락 문제를 해결할 수 있습니다.

key를 추가한 배열 엘리먼트 리스트 예제
function NumberList(props) {
  const numbers = props.numbers;
  const listItems = numbers.map((number) =>
    <li key={number.toString()}>
      {number}
    </li>
  );
  return (
    <ul>{listItems}</ul>
  );
}

const numbers = [1, 2, 3, 4, 5];
ReactDOM.render(
  <NumberList numbers={numbers} />,
  document.getElementById('root')
);

예제를 실행해 보면 경고없이 리스트가 잘 출력되는 것을 확인할 수 있습니다.

Key

Key는 React가 어떤 항목을 변경, 추가 또는 삭제할지 식별하는 것을 돕는 속성으로, 엘리먼트에 안정적인 고유성을 부여할 수 있도록 배열 내부의 엘리먼트에 지정해 주어야 합니다.

배열 내부의 엘리먼트에 Key 지정하기
const numbers = [1, 2, 3, 4, 5];
const listItems = numbers.map((number) =>
  <li key={number.toString()}>
    {number}
  </li>
);

Key를 선택하는 가장 좋은 방법은 리스트의 다른 항목들 사이에서 해당 항목을 고유하게 식별할 수 있는 문자열을 사용하는 것인데, 대부분 데이터의 IDkey로 사용하는 것이 가장 좋은데, 렌더링 한 항목에 대한 안정적인 ID가 없는 경우에는 항목의 인덱스key로 사용할 수도 있습니다.

엘리먼트의 key를 id로 사용하는 경우
const todoItems = todos.map((todo) =>
  <li key={todo.id}>
    {todo.text}
  </li>
);
엘리먼트의 key를 index로 사용하는 경우
const todoItems = todos.map((todo, index) =>
  // 항목에 안정적인 ID가 없는 경우에만 사용
  <li key={index}>
    {todo.text}
  </li>
);

하지만 항목의 순서가 바뀔 수 있는 경우에는 key에 인덱스를 사용하는 것은 권장하지 않는데, 이로 인해 성능이 저하되거나 컴포넌트의 state와 관련된 문제가 발생할 수 있기 때문입니다.

만약 리스트 항목에 명시적으로 key를 지정하지 않으면 React는 기본적으로 인덱스key로 사용하게 됩니다.

Key로 컴포넌트 추출하기

키는 주변 배열의 context에서만 의미가 있는데, 이는 ListItem 컴포넌트를 추출 한 경우 ListItem 안에 있는 <li> 엘리먼트가 아닌 배열의 <ListItem /> 엘리먼트가 key를 가져야 한다는 의미입니다.

key의 잘못된 사용법
function ListItem(props) {
  const value = props.value;
  return (
    // (X) 여기에는 key를 지정할 필요가 없음
    <li key={value.toString()}>
      {value}
    </li>
  );
}

function NumberList(props) {
  const numbers = props.numbers;
  const listItems = numbers.map((number) =>
    // (X) 여기에 key를 지정해야 됨
    <ListItem value={number} />
  );
  return (
    <ul>
      {listItems}
    </ul>
  );
}

const numbers = [1, 2, 3, 4, 5];
ReactDOM.render(
  <NumberList numbers={numbers} />,
  document.getElementById('root')
);

위의 코드는 key의 잘못된 사용 예를 보여주는데, key올바른 사용법은 다음과 같습니다.

key의 올바른 사용법
function ListItem(props) {
  // (O) 여기에는 key를 지정할 필요가 없음
  return <li>{props.value}</li>;
}

function NumberList(props) {
  const numbers = props.numbers;
  const listItems = numbers.map((number) =>
    // (O) 배열 안에 key를 지정
    <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')
);

Key의 고유값 설정 범위

Key는 배열 안의 형제 사이에서는 고유해야 하지만 전체 범위에서 고유할 필요는 없습니다.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.'}
];
ReactDOM.render(
  <Blog posts={posts} />,
  document.getElementById('root')
);

React에서 key는 힌트를 제공하지만 컴포넌트로 전달하지는 않는데, 컴포넌트에서 key와 동일한 값이 필요하면 다른 이름의 prop을 명시적으로 전달하면 됩니다. 다음의 코드에서 Post 컴포넌트는 props.id를 읽을 수 있지만 props.key는 읽을 수 없습니다.

컴포넌트에 key 전달하기
const content = posts.map((post) =>
  <Post
    key={post.id}
    id={post.id}
    title={post.title} />
);

JSX에 map() 포함시키기

별도의 listItems 변수 선언 후 JSX에 포함시키기
function NumberList(props) {
  const numbers = props.numbers;
  const listItems = numbers.map((number) =>
    <ListItem key={number.toString()}
              value={number} />
  );
  return (
    <ul>
      {listItems}
    </ul>
  );
}

위 코드는 별도의 listItems 변수를 선언하고 이를 JSX에 포함했는데, JSX를 사용하면 중괄호{} 안에 모든 표현식을 포함 시킬 수 있기 때문에 map() 함수의 결과를 인라인으로 처리할 수 있습니다.

JSX에 표현식 포함시키기
function NumberList(props) {
  const numbers = props.numbers;
  return (
    <ul>
      {numbers.map((number) =>
        <ListItem key={number.toString()}
                  value={number} />
      )}
    </ul>
  );
}

이 방식을 사용하면 코드가 더 깔끔해 지지만, 남발하는 것은 좋지 않습니다. JavaScript와 마찬가지로 가독성을 위해 변수로 추출해야 할지 아니면 인라인으로 넣을지는 개발자가 직접 판단할 필요가 있는데, map() 함수가 너무 중첩되는 경우라면 컴포넌트로 추출 하는 것을 고려해 보는 것이 좋습니다.

답글 남기기