Next.js
React list에서 key를 index 대신 id로 써야 하는 이유
key={index}는 경고를 없애주지만 문제를 해결하지는 않는다. 항목 순서가 바뀌거나 삭제될 때 컴포넌트 상태가 엉킬 수 있다.
React에서 배열을 렌더링할 때 key를 안 붙이면 콘솔에 경고가 뜬다. 빠르게 경고를 없애려고 key={index}를 쓰는 경우가 많은데, 이것이 실제 문제를 숨기고 있을 수 있다.
key={index}가 왜 문제인가
React는 key를 보고 이전 렌더링과 다음 렌더링에서 같은 컴포넌트를 매칭한다. index를 key로 쓰면 항목의 위치만 같으면 같은 컴포넌트라고 판단한다.
항목 순서가 바뀌거나, 중간 항목이 삭제될 때 문제가 생긴다. 예를 들어 인덱스 0의 항목이 삭제되면, 이전에 인덱스 1이었던 항목이 인덱스 0으로 이동한다. React는 인덱스 0이 여전히 같은 컴포넌트라고 보고, 삭제된 항목의 상태(입력값, 선택 상태 등)가 다음 항목에 그대로 남는다.
// ❌ index를 key로 쓸 때
const [items, setItems] = useState(['A', 'B', 'C'])
// A를 삭제하면 ['B', 'C']가 되는데
// React 입장에서 key=0은 그대로이므로 B가 A의 상태를 이어받는다
items.map((item, index) => (
<ItemInput key={index} defaultValue={item} />
))
controlled input이 아니라 uncontrolled input(defaultValue 사용)이나 내부 상태를 가진 컴포넌트에서 특히 눈에 띄게 나타난다.
안정적인 id를 key로 쓴다
// ✅ 고유 id를 key로
const [items, setItems] = useState([
{ id: 'a1', value: 'A' },
{ id: 'b2', value: 'B' },
{ id: 'c3', value: 'C' },
])
items.map((item) => (
<ItemInput key={item.id} defaultValue={item.value} />
))
item.id는 항목이 어떤 위치로 이동하더라도 변하지 않는다. React가 올바르게 컴포넌트를 재사용하거나 새로 마운트한다.
API에서 id가 없을 때
서버에서 받은 데이터에 고유 id가 있는 경우가 대부분이다. 정말 없다면 다음 대안을 쓴다.
내용 자체가 고유하다면: key={item.email}, key={item.slug} 처럼 불변하는 고유값을 쓴다.
클라이언트에서 id를 붙여야 한다면: 데이터를 받을 때 crypto.randomUUID()나 nanoid로 id를 생성해서 붙인다.
import { nanoid } from 'nanoid'
const items = apiResponse.map((item) => ({
...item,
id: nanoid(),
}))
단, 이렇게 하면 컴포넌트가 리렌더링될 때마다 새 id가 생겨서 모든 항목이 다시 마운트된다. 이 id는 useState나 useMemo로 한 번만 생성하게 해야 한다.
index가 괜찮은 경우
목록이 절대 변하지 않고(추가/삭제/재정렬 없음), 항목 컴포넌트가 내부 상태를 갖지 않는 순수 표시용일 때는 key={index}를 써도 실질적인 문제가 없다. 그래도 나는 처음부터 id를 쓰는 습관을 들이는 편이 낫다고 본다. 나중에 기능이 추가될 때 따로 고칠 일이 없다.