- 왜 반복되는 엘리먼트를 추가할 때는
key
prop을 넣으라고 Warning을 띄우는 걸까? key
prop은 왜 존재하며, 어떤 값을 넣어주어야 할까?- 리액트 공식 문서에서는 key를 다음과 같이 설명하고 있다.
Key는 React가 어떤 항목을 변경, 추가 또는 삭제할지 식별하는 것을 돕습니다. key는 엘리먼트에 안정적인 고유성을 부여하기 위해 배열 내부의 엘리먼트에 지정해야 합니다.
Diffing Algorithm
- 리액트는 컴포넌트를 렌더하면서 어떤 변화가 있었는지를 비교하는 알고리즘(Diffing Algorithm)을 가지고 있다.
render
함수는 이 알고리즘을 이용하여 기존 React 엘리먼트 트리와 비교하고 새로운 React 엘리먼트 트리를 생성해서 반환한다. - 기존 트리를 가지고 다른 트리로 변환하기 위해 최소한의 연산을 하는 알고리즘 문제를 해결하기 위해 일반적으로 O(n^3)의 시간 복잡도를 가진다. 이대로 리액트에 적용한다면, 1000개의 엘리먼트를 그리기 위해 10억 번의 비교 연산을 해야하며, 이는 너무 비싼 연산이라고 리액트에서는 설명한다.
- 따라서, 리액트에는 두 가지 가정을 통한 휴리스틱 알고리즘을 적용하여 이 연산을 O(n)까지 줄일 수 있었다고 설명한다.
- 서로 다른 타입의 두 엘리먼트는 서로 다른 트리를 만들어낸다.
- 개발자가
key
prop을 통해, 여러 렌더링 사이에서 어떤 자식 엘리먼트가 변경되지 않아야 할지 표시해 줄 수 있다.
- 이 때, 사용되는 것이
key
prop이다. 리액트에서는key
prop을 통해 컴포넌트를 새로 렌더하면서 엘리먼트 트리를 생성할 때, 더 빠르게 비교 연산을 수행하고 렌더할 수 있다.
리스트의 자식 요소가 변경될 때
- 다음과 같이, 리스트의 마지막에 새로운 엘리먼트를 추가할 때는 큰 성능 문제를 유발하지 않는다.
// before
<ul>
<li>first</li>
<li>second</li>
</ul>
// after
<ul>
<li>first</li>
<li>second</li>
<li>third</li>
</ul>
- 리액트에서는 렌더할 때, 기존 리스트와 바뀔 리스트를 동시에 순회하면서 차이점을 찾는다. 첫 번째 엘리먼트(first)와 두 번째 엘리먼트(second)는 차이점이 없고, 마지막에 새로운 엘리먼트(third)만 추가하면 된다.
- 실제 렌더 시에도, 기존 엘리먼트의 변화 없이 새롭게 추가된 마지막 엘리먼트만 새로 렌더한다.
- 하지만 다음과 같이 엘리먼트를 맨 위에 추가했을 때는 상황이 다르다.
// before
<ul>
<li>대구</li>
<li>부산</li>
</ul>
// after
<ul>
<li>서울</li>
<li>대구</li>
<li>부산</li>
</ul>
- 위에서 리액트에서는 기존 리스트와 바뀔 리스트를 동시에 순회한다고 했는데, 이미 첫 번째 엘리먼트에서 차이점이 생긴다. 첫 번째 자식 엘리먼트는
대구
에서서울
로 바뀐다. - 이후의 비교에도 차이점이 계속 생길 것이다. (
부산
→대구
/부산
추가) - 따라서, 모든 엘리먼트가 새롭게 렌더된다.
- 이미 렌더된 엘리먼트가 분명히 있는데 찾지 못하고 다시 렌더하는 과정은 불필요한 연산이며 성능 이슈로 이어질 수 있을 것이다.
- 이러한 문제를 해결하기 위해서
key
prop을 넣을 수 있다.
// before
<ul>
<li key="daegu">대구</li>
<li key="busan">부산</li>
</ul>
// after
<ul>
<li key="seoul">서울</li>
<li key="daegu">대구</li>
<li key="busan">부산</li>
</ul>
key
를 추가하면, 리액트에서는 자식 엘리먼트의key
값의 비교를 통해 렌더한다.- 리액트는
key
가seoul
이라는 엘리먼트가 새로 생성되었으며,key
가daegu
,busan
인 엘리먼트에는 변화가 없기 때문에 이동만 하면 된다는 것을 알 수 있다.
key
를 지정할 때의 주의사항
key
를 지정할 때 index를 넣는 것은 안티 패턴이다.
map
함수의index
값을key
로 지정하게 된다면,map
함수가 다시 실행되면서index
도 다시 생성된다. 이렇게 될 경우, 기존 리스트와 비교해서 최적화하려는 시도 자체가 무의미해진다. 즉, 안티 패턴이다.- 심지어,
key
값을 지정하지 않았을 때 리액트에서는 기본적으로index
를key
로 사용한다. - 리스트를 렌더할 때는 각 아이템이 고유한 id를 가질 수 있도록 하고, 해당 id를
key
로 지정하는 것이 좋다.
const list = ['서울', '대전', '대구', '부산'];
{list.map((item, index) => <Item key={index}>{item}</Item>}
// 위는 다음과 같이 반환될 것이다.
<Item key={0}>서울</Item>
<Item key={1}>대전</Item>
<Item key={2}>대구</Item>
<Item key={3}>부산</Item>
// 다음과 같이 앞부분에 '경기'를 추가했다면 index값이 하나씩 뒤로 밀리게 될 것이다.
const list = ['경기', '서울', '대전', '대구', '부산'];
{list.map((item, index) => <Item key={index}>{item}</Item>}
// 위는 다음과 같이 반환될 것이다.
<Item key={0}>경기</Item>
<Item key={1}>서울</Item>
<Item key={2}>대전</Item>
<Item key={3}>대구</Item>
<Item key={4}>부산</Item>
key
는 주변 배열의 컨텍스트에서만 의미가 있다.
key
는 리스트의 순서를 비교하는 데 사용하는 값이며, 리스트를 렌더하고 있는 곳에서key
를 넣어주어야 한다.map
함수 내부에 있는 엘리먼트에key
를 넣어주는 것이 가장 일반적이다.
function ListItem(props) {
// 이 곳에서 li에 key 값을 지정해도 별 효과는 없다.
return <li key={props.id}>{props.value}</li>
}
function List(props) {
const data = props.data;
// 다음과 같이 ListItem 엘리먼트가 key를 가져야 한다.
// 참고: key는 ListItem 엘리먼트의 prop으로 전달되지 않는다.
return (
<ul>
{data.map((item) => <ListItem key={item.id} />}
</ul>
);
}
key
는 형제 사이에서만 고유한 값이면 된다.
- 모든 범위에서 자식 엘리먼트의
key
값이 고유할 필요는 없으면, 특정 리스트 안에서만 고유한 값을 가지고 있으면 된다.
부록: nanoid
나 uuid
같은 라이브러리를 사용하여 key
를 지정할 때의 주의점
- 다음과 같이 렌더할 때마다
nanoid
나uuid
가 실행되지 않도록 주의하자. 렌더할 때마다nanoid
가 실행되는 불필요한 연산이 이루어질 뿐만 아니라, 기존 리스트와 비교하려는 시도가 무의미해지는 안티 패턴이다.
function List(props) {
const data = props.data;
return (
<ul>
{data.map((item) => <ListItem key={**nanoid(8)**} /> /* <- DON'T DO THAT */}
</ul>
);
}