Next.js
React key 경고가 버그로 이어지는 상황
key 경고를 콘솔에서 무시하다 보면 리스트 아이템이 섞이거나 상태가 엉뚱한 컴포넌트에 붙는 문제를 겪게 된다.
React 리스트를 렌더링할 때 key를 index로 쓰거나 아예 빼먹으면 콘솔에 경고가 뜬다. 처음엔 그냥 넘기기 쉬운데, 실제로 입력 폼이나 애니메이션이 섞인 리스트에서는 눈에 보이는 버그로 튀어나온다.
key가 없으면 React가 무엇을 헷갈리나
React는 리스트를 다시 렌더링할 때 key를 기준으로 어느 DOM 노드를 재사용할지 결정한다. key가 없으면 순서만으로 판단한다. 아이템이 맨 앞에 추가되는 경우를 생각해보면, React는 첫 번째 자리에 있던 노드를 그대로 두고 내용만 교체한다. 노드 자체를 재사용하니 해당 노드에 묶인 로컬 상태(예: 체크박스 체크 여부, 입력값)는 새 아이템이 아니라 원래 자리에 계속 붙어 있게 된다.
가장 흔하게 목격한 증상은 이렇다.
- 리스트 상단에 아이템 추가 → 아래 아이템의 input 값이 위로 밀려 올라감
- 아이템 삭제 후 다른 아이템에 포커스가 잘못 이동
- transition 애니메이션이 의도와 다르게 엉킨 아이템에 적용
index를 key로 쓰면 왜 문제인가
// 이렇게 쓰면 아이템 순서가 바뀔 때 key가 같은 위치에 그대로 남는다
{items.map((item, index) => (
<TodoItem key={index} item={item} />
))}
배열이 정렬되거나 맨 앞에 아이템이 끼어들면, 기존 아이템들의 index가 바뀐다. React는 같은 key(0, 1, 2...)가 여전히 존재한다고 판단해서 컴포넌트를 새로 마운트하지 않고 props만 업데이트한다. 안에 있는 로컬 state는 건드리지 않은 채로.
// 데이터 고유 ID를 key로 써야 한다
{items.map((item) => (
<TodoItem key={item.id} item={item} />
))}
DB에서 온 데이터라면 id가 있을 것이고, 클라이언트에서 생성한 리스트라면 crypto.randomUUID()나 nanoid()로 생성 시점에 붙여두면 된다. 생성 때마다 새로 만들면 안 되고, 아이템이 만들어질 때 한 번 고정해야 한다.
실제로 문제가 생기는 조건
key를 잘못 써도 표시만 하는 정적 리스트라면 대부분 티가 안 난다. 문제가 드러나는 건 다음 상황이다.
- 리스트 아이템 안에 자체 로컬 state가 있을 때 (input, accordion 열림 여부 등)
- 아이템이 추가·삭제·정렬되는 동적 리스트
- React가 언마운트/마운트 시점에 side effect를 실행하는 컴포넌트 (useEffect cleanup 포함)
경고를 고쳐두면 이런 버그 자체가 생기지 않는다. 그리고 React DevTools에서 컴포넌트를 추적할 때도 key가 제대로 있어야 어느 인스턴스가 어느 데이터와 연결됐는지 읽기 쉽다.