사건의 발단..
react에서는 Array.prototype.map을 통해 동일하지만 값만 다른 컴포넌트를 쉽게 렌더링 할 수 있습니다.
보통 다음과 같은 형태로 많이 사용합니다.
['a', 'b', 'c'].map((item) => <p>{item}</p>)
리액트에서는 위와 같이 동일한 컴포넌트에 대해 각각의 컴포넌트를 구별할 수 있도록 key props를 입력해야합니다.
Array.prototype.map 메서드를 간단하게 살펴보면 첫 번째 매개변수로 콜백 함수를 받습니다. 이 콜백 함수의 두 번째 인자는 index임을 알 수 있습니다.
arr.map(callback(currentValue[, index[, array]])[, thisArg])
이 점을 활용하여 과거에 저는 종종 다음과 같이 사용하고는 했습니다.
['a', 'b', 'c'].map((item, i) => <p key={i}>{item}</p>
이는 나중에 큰 버그를 불러오는데,,
React의 재조정(reconciliation)
React 컴포넌트에서 key props를 받는 이유는 React의 비교 알고리즘 때문입니다.
React에서는 컴포넌트의 state나 props가 변경되면 기존에 React 엘리먼트 트리에서 값을 변경시켜야햐는 엘리먼트만 변경합니다.
이를 효과적으로 생성하기 위한 알고리즘이 존재하지만 그러한 알고리즘조차 N개의 엘리먼트가 있는 트리에 대해서 O(N^3)의 시간 복잡도를 가집니다.
이는 너무나 오랜 시간이 걸리는 것이기 때문에 다음과 같은 규칙으로 기존 React 엘리먼트 트리를 변환합니다.
1. 엘리먼트의 이름이 다르면 해당 엘리먼트와 그의 자식 엘리먼트들을 제거한 후 새 트리를 생성합니다.
React 엘리먼트 트리를 비교할 때, 엘리먼트 이름이 다르면 기존에 존재하던 엘리먼트 트리를 제거하고 새로운 엘리먼트 트리를 만들어 넣습니다. (서브 트리일 경우 해당 서브 트리만 제거 후 교체합니다.)
만약 attribute의 값만 다를 경우 해당 attribute만 변경합니다.
2. 개발자가 key props라는 고윳값을 통해 해당 엘리먼트의 변경 여부를 React에게 알려줍니다.
엘리먼트의 자식들을 비교할 때, React는 기존 엘리먼트의 자식들과 새로운 엘리먼트의 자식들을 동시에 순회합니다.
다음은 React에서 제공하는 예시입니다.
<ul>
<li>Duke</li>
<li>Villanova</li>
</ul>
<ul>
<li>Connecticut</li>
<li>Duke</li>
<li>Villanova</li>
</ul>
두 <ul>
의 자식들을 순차대로 순회하기 때문에 3번의 작업이 진행될 것입니다.
Duke → Connecticut
Villanova → Duke
[empty] → Villanova (새로 생성)
이는 너무나 비효율적이기 때문에 key props를 활용합니다.
<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>
위 코드와 같이 key props가 존재하면 React는 key props의 값을 통해 비교합니다.
2014가 없는 것을 확인하고 <li key="2014">Connecticut</li>
만 추가하는 것이지요.
ESLint의 react/no-array-index-key 에러
만약 ESLint 규칙을 타이트하게 잡아놓으셨다면
**Prevent usage of Array index in keys (react/no-array-index-key)
경고 혹은 오류를 보신 적이 있을 겁니다. (저 역시 자주 만났었죠,,ㅎ)
ESLint에서 이를 막기 위해 알려주는 이유는 개발자의 의도대로 작동하지 않을 가능성이 있기 때문입니다.
key props의 값을 index로 설정하게 되면 재배열이 일어났을 때 문제가 발생합니다.
다음 코드와 같은 UserList 컴포넌트가 있습니다.
function UserList () {
const fetchedData = [
{ name: 'Kim', age: 22 },
{ name: 'Park', age: 24 },
{ name: 'Lee', age: 23 },
{ name: 'Ryu', age: 27 },
{ name: 'Chang', age: 26 }
];
return (<>
{fetchedData.map((item, i) => <UserListItem user={item} key={i} />}
</>);
}
유감스럽게도 제 <UserListItem />
컴포넌트에는 메모와 체크박스와 같은 것들이 각각의 상태 값 <string, boolean>
을 가지며 포함되어있습니다.
만약 fetchedData가 모종의 이유로 나이순으로 정렬된다면 어떻게 될까요?
user 값은 다음과 같이 변경되었지만 key 값은 변하지 않을 것입니다.
[
{ name: 'Kim', age: 22 },
{ name: 'Lee', age: 23 },
{ name: 'Park', age: 24 },
{ name: 'Chang', age: 26 },
{ name: 'Ryu', age: 27 },
]
이로 인해 <UserListItem />
내부에 있는 <string, boolean>
값들은 변하지 않고, user props로 새롭게 들어온 name
과 age
값만 변하게 될 것입니다.
이러한 문제는 React 공식 문서에서도 잘 설명해주고 있는데요, 이렇게 삽질을 해가며 또 하나 배워갑니다😂😂😂
'개발 노트 > FE' 카테고리의 다른 글
useQuery onComplete를 data 프로퍼티로 리팩토링하기 (0) | 2022.02.06 |
---|