React

[React] 배열 컴포넌트 사용 시 key 값으로 index를 사용하면 안되는 이유

킹우현 2023. 7. 17. 14:58

key prop Error

Element 배열 사용 시에 배열의 각 요소에 대해 key 값을 prop으로 전달해주지 않으면 위와 같은 에러가 발생하게 된다.

 

React 에서의 key

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

key는 React가 어떤 항목을 변경, 추가 또는 삭제할지 식별하는 것을 도와줍니다. key는 Element에 안정적인 고유성을 부여하기 위해 배열 내부의 Element에 지정해야 합니다.

 

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

Key를 선택하는 가장 좋은 방법은 리스트의 다른 항목들 사이에서 해당 항목을 고유하게 식별할 수 있는 문자열을 사용하는 것입니다. 대부분의 경우 데이터의 ID를 key로 사용합니다.

👉🏻 key 속성에는 해당 배열 내부에 각기 고유(uniquq) 값을 넣어 주어야 합니다.

 

key를 지정해야 하는 이유

DOM 노드의 자식들을 재귀적으로 처리할 때, React는 기본적으로 동시에 두 리스트를 순회하고 차이점이 있으면 변경을 생성합니다.

 

예를 들어, 자식의 끝에 엘리먼트를 추가하면, 두 트리 사이의 변경은 잘 작동할 것입니다.

<ul>
  <li>first</li>
  <li>second</li>
</ul>

<ul>
  <li>first</li>
  <li>second</li>
  <li>third</li>
</ul>

React는 두 트리에서 <li>first</li>가 일치하는 것을 확인하고, <li>second</li>가 일치하는 것을 확인합니다. 그리고 마지막으로 <li>third</li>를 트리에 추가합니다.

 

하지만 위와 같이 key 값을 전달하지 않고 구현하게 되면, 리스트의 맨 앞에 엘리먼트를 추가하는 경우 성능이 좋지 않게 됩니다. 예를 들어, 아래의 두 트리 변환은 비효율적으로 작동합니다.

<ul>
  <li>Duke</li>
  <li>Villanova</li>
</ul>

<ul>
  <li>Connecticut</li> // 리스트의 맨 앞에 Element 추가
  <li>Duke</li>
  <li>Villanova</li>
</ul>

React는 <li>Duke</li> <li>Villanova</li> 를 포함한 모든 자식을 변경하게 됩니다. 이러한 비효율은 문제가 될 수 있습니다.

 

이러한 문제를 해결하기 위해, React는 key 속성을 지원합니다. 자식들이 key를 가지고 있다면, React는 key를 통해 기존 트리와 이후 트리의 자식들이 일치하는지 확인합니다. 예를 들어, 위 비효율적인 예시에 key를 추가하여 트리의 변환 작업이 효율적으로 수행되도록 수정할 수 있습니다.

<ul>
  <li key="2015">Duke</li>
  <li key="2016">Villanova</li>
</ul>

<ul>
  <li key="2014">Connecticut</li>
  <li key="2015">Duke</li>
  <li key="2016">Villanova</li>
</ul>

이제 React는 '2014' key를 가진 엘리먼트가 새로 추가되었고, '2015' '2016' key를 가진 엘리먼트는 이미 기존 트리에 존재하기 때문에 이동만 하면 되는 것을 알 수 있습니다.

 

다시 말해, 3개의 리스트를 가진 변수를 통해 key가 없이 배열 렌더링을 진행하게 한다면 해당 리스트 변수에 1개가 맨 앞에 추가되는 경우에 React 는 총 4개를 처음부터 다시 리렌더링 하게 됩니다. 

 

하지만 key를 지정한다면 기존의 요소들은 변경되지 않았다는걸 React 에서 자동으로 파악한 후 새로 생기는 요소에 대해서만 렌더링을 진행하게 됩니다.

 

즉, 단순히 key 요소만 추가한 것만으로도 더욱 최적화된 렌더링을 진행할수 있습니다 ! ✨

 

map 속성에서의 index를 통한 key 주입

const arrayData = [{...},{...}];
const items = arrayData.map((obj, index) =>
  <li key={index}>
    {obj.name}
  </li>
);

