-
[React] key값으로 index를 지양하는 이유공부 2021. 10. 18. 21:01
면접을 보면서 리액트는 key값을 index로 사용하는 것을 지양하는 이유에 대한 질문을 받았다. key값으로 index밖에 사용하지 않았던지라 그러한 사실을 인지하지 못했고, 답변을 할 수 없었다. 그래서 이번 기회를 통해 그 이유를 알아보고자 한다.
리액트에서 key값을 index로 지정하지 않는 이유를 이해하기 위해선 리액트가 O(n)의 알고리즘 복잡도를 구현한다는 것을 알아야 한다. 하나의 트리를 다른 트리로 변환하기 위한 최소한의 연산 수를 구하는 일반적인 알고리즘도 n개의 엘리먼트가 있는 트리에 대해 O(n³)의 복잡도를 가진다. 하지만 리액트는 아래의 두 가정을 기반하여 O(n) 복잡도를 갖는 알고리즘을 구현한다.
1. 서로 다른 타입의 두 엘리먼트는 서로 다른 트리를 만들어 낸다.
2. 개발자가 key prop을 통해 여러 렌더링 사이에서 어떤 자식 엘리먼트가 변경되지 않아야 할지 표시해 줄 수 있다.특히 2번의 이유때문에 리액트에서 key값을 index로 사용하는 것을 지양한다.
비교알고리즘
리액트는 두 개의 트리를 비교할 때, 두 엘리먼트의 루트(root) 엘리먼트부터 비교한다.
1. 엘리먼트 타입이 다른 경우: 이전 트리를 버리고 완전히 새로운 트리를 구축.
//이전 트리인 <div>~</div>가 삭제되고 <span>~</span>트리가 새로 생성. <div> <Counter /> </div> <span> <Counter /> </span>
2. DOM 엘리먼트 타입이 같은 경우: 두 엘리먼트 사이에서 동일한 내역은 유지하고 변경된 속성만 갱신.
<div className="before" title="stuff" /> <div className="after" title="stuff" />
<div>로 DOM 엘리먼트 타입이 같지만, className의 before가 after로 변경되었으므로 리액트는 현재 DOM 노드에서 className만 수정한다.
<div style={{color: 'red', fontWeight: 'bold'}} /> <div style={{color: 'green', fontWeight: 'bold'}} />
style이 갱신 될 때도 리액트는 변경된 속성만을 갱신한다. 위에서 볼 수 있듯이, <div>로 DOM 엘리먼트 타입은 같지만 color가 red에서 green으로 변경되었으므로 fontWeight는 수정하지 않고 color의 속성만을 수정한다.
DOM 노드의 처리가 끝나면, 리액트는 이어서 해당 노드의 자식들을 재귀적으로 처리한다.
3. 같은 타입의 컴포넌트 엘리먼트
컴포넌트가 갱신되면 인스턴스는 동일하게 유지되어 렌더링 간 state가 유지된다. 리액트는 새로운 엘리먼트의 내용을 반영하기 위해 현재 컴포넌트 인스턴스의 props를 갱신한다.
자식에 대한 재귀적 처리
DOM 노드의 자식들을 재귀적으로 처리할 때, 리액트는 기본적으로 동시에 두 리스트를 순회하고 차이점이 있으면 변경을 생성한다.
예를 들어, 자식의 끝에 엘리먼트를 추가하면 두 트리 사이의 변경은 잘 작동한다.
<ul> <li>first</li> <li>second</li> </ul> <ul> <li>first</li> <li>second</li> <li>third</li> // 엘리먼트 추가 </ul>
리액트는 두 트리에서 <li>first</li>가 일치하는 것을 확인하고, <li>second</li>가 일치하는 것을 확인합니다. 그리고 마지막으로 <li>third</li>를 트리에 추가한다.
하지만 위와 같이 단순하게 구현하면, 리스트의 맨 앞에 엘리먼트를 추가하는 경우 성능이 좋지 않게된다. 예를 들어, 아래의 두 트리 변환은 형편없이 작동한다.
<ul> <li>Duke</li> <li>Villanova</li> </ul> <ul> <li>Connecticut</li> // 맨 앞으로 추가 <li>Duke</li> <li>Villanova</li> </ul>
리액트는 <li>Duke</li>와<li>Villanova</li>종속 트리를 그대로 유지하는 대신 모든 자식을 변경한다. 이러한 비효율은 문제가 될 수 있고, 이를 해결하기 위해 리액트는 key 속성을 지원한다.
Keys
자식들이 key를 가지고 있다면, 리액트는 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>
이제 리액트는 '2014' key를 가진 엘리먼트가 새로 추가되었고, '2015'와 '2016' key를 가진 엘리먼트는 그저 이동만 하면 되는 것을 알 수 있다.
항목들이 재배열되지 않는다면 key를 index값으로 사용할 수 있지만, 재배열이 되는 경우 컴포넌트의 state와 관련된 문제가 발생한다. 컴포넌트의 인스턴스는 key를 기반으로 갱신되고 재사용 되는데, index를 key로 사용하면 항목의 순서가 바뀌었을 때 key 또한 바뀌게 되고, 그 결과 컴포넌트의 state가 엉망이 되거나 의도하지 않는 방식으로 바뀔 수 있다.
리액트 공식문서에서 index를 key로 사용하였을 때 발생하는 문제를 보여주는 Codepen을 보자.
1. 첫 리스트에 저녁먹기 작성
2. Add New to End 클릭 시, ID 2가 1뒤로 생성되었고 운동하기를 작성함
3. Add New to Start 클릭 시, ID 3이 제일 앞으로 생성되었고 기존 ID 1과 2에 있던 값이 밀려나지 않고 컴포넌트의 state가 엉망이 됨.
따라서, key는 반드시 변하지 않고, 예상 가능하며, 유일해야 한다. 엘리먼트가 식별자를 가지고 있다면 그 식별자를 key 로 사용하도록 한다.
<li key={item.id}>{item.name}</li>
참고 https://ko.reactjs.org/docs/reconciliation.html
참고 https://sambalim.tistory.com/150
'공부' 카테고리의 다른 글
[Next.js] 1. Create a Next.js App (0) 2021.10.21 [React] styled-components를 사용하는 이유 (0) 2021.10.20 [WEB] CSS 이해하기 (0) 2021.10.14 [JS] Web API (0) 2021.10.06 [JS] 실행 컨텍스트(Execution Context) (0) 2021.10.01