
리액트에서 key는 보통 map 함수를 사용하여 이터러블(iterable) 객체를 렌더링할 때 사용된다
const ItemList = ({ items }) => {
return (
<ul>
{items.map(item => (
<li key={item.id}>{item.name}</li>
))}
</ul>
);
}
이 글에서는 key가 왜 필요한지에 대해 알아본다
리액트에서는 컴포넌트마다 고유의 메모리를 보유하고 있다
이 메모리를 통해 useState, useRef를 사용해서 컴포넌트가 리렌더링 되더라도 이전의 상태나 값을 기억하도록 할 수 있다
컴포넌트의 메모리가 실제로 컴포넌트 내부에 존재한다면 key는 필요가 없을 것이다
하지만 컴포넌트의 메모리는 리액트의 내부 인스턴스와 상태 저장 매커니즘에 저장된다
그 후 렌더링 과정으로 생성된 UI 트리에 따라 컴포넌트에 순차적으로 연결된다
실제로는 보다 복잡한 메커니즘이지만 이해를 돕기 위해
순차적으로 연결된다고 가정한다

문제는 이터러블한 객체는 리렌더링 할 때에 그 순서가 바뀔 수 있다는 것이다
다음의 예시를 보자
const Component = ({ alphabet }) => {
const [count, setCount] = useState(0);
const addCount = () => {
setCount((prev) => prev + 1);
};
return (
<div>
<div>{alphabet}</div>
<div>{count}</div>
<button onClick={addCount}>+</button>
</div>
);
};
const App = () => {
const [arr, setArr] = useState(['A', 'B', 'C']);
const swapBC = () => {
setArr((prev) => [prev[0], prev[2], prev[1]]);
};
return (
<div>
{arr.map((alphabet) => (
<Component name={alphabet} />
))}
<button onClick={swapBC}>swapBC</button>
</div>
);
};
각각 A, B, C 라는 name 속성을 가진 세 컴포넌트가 존재하고, 이 컴포넌트들은 모두 각자의 count 상태를 가지고 있다
swapBC 버튼을 클릭하는 것으로 B와 C 컴포넌트의 위치를 바꿀 수 있다
초기 렌더링 시에 다음과 같은 모습일 것이다

이 때 B와 C의 자리를 바꾸면 어떻게 될까?

분명 B의 카운트를 5까지 올린 후 B와 C의 자리를 바꿨는데 C의 카운트가 5가되고 B는 0이 되었다
그림과 같이 이유를 확인해보자

카운트를 담은 각 메모리는 UI 트리에 순차적으로 연결이 됐었다
B와 C의 순서가 바뀌었지만 UI 트리에서 보면 B의 자리에 C가 왔기 때문에 B에 연결되었던 메모리가 C에 연결 된 것이다
이처럼 각 컴포넌트 메모리는 어떤 컴포넌트에 연결되는 것이 아닌 어디에 있는 컴포넌트에 연결되고 이는 기대와 다른 결과를 가져올 수 있다
index값을key의 값으로 사용하는 것은 안티패턴이라는 말이 존재하는데 위의 예시가 그 이유다
실제로 이터러블한 함수 내부에서key를 명시적으로 할당하지 않으면 내부적으로index값이 할당된다
그렇다면 key를 할당하는 것으로 문제를 해결해보자
const App = () => {
const [arr, setArr] = useState([
{ id: 0, alphabet: 'A' },
{ id: 1, alphabet: 'B' },
{ id: 2, alphabet: 'C' },
]);
const swapBC = () => {
setArr((prev) => [prev[0], prev[2], prev[1]]);
};
return (
<div>
{arr.map(({ id, alphabet }) => (
<Component key={id} name={alphabet} />
))}
<button onClick={swapBC}>swapBC</button>
</div>
);
};
각각의 컴포넌트의 key 값으로 id를 할당해주었다
이것은 React가 컴포넌트에 메모리를 연결할 때 다음과 같은 명령을 내린다
UI 트리의 위치가 아닌, 특정
key를 가진 컴포넌트를 찾아가

이제 메모리는 특정 위치의 컴포넌트에 연결되는 것이 아닌 특정 key를 가진 컴포넌트에 연결되게 된다

따라서 위치를 바꾸더라도 정확하게 기대와 같이 동작하게 된다

이 떄문에
key값은 고유해야하며, 이는 형제 요소 사이에서만 고유하면 된다
리액트는 렌더링 시에 가상 DOM을 생성하고 Reconciliation(재조정) 과정에서 이전 렌더와 새 렌더 사이의 차이를 비교한 후, 필요한 변경만 DOM에 적용한다
이는 React의 렌더링 최적화 기법으로 불필요한 DOM 조작을 줄이고 성능을 향상시킨다
위에서 이터러블(iterable) 객체를 렌더링할 때 명시적으로 key 값을 할당하지 않으면 index 값이 할당된다고 했다
이는 key의 변경을 발생시킬 수 있다
다음의 예시를 살펴보자
export const App = () => {
const [arr, setArr] = useState(['A', 'B', 'C']);
return (
<ul>
{arr.map((alphabet, index) => (
<li key={index}>{alphabet}</li>
))}
</ul>
);
};
그림으로 보면 다음과 같은 모습일 것이다

이 때, A와 B사이에 D를 추가해보자

D의 key 값으로 1이 할당되고 B, C의 key 값이 각각 1에서 2로 2에서 3으로 변경되었다
key가 변경되면 뭐가 문제일까?
key 값이 변경되면 React는 해당 컴포넌트를 완전히 새로운 컴포넌트로 간주하고, 이전 인스턴스를 파괴한 후에 새로운 인스턴스를 생성한다
위의 특성을 활용해
key가 필요하지 않은단일 요소에 의도적으로key를 할당하거나 변경하는 것으로 해당 컴포넌트의상태를 초기화할 수 있다
이는 강제적으로 DOM 업데이트를 발생시키기 때문에 성능에 부정적인 영향을 미칠 수 있다
D는 새롭게 추가되었기에 DOM에 새롭게 그려지는 것이 당연하지만, B와 C는 그 내부가 전혀 바뀌지 않았음에도 DOM에 새롭게 그려진다는 것이다
다시 고유의 key를 할당하는 것으로 문제를 해결해보자
export const App = () => {
const [arr, setArr] = useState([
{ id: 0, name: 'A' },
{ id: 1, name: 'B' },
{ id: 2, name: 'C' },
]);
return (
<ul>
{arr.map(({ id, name }) => (
<li key={id}>{name}</li>
))}
</ul>
);
};

이 상태에서 이전과 같이 A와 B 사이에 { id: 3, name: 'D' }를 추가해보자

D가 추가되었지만 각각의 컴포넌트의 key 값은 변하지 않았다
때문에 가상 DOM으로부터 실제 DOM에 반영될 때 변경된 부분만 반영하게 되고, B와 C 컴포넌트에 변경이 없었다고 가정한다면 D만 실제 DOM에 추가되게 되는 것이다
퀄리티 좋네요..