Skip to content
This page has been auto-translated and may contain errors.View in English

리스트와 키

할 일 목록을 만들고 있다고 생각해 봅시다. 각 항목이 고유한 텍스트를 가지고 있고, todo 객체의 배열에서 가져온 것입니다. React에는 배열을 마크업으로 변환하는 특별한 리스트 컴포넌트가 없습니다. 일반 JavaScript의 map을 사용해서 각 항목을 JSX 조각으로 변환하고, React가 결과를 렌더링합니다.

jsx
function TodoList({ todos }) {
  return (
    <ul>
      {todos.map(todo => (
        <li key={todo.id}>{todo.text}</li>
      ))}
    </ul>
  )
}

map으로 리스트 렌더링하기

todos 배열은 보통 실제 데이터를 소유하고 있는 부모 컴포넌트에서 prop으로 전달됩니다. todos.map은 배열을 순회하면서 각 todo에 대해 하나의 <li>를 반환합니다. 그 주위의 중괄호는 JSX 요소의 배열을 <ul> 안에 바로 삽입합니다. 단일 표현식을 삽입하는 것과 같은 방식입니다. 각 <li>는 또한 key prop을 가지고 있으며, todo.id로 설정됩니다. 리스트가 제대로 동작하려면 이 부분은 선택사항이 아닙니다. 키를 생략하면 React는 여전히 리스트를 렌더링하지만 콘솔 경고를 남기고, 아래에서 설명하는 재정렬 버그가 리스트가 처음 변할 때 실제 위험이 됩니다.

키는 React가 한 렌더링에서 다음 렌더링으로 리스트 항목을 구분하는 방식입니다. 형제들 사이에서 고유해야 하므로 이 리스트의 두 <li> 요소가 같은 키를 공유할 수 없고, 안정적이어야 합니다. 즉, 같은 todo는 리스트가 다시 렌더링될 때마다 같은 키를 받아야 합니다. todo.id 같은 데이터의 id는 정확히 그러합니다. 배열의 위치가 아니라 todo에 속하므로, 주변 리스트가 변해도 그대로 유지됩니다.

배열 인덱스가 좋지 않은 키인 이유

마지막 점이 배열 인덱스가 좋지 않은 키인 이유입니다. 모든 배열이 이미 가지고 있으므로 그것을 사용하는 것이 유혹적입니다.

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

리스트가 절대 재정렬되지 않고 항목이 삽입되거나 삭제되지 않는 한 이것은 잘 작동합니다. 그 순간, 인덱스는 이전에 가리키던 항목과 일치하지 않게 됩니다. 첫 번째 todo를 삭제하면 모든 남은 항목이 한 칸씩 올라가므로, React는 같은 키가 다른 todo에 붙어 있는 것을 봅니다. 그 리스트 항목들이 자신의 상태를 가지고 있다면, 편집 중인 체크박스나 누군가 입력 중인 input 같은 경우, 그 상태는 인덱스에 붙어 있게 되고 결국 잘못된 행에 끝나게 됩니다. 인덱스를 정적이고 절대 재정렬되지 않는 리스트의 대체제로 취급하세요. 다른 곳에서는 진짜 id를 사용하세요.

좋은 키는 렌더링이 아니라 데이터에서 나옵니다. todo가 API나 데이터베이스에서 오면, 거의 확실하게 이미 id를 가지고 있습니다. 새로운 것을 유도하는 대신 직접 사용하세요. 키는 전체 앱에서만이 아니라 그 하나의 map 호출로 생성된 형제들 사이에서만 고유하면 됩니다. 따라서 할 일 리스트와 같은 데이터에서 만든 완료된 항목 리스트 모두 todo.id로 키를 지정할 수 있습니다. React는 한 번에 하나의 자식 집합 내에서만 키를 비교합니다.

키는 또한 map이 반환하는 요소의 가장 바깥쪽에 있어야 하고, 그 안에 중첩된 것에는 있으면 안 됩니다.

jsx
// 잘못됨: 키가 내부 요소에 있어서 React가 보지 못함
{todos.map(todo => (
  <li>
    <span key={todo.id}>{todo.text}</span>
  </li>
))}

// 올바름: 키가 콜백이 반환하는 가장 바깥쪽 요소에 있음
{todos.map(todo => (
  <li key={todo.id}>{todo.text}</li>
))}

React는 배열의 각 항목의 최상위 요소에서 키를 읽습니다. 자식 요소 안에 묻혀 있는 키는 작동하지 않으며, JSX의 어딘가에 키가 있어도 여전히 "리스트의 각 자식은 고유한 키를 가져야 합니다"라는 경고를 받습니다.

