먼저 자바스크립트로 리스트를 다루는 방법을 살펴봅시다.

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

map함수에 익숙하신 분들은 위의 doubled가 [2, 4, 6, 8, 10]인 것을 알 수 있습니다.

리액트에서 리스트를 다룰때도 위와 비슷한 방식을 사용합니다.

Rendering Multiple Components

리액트에선 curly brace({})를 사용하여 JSX가 포함된 여러 요소를 묶을 수 있습니다.

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

이렇게 묶인 listItems배열 전체를 ul 엘리먼트의 하위에 넣을 수 있습니다.

<ul>{listItems}</ul>
<ul>
  <li>1</li>
  <li>2</li>
  <li>3</li>
  <li>4</li>
  <li>5</li>
<ul>

Basic List Component

일반적으로 리스트는 특정 컴포넌트 안에서 렌더링 됩니다.

위에서 살펴본 예시를 컴포넌트 안에서 작성해봅시다.

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];
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(<NumberList numbers={numbers} />);

위 코드를 실행해보면 리스트 아이탬에 키 추가를 요구하는 에러 메시지를 확인할 수 있습니다.

key란 리스트 요소들을 생성할 때 포함되어야 하는 특별한 문자열 attribute입니다. key를 넣어봅시다.

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

이제 에러가 없어진 것을 확인할 수 있습니다.

Keys

key는 리액트가 어떤 아이탬이 추가/수정/삭제 되었는지 알 수 있도록 합니다. key는 배열 안의 요소들에게 stable identity를 제공하기 위해 필요합니다.

stable identity란 being able to see yourself as the same person in the past, present, and future 로 문맥상 요소가 추가/수정/삭제 되어 상태가 변하더라도 이전 요소와 같은 요소로 파악할 수 있도록 도와주는 것입니다. 이것이 key가 필요한 가장 중요한 핵심입니다.

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

따라서 key는 모든 요소별로 유니크하게 독립적으로 존재해야 합니다. 그래야 key를 기준으로 특정 아이탬을 구별할 수 있기 때문입니다. 이는 어플리케이션 전체가 아니라 특정 리스트 안에서만 만족하면 되는 조건입니다.

key값을 정하는 가장 보편적인 방법은 데이터의 id값을 키로 사용하는 것입니다.

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

기본적으로 리스트로 만들어지는 대부분의 데이터는 서버에서 가져온 리소스인 경우가 많습니다. 그런 경우 id값을 이용해 유니크한 값을 정할 수 있습니다.

만약 stable id가 없다면 index를 key로 사용할 수 있습니다.

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

그러나 아이탬이 변경될 수 있는 상황에서는(아마 대부분) 이러한 방식을 추천하지 않습니다. 성능에 안좋은 영향을 끼치고 컴포넌트 상태에 이슈를 만들 수 있기 때문입니다.

key는 React가 DOM elements를 구별하는 유일한 방법입니다. 만약 리스트 중간에 새로운 아이탬을 넣거나, 제거하는 경우 어떻게 될까요? key로 지정된 인덱스가 전부 달라지게 될 것입니다.

리액트는 key가 동일한 경우 DOM element가 동일한 컴포넌트를 나타낸다고 판단합니다. 그러나 인덱스를 이용하면 이는 더이상 참이 아닙니다.

즉, 잘못된 데이터를 표현할 수 있는 가능성이 생기며 렌더링시에도 성능적으로 불리하게 됩니다.

리액트는 특정 key를 정해주지 않으면 암묵적으로 index를 key로 지정합니다. 따라서 주의할 필요가 있습니다.

Extracting Components with Keys

keys는 배열을 둘러싸고 있는 경우에만 의미를 갖습니다.

예를 들어 위의 ListItem 컴포넌트는 element에 key를 가지고 있어야 합니다.

Example: Incorrect Key Usage

function ListItems(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 = number.map((number) =>
    // Wrong! The key should have been specified here:
    <ListItem value={number} />
  );
  return (
    <ul>
      {listItems}
    </ul>
  );

Example: Incorrect Key Usage

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

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

좋은 원칙 중 하나는 map안의 요소들은 key가 필요하다는 것 입니다.

Keys Must Only Be Unique Among Siblings

배열 내에서 사용되는 key는 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.'}
];

const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(<Blog posts={posts} />);

Keys는 리액트에게 stable identity를 제공해주지만 props로 하위 컴포넌트에 전달되지는 않습니다. 만약 key값이 컴포넌트에서 필요하다면, 동일한 값의 다른 이름을 가진 props로 넘겨주어야 합니다.

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

위의 예시에서 컴포넌트는 props.id를 읽을 수 있지만 props.key는 읽지 못합니다.

Embedding map() in JSX

JSX는 curly braces를 포함한 embedding expression을 허용합니다. 따라서 우리는 map()결과를 inline하게 보여줄 수 있습니다.

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

이런 방식은 몇몇 상황에서 코드를 깔끔하게 만들어줄 수 있습니다. 그러나 스타일이 남용될 가능성도 높습니다. 자바스크립트와 마찬가지로 어떻게 가독성을 높일지는 개발자의 선택입니다. map()의 body가 너무 중첩되면 component로 나누는 것이 더 좋을 수 있습니다.

출처

profile
웹 개발을 공부하고 있는 윤석주입니다.

0개의 댓글