Next.js
React 리스트 key를 index로 둔 채 정렬 기능을 붙였을 때 생긴 버그
정렬 버튼을 추가한 뒤 체크 상태가 엉뚱한 항목에 붙던 문제를 key 기준으로 정리한 회고다.
버그는 정렬 버튼을 누른 뒤에만 나타났다
할 일 목록에 "마감일순" 정렬을 붙였는데, 체크해 둔 항목이 정렬 후 다른 줄에 붙어 보였다. 저장된 데이터는 맞았고 화면만 이상했다. 새로고침하면 다시 정상이라서 처음에는 상태 업데이트 순서를 의심했다.
문제는 리스트의 key를 배열 index로 둔 것이었다. 정렬 전에는 티가 나지 않았지만, 순서가 바뀌는 순간 React가 같은 위치의 컴포넌트를 재사용하면서 체크 상태가 다른 항목에 붙었다.
문제가 있던 코드
{todos.map((todo, index) => (
<TodoRow key={index} todo={todo} />
))}
항목이 추가만 되는 목록에서는 이 코드가 꽤 오래 버틴다. 그래서 더 위험했다. 삭제, 정렬, 필터가 들어오는 순간 index는 안정적인 식별자가 아니게 된다.
고친 코드
서버에서 내려오는 id를 key로 쓰도록 바꿨다.
{todos.map((todo) => (
<TodoRow key={todo.id} todo={todo} />
))}
임시 데이터에도 id를 넣었다. 예제 코드에서 id 만들기 귀찮다고 index를 쓰면 나중에 실제 화면으로 옮길 때 그대로 남는 경우가 많았다.
const draftTodos = [
{ id: 'draft-1', title: '배포 로그 확인', done: false },
{ id: 'draft-2', title: 'README 수정', done: true },
];
재현 테스트를 이렇게 했다
- 첫 번째 항목을 체크한다.
- 마감일순 정렬을 누른다.
- 체크 표시가 같은 할 일에 남아 있는지 본다.
- 필터를 켰다가 꺼도 같은 항목인지 확인한다.
눈으로만 보면 놓치기 쉬워서, 정렬 전후 항목 id를 콘솔에 찍어 확인했다.
console.table(todos.map((todo) => ({ id: todo.id, title: todo.title, done: todo.done })));
결론 대신 남기는 기준
index key를 완전히 금지할 필요는 없지만, 나는 아래 조건 중 하나라도 있으면 쓰지 않기로 했다.
- 사용자가 항목을 삭제할 수 있다.
- 정렬이나 필터가 있다.
- 각 행 내부에 입력값, 체크박스, 펼침 상태가 있다.
- 서버 데이터와 동기화된다.
이번 버그는 React가 이상하게 동작한 게 아니라 내가 React에게 항목의 정체를 잘못 알려준 문제였다.