인덱스가 항상 잘못된 것도 아닙니다. 한 번 렌더링되고 절대 재정렬되거나, 필터링되거나, 분할되지 않는 리스트, 예를 들어 푸터의 정적 링크 집합이라면, key={index}는 잘 작동합니다. 인덱스와 가리키는 항목이 절대 벗어나지 않기 때문입니다. 문제는 그 리스트가 형태를 바꿀 수 있는 순간부터 시작됩니다. 행을 재정렬하기, 누군가 입력할 때 검색 결과 필터링하기, 항목 삭제하기. 그 시점에서 인덱스가 지난 렌더링보다 다른 데이터를 가리키기 시작하고, 그때 상태와 DOM 노드가 잘못된 행에 붙게 됩니다.

때때로 데이터에 id가 전혀 없는 경우가 있습니다. 일반 문자열 배열이거나 자연스러운 식별자가 없는 계산으로 만든 배열 같은 경우입니다. 그런 경우, 인덱스로 기본 설정하는 대신 그 리스트에서 실제로 안정적이고 고유한 필드의 조합으로 복합 키를 만드세요. 예를 들어 ${todo.category}-${todo.text} 같은 것입니다.

키는 React의 조정(reconciliation)이 리스트에서 올바르게 작동하게 하는 것입니다. 컴포넌트가 다시 렌더링될 때, React는 필요한 DOM 변경의 최소 집합을 파악하기 위해 새로운 요소 리스트를 이전 것과 비교하고, 키를 사용해서 그 비교에서 요소들을 일치시킵니다. 두 렌더링 모두에 같은 키가 있다는 것은 React가 그것을 같은 요소로 취급한다는 의미입니다. 제자리에서 업데이트되고 DOM 노드, 내부 상태, 그리고 붙어 있는 다른 모든 것을 유지합니다. 이전 렌더링에서 일치하는 키가 없다는 것은 React가 그것을 새로운 것으로 취급하고 새로 마운트한다는 의미입니다. 렌더링 사이에 사라지는 키는 React가 그 요소를 언마운트하고 상태를 버린다는 의미입니다.

키가 잘못되면 바로 이것이 깨집니다. 인덱스 키 뒤에 숨겨진 리스트 항목이 자신의 상태를 가지고 있다고 합시다. 예를 들어 아코디언 행의 "expanded" 플래그입니다. 키를 바꾸지 않으면서 기본 배열을 재정렬하면, React는 여전히 이전 인덱스 2를 새로운 인덱스 2와 일치시킵니다. 그것을 "같은 요소"로 보고, DOM 노드와 상태를 재사용하고, expanded 플래그를 인덱스 2에 있는 todo로 전달합니다. 아무것도 에러를 던지지 않고, 콘솔에서 아무것도 경고하지 않습니다. 행은 조용히 잘못된 상태를 보여주고, 디버깅하는 사람은 보통 처음에는 키 문제를 의심하지 않습니다. 안정적인 id는 배열 위치가 아니라 데이터와 함께 이동하므로 이것을 우회합니다. 따라서 조정은 리스트가 어떻게 재정렬되든 올바른 요소를 올바른 항목과 일치시킵니다.

Juno키는 React에게 어느 항목이 어느 것인지 알려줍니다 배열을 map으로 요소의 리스트로 변환할 때, 각각에 key를 주고, 배열의 위치가 아니라 데이터의 진짜 id를 사용하세요. 키는 React가 렌더링 사이에 어느 요소가 어느 것인지 추적하는 방법이고, 항목에 속하는 id는 리스트가 재정렬되더라도 올바르게 유지됩니다.
Juno키는 React에게 어느 항목이 어느 것인지 알려줍니다 리스트를 렌더링하는 것은 JSX를 반환하는 array.map이고, 각 항목의 가장 바깥쪽 요소에 key가 있습니다. 데이터에서 안정적인 id를 사용하세요. 배열 인덱스는 지름길처럼 보이지만, 리스트가 재정렬되거나 삽입되거나 삭제될 수 있는 순간부터 깨집니다. 인덱스가 더 이상 같은 기본 항목과 일치하지 않기 때문에, 상태가 잘못된 행에 붙게 될 수 있습니다.
Juno키는 React에게 어느 항목이 어느 것인지 알려줍니다 키는 React가 렌더링 전체에서 리스트를 조정할 때 사용하는 식별자입니다. 같은 키면 같은 요소이고 상태와 DOM 노드가 이어집니다. 키가 없으면 언마운트됩니다. 인덱스 키는 절대 변하지 않는 리스트에만 안전합니다. 재정렬되거나, 삽입되거나, 삭제되는 것들은 진짜 안정적인 id가 필요합니다. 그렇지 않으면 상태가 조용히 잘못된 행에 붙게 되고 가리켜 줄 에러가 없습니다.

다음: 상태. 컴포넌트가 렌더링 사이에 정보를 기억하기 시작합니다.