배열을 통한 컴포넌트를 만들어야 하는 경우 해당 object 에 고유 값이 무엇인지 모를 경우 map의 두번째 인자인 index 속성을 통하여 위와 같이 key 값을 지정하곤 합니다.

 

항목들이 재배열되지 않는다면 이 방법도 잘 동작할 것이지만, 재배열되는 경우 비효율적으로 동작할 것입니다.

 

해당 예시대로 작성할 경우 단순한 리스트 렌더링 역할만 한다면 문제가 생기지 않지만 정렬, 추가, 삭제 등의 작업이 있는 경우 예상치 못한 문제가 발생할 수 있습니다.

 

배열의 index를 통한 key 주입 시 생기는 문제

import React, { useState } from 'react';

function PersonList() {
  const [personList, setPersonList] = useState([{ name: '성인'}, { name: '혜진' }]);

  // 사람 추가 이벤트 
  const addPerson = () => {
    setPersonList((prev) => [{ name: '추가된사람', description: '추가된 사람 입니다.' }, ...prev]);
  };

  return (
    <div>
      <h2>배열 index key 예시</h2>
      <button onClick={addPerson}>사람 추가</button>
      {personList.map((person, index) => {
        return (
          <div key={index}>
            <span>{person.name}</span>
            <input type='text' />
          </div>
        );
      })}
    </div>
  );
}

export default PersonList;

배열의 index를 key 값으로 전달하였을 때 배열의 내부에 새로운 값이 추가가 되거나 삭제될 경우에 항목의 순서(index)가 바뀌게 되어 기존의 key 값이 변경되는 상황이 발생하게 됩니다.

 

이때 React는 key가 동일할 경우, 동일한 DOM Element를 보여주기 때문에 예상치 못한 문제가 발생합니다.

 

컴포넌트 인스턴스는 key를 기반으로 갱신되고 재사용됩니다. 인덱스를 key로 사용하면, 항목의 순서가 바뀌었을 때 key 또한 바뀔 것입니다. 그 결과로, 컴포넌트의 state가 엉망이 되거나 의도하지 않은 방식으로 바뀔 수도 있습니다.

항목의 순서가 바뀔 수 있는 경우 key에 인덱스를 사용하는 것은 권장하지 않습니다. 이로 인해 성능이 저하되거나 컴포넌트의 state와 관련된 문제가 발생할 수 있습니다. (출처 : React 공식문서)

 

리스트의 아이템 요소에 유니크 값이 없는 경우

import { nanoid } from 'nanoid';

function PersonList() {
  const [personList, setPersonList] = useState([{ name: '성인'}, { name: '혜진' }]);

  // 사람 추가 이벤트 
  const addPerson = () => {
    setPersonList((prev) => [{ name: '추가된사람', description: '추가된 사람 입니다.' }, ...prev]);
  };

  return (
    <div>
      <h2>배열 index key 예시</h2>
      <button onClick={addPerson}>사람 추가</button>
      {personList.map((person, index) => {
        return (
          // nanoid 를 사용한 고유 key 부여 
          <div key={nanoid()}>
            <span>{person.name}</span>
            <input type='text' />
          </div>
        );
      })}
    </div>
  );
}

export default PersonList;

대부분의 경우는 각 배열 요소의 고유한 유니크 키 값이 존재할테지만 간혹 이렇지 못한 경우가 존재하기 마련입니다.

 

이런 경우는 nanoid 와 같은 uniqueID 생성 라이브러리를 통해 key 속성을 주입하면 됩니다.

 

$ npm install nanoid
import { nanoid } from "nanoid"; // ESM
// const { nanoid } = require("nanoid"); // CommonJS

const id = nanoid();
console.log(id); // '8SbSCpLGGyCaMNXlfb1ZS'
NanoID는 21바이트 길이로 UUID보다 적은 비용으로 계산이 가능하기 때문에 최근에는 많은 프로젝트에서는 UUID 대신에 NanoID를 식별자 생성기로 채택하고 있습니다.

 

결론

배열의 각 요소가 수정, 삭제, 추가 등의 기능이 없거나 정렬 및 필터 요소가 없는 단순 렌더링, 즉 기존 index가 변경되지 않는 경우라면 key 값으로 index를 사용하여도 무방하지만, 이러한 경우가 아니라면 key 값으로 가급적 index를 사용하지 않는 것이 좋다 :)