FrontEnd/React.js

React 에서 key값을 index로 하면 안되는 이유

SambaLim 2021. 7. 29. 10:36
{list.map((item, index) => (<Comp title={item.title} />))}

map 함수를 사용하여 DOM Elements들을 만들 경우 key 값이 없으면 Each child in an array or iterator should have a unique "key" prop. 이라는 에러와 만나게 됩니다.

{list.map((item, index) => (<Comp title={item.title} key={index} />))}

따라서 우리는 종종 배열의 index 값을 사용하여 key 값을 넣어 에러를 피하곤 합니다.

하지만 이렇게 key 값으로 index 를 넣는 것은 React를 통해 완성된 페이지가 잘못된 데이터를 보여줄 수 있게하는 원인이 됩니다. 왜 indexkey 값을 넣는 것이 비효율적인 동작을 야기하는지, 그리고 어떻게 해결할 수 있을지 알아봅시다.

React 트리 변환의 복잡도

하나의 트리를 가지고 다른 트리로 변환하기 위한 최소한의 연산 수를 구하는 알고리즘은 O(n3)의 복잡도를 가집니다. 하지만 React에 적용하기에는 너무나도 비용이 큰 알고리즘입니다.

따라서 React는 두 가지 조건을 두어 O(n) 복잡도를 가지는 알고리즘을 사용합니다.

  1. 서로 다른 타입의 두 엘리먼트는 서로 다른 트리를 만들어낸다.
  2. 개발자가 key prop을 통해, 여러 렌더링 사이에서 어떤 자식 엘리먼트가 변경되지 않아야 할지 표시해줄 수 있다.

서로 다른 타입

두 개의 트리를 비교할때, React는 Element의 루트(Root) 엘리먼트부터 비교합니다. 예를 들어 루트 엘리먼트가 <div> 에서 <span> 으로 변경되는 아래의 예시의 경우 <div> 로 감싸져있는 Counter 는 사라지고 <span> 으로 감싸져있는 새로운 트리를 만들어낼 것입니다.

{/* Before */}
<div>
  <Counter />
</div>

{/* After */}
<span>
  <Counter />
</span>

DOM 엘리먼트의 타입이 같은 경우

같은 타입의 두 React DOM 엘리먼트를 비교할 때, React는 두 엘리먼트의 속성을 확인하여, 변경된 속성들만 갱신합니다.

<div className="before" title="stuff" />

<div className="after" title="stuff" />

위와 같이 두 엘리먼트를 비교한다면 className 만 수정합니다.

style 의 경우는 변경된 style 만 갱신합니다.

<div style={{color: 'red', fontWeight: 'bold'}} />

<div style={{color: 'green', fontWeight: 'bold'}} />

위와 같은 경우는 color 만 변경되고 fontWeight 는 변경되지 않습니다.

같은 타입의 컴포넌트 엘리먼트

컴포넌트의 인스턴스는 동일하게 유지되어 렌더링간 state 가 유지됩니다. React는 새로운 엘리먼트의 수정사항을 반영하기 위해서 인스턴스의 props 만 갱신합니다.

자식에 대한 재귀적 처리

DOM 노드의 처리가 끝나면, React는 이어서 해당 노드의 자식들을 재귀적으로 처리합니다.

이 때, 개발자는 비효율적인 렌더링을 막기 위해 key 를 사용할 수 있습니다. 어떤 것이 비효율적인지 알아보기 위해 예시를 보도록 합시다.

비효율적인 렌더링 예시

{/* Before */}
<ul>
  <li>first</li>
  <li>second</li>
</ul>

{/* After */}
<ul>
  <li>first</li>
  <li>second</li>
  <li>third</li>
</ul>

<ul> 자식의 끝에 <li>third</li> 를 추가한 경우, React는 두 트리에서 <li>first</li> , <li>second</li> 가 일치하는 것을 확인하고 마지막으로 <li>third</li> 를 추가합니다. 이 때 두 트리 사이의 변경은 잘 일어나게됩니다.

{/* Before */}
<ul>
  <li>first</li>
  <li>second</li>
</ul>

{/* After */}
<ul>
  <li>third</li>
  <li>first</li>
  <li>second</li>
</ul>

하지만 위의 예시와 같이 리스트의 앞에 새로운 엘리먼트를 추가하는 경우 성능이 떨어지게 됩니다.

React는 <li>first</li> , <li>second</li> 의 종속 트리는 그대로 유지하겠지만 <ul> 내의 모든 자식들을 변경합니다. 이렇게 비효율적인 렌더링은 문제가 될 수 있습니다.

해결책 key

이러한 비효율적인 렌더링 문제를 해결하기 위해 React는 key 속성을 지원합니다. React는 key 를 통해 기존 트리와 수정된 트리의 자식들이 일치하는지 확인합니다.

{/* Before */}
<ul>
  <li key="1">first</li>
  <li key="2">second</li>
</ul>

{/* After */}
<ul>
  <li key="3">third</li>
  <li key="1">first</li>
  <li key="2">second</li>
</ul>

위의 비효율적인 예시에 key 를 추가한 예시입니다.

key 를 추가하여 React는 key="3" 인 엘리먼트가 새로 추가되었고 key="1" , key="2" 인 엘리먼트는 이동만 하면 된다는 것을 알 수 있습니다.

key 를 추가한 것만으로 비효율을 해결할 수 있습니다.

key값을 배열의 index로 주면 안되는 이유

배열의 index로 key를 사용하면 재배열이 일어날 경우, 컴포넌트의 state 관련하여 문제가 발생할 수 있습니다. 컴포넌트는 key 를 보고 갱신되고 재사용됩니다. index를 사용했다면 항목의 순서가 바뀌었을 경우 key 또한 바뀌었을 거고 이는 state를 엉망으로 만들거나 원하지 않는 방식으로 컴포넌트를 바꿀 수 있습니다.

더 나은 방법

key는 변하지 않고, 예상가능하며, 유일해야합니다. Math.random() 과 같이 변하는 값을 key 로 사용하게되면 많은 컴포넌트 인스턴스와 DOM 노드를 불필요하게 재생성하여 성능이 나빠질 수 있습니다.

따라서 엘리먼트가 식별자를 가지고 있다면 그 식별자를 key 로 사용하는 것이 좋습니다.

<li key={item.id}>{item.name}</li>

예외사항

문제는 배열의 순서가 바뀌어 key 가 바뀌었을 경우 일어나게 됩니다. 따라서 아래와 같은 경우는 index 를 key 값으로 사용해도 문제가 발생할 확률이 낮습니다.

  1. 배열과 배열의 항목들이 static 하여 변경되지 않는 경우
  2. 배열의 항목들이 id 값이 없는 경우
  3. 배열이 절대 재배열되거나 일부가 삭제되지 않는 경우

마무리

React는 트리를 비교할 때, O(n)의 복잡도를 갖는 알고리즘을 사용하고 있고 이에 대한 제약사항으로 사용자는 key 를 두어 여러 렌더링 사이에서 어떤 자식 엘리먼트가 변경되지 않아야 할지 표시합니다.

따라서 배열이 수정되는 경우, 자식 엘리먼트에서 원하지 않는 변경이 일어나지 않도록 index가 아닌 id를 사용하여 key 값을 지정하는 것이 좋습니다.

참고자료