들어가기 전에

리액트 공식문서로 배워보자 #8, 리스트와 키

리스트와 키

처음으로 우리가 어떻게 자바스크립트에서 리스트를 변환했는지 리뷰해봅시다.

아래 주어진 코드를 보면, 우리는 numbers의 배열을 가져와서 그 값을 2배로 증가시키기 위해 map() 함수를 이용했습니다. 우리는 map() 메소드에 의해 반환된 새로운 배열을 double에 할당하고 로깅했습니다.

const numbers = [1, 2, 3, 4, 5];
const doubled = numbers.map((number) => number * 2);
console.log(doubled);

이 코드는 [2, 4, 6, 8, 10]을 콘솔에 로깅합니다.

리액트에서, 배열을 element의 리스트로 변환하는 것은 거의 동일합니다.

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

엘리먼트의 컬렉션을 만들고 {}괄호를 통해 그 엘리먼트들을 JSX에 포함시킬 수 있습니다.

아래에서, 우리는 자바스크립트의 map() 함수를 사용하여 numbers 배열을 반복하였고 각 아이템에 대한 <li> 엘리먼트를 리턴했습니다. 마침내, 우리는 엘리먼트의 결과 배열을 listItems에 할당했습니다.

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

우리는 전체 listItems 배열을 <ui> 엘리먼트 내부에 포함시켰습니다. 그리고 DOM에서 렌더링 시켰습니다.

ReactDOM.render(
  <ul>{listItems}</ul>,
  document.getElementById('root')
);

코드펜에서 직접 해보기

이 코드는 1에서 5까지의 bullet list를 보여줍니다.

기본 리스트 컴포넌트

주로 우리는 리스트를 컴포넌트 내부에 렌더링할 것입니다.

우리는 이전 예제를 numbers 배열을 받고 elements의 리스트를 출력하는 컴포넌트로 리팩토링해볼 것입니다.

function NumberList(props) {
  const numbers = props.numbers;
  const listItems = numbers.map((numbers) =>
    <li>{number}</li>
  );
  return (
    <ul>{listItems}</ul>  
  );
}

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

이 코드를 실행할 때, 리스트 아이템에 대한 key가 제공되어야 한다는 경고메시지를 받게될 것입니다. 엘리먼트의 리스트를 만들 때, "key"는 여러분이 포함시켜야 하는 특별한 문자열 속성입니다. key가 왜 중요한지는 다음 섹션에서 다뤄보겠습니다.

이제 keynumbers.map() 내부의 우리 리스트 아이템에 할당하고 키가 없다고 하는 이슈를 해결해봅시다.

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')
);

코드펜에서 직접 해보기

키(Keys)

키는 리액트가 어떤 아이템이 추가, 변경, 삭제되었는지 구분하는 것을 도와줍니다. 엘리먼트에게 안정적인 구분자(identity)를 주기 위해 엘리먼트의 내부 배열에 주어지는 것이 좋습니다.

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

키를 고르는 최고의 방법은 동등한 레벨의 엘리먼트(siblings) 사이에서 유일하게 그 리스트 아이템을 식별하는 문자열을 사용하는 것입니다. 여러분은 아마 데이터에서 ID를 뽑아 key로 사용하게 될 것입니다.

const todoItems = todos.map((todo) =>
  <li key={todo.id}>
    {todo.text}
  </li>
);

만일 렌더링 하는 아이템에 대한 ID가 없다면, 마지막 방책으로 item index를 키로 사용할 수도 있습니다.

const todoItems = todos.map((todo, index) =>
  // Only do this if items have no stable IDs
  <li key={index}>
    {todo.text}
  </li>
);

만일 아이템의 순서가 변할 수 있다면, index를 키로 사용하는 것을 권장하진 않습니다. 성능에 나쁘게 영향을 미칠 수도 있고 컴포넌트의 상태에 관련된 이슈를 초래할 수도 있습니다. Robin Pokorny의 아티클 (인덱스를 키로 사용하는 것이 왜 나쁜지에 대한 깊이 있는 설명)를 확인해보세요. 만일 명시적으로 키를 리스트 아이템에 할당하지 않기로 했다면, 리액트는 기본값으로 인덱스를 키로 사용할 것입니다.

여기 왜 키가 필수적인가에 대한 깊이 있는 설명이 있으니 확인해보세요.

키로 컴포넌트 추출하기

키는 오직 감싸진 배열의 컨텍스트에서만 영향을 발휘합니다.

예를 들면, 여러분이 ListItem 컴포넌트를 추출한다면, 여러분은 키를 ListItem 자체 내부 <li> 엘리먼트에 갖고 있기 보다는 배열 속 <ListItem /> 엘리먼트에 갖고 있어야 합니다.

예제: 잘못된 키 사용

function ListItem(props) {
  const value = props.value;
  return (
    // Wrong! There is no need to specify the key here:
    <li key={value.toString()}>
      {value}
    </li>
  );
}

function NumberList(props) {
  const numbers = props.numbers;
  const listItems = numbers.map((number) =>
    // Wrong! The key should have been specified here:
    <ListItem value={number} />
  );
  return (
    <ul>
      {listItems}
    </ul>
  );
}

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

예제: 올바른 키 사용

function ListItem(props) {
  // Correct! There is no need to specify the key here:
  return <li>{props.value}</li>;
}

function NumberList(props) {
  const numbers = props.numbers;
  const listItems = numbers.map((number) =>
    // Correct! Key should be specified inside the array.
    <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')
);

경험적으로 좋은 방법은 map() 호출 안에 있는 엘리먼트가 키를 요구하는 것입니다.

같은 레벨의 엘리먼트(Sibling)사이에서 키는 유일해야 한다.

배열 내부에서 사용되는 키는 Sibling사이에서 유일해야 합니다. 하지만 키가 전역적으로 유일할 필요는 없습니다. 두 개의 다른 배열을 생성할 때, 우리는 같은 키를 사용할 수 있습니다.

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')
);

코드펜에서 직접 해보기

키는 리액트에 힌트로서 제공됩니다. 하지만 키는 컴포넌트로 넘어가지 않습니다. 만일 컴포넌트에 같은 값이 필요하다면, prop으로서 명시적으로 다른 이름으로 넘겨주세요.

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

위의 예제와 함께, Post 컴포넌트는 props.id를 읽을 수 있습니다 하지만 props.key는 못 읽습니다.

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>
  );
}

JSX는 {}괄호에 내장된 어떤 표현식을 허락합니다. 그래서 우리는 map() 결과를 내부에서 처리할 수 있습니다.

function NumberList(props) {
  const numbers = props.numbers;
  return (
    <ul>
      {numbers.map((number) =>
        <ListItem key={number.toString()}
                  value={number} />

      )}
    </ul>
  );
}

코드펜에서 해보기

때때로 이러한 코드는 더욱 깔끔한 결과를 낳을 수 있습니다. 하지만 이런 스타일이 남용될 수도 있습니다. 자바스크립트에서처럼, 가독성을 위해 변수를 추출하는 것이 가치가 있는지 없는지는 여러분이 판단할 문제입니다. 언제나 map()의 몸통부분이 너무 nested(들여쓰기가 많이 중첩) 되지는 않았는지 명심해야 합니다. 그렇다면 컴포넌트를 추출하기에 좋은 시기입니다.