React 에서 key값을 index로 하면 안되는 이유
{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를 통해 완성된 페이지가 잘못된 데이터를 보여줄 수 있게하는 원인이 됩니다. 왜 index
에 key
값을 넣는 것이 비효율적인 동작을 야기하는지, 그리고 어떻게 해결할 수 있을지 알아봅시다.
React 트리 변환의 복잡도
하나의 트리를 가지고 다른 트리로 변환하기 위한 최소한의 연산 수를 구하는 알고리즘은 O(n3)의 복잡도를 가집니다. 하지만 React에 적용하기에는 너무나도 비용이 큰 알고리즘입니다.
따라서 React는 두 가지 조건을 두어 O(n) 복잡도를 가지는 알고리즘을 사용합니다.
- 서로 다른 타입의 두 엘리먼트는 서로 다른 트리를 만들어낸다.
- 개발자가
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 값으로 사용해도 문제가 발생할 확률이 낮습니다.
- 배열과 배열의 항목들이 static 하여 변경되지 않는 경우
- 배열의 항목들이 id 값이 없는 경우
- 배열이 절대 재배열되거나 일부가 삭제되지 않는 경우
마무리
React는 트리를 비교할 때, O(n)의 복잡도를 갖는 알고리즘을 사용하고 있고 이에 대한 제약사항으로 사용자는 key
를 두어 여러 렌더링 사이에서 어떤 자식 엘리먼트가 변경되지 않아야 할지 표시합니다.
따라서 배열이 수정되는 경우, 자식 엘리먼트에서 원하지 않는 변경이 일어나지 않도록 index가 아닌 id를 사용하여 key
값을 지정하는 것이 좋습니다.