key의 역할과 올바른 사용
React에서 key prop이 리스트 재조정에서 하는 역할, 인덱스 key의 위험성, 그리고 key를 이용한 컴포넌트 초기화 트릭을 설명합니다.
지난 글에서 .map()으로 리스트를 렌더링할 때 key prop을 붙여야 한다고 소개했습니다. 왜 key가 필요한지, 무엇을 key로 써야 하는지, 그리고 key를 활용하는 고급 패턴까지 이번 글에서 자세히 살펴봅니다.
key가 필요한 이유
React는 상태가 변경되면 이전 UI 트리와 새 UI 트리를 **비교(재조정, Reconciliation)**해서 달라진 부분만 DOM에 반영합니다. 리스트에서 항목이 추가·삭제·재정렬될 때 React는 각 항목이 어느 것인지 파악해야 효율적으로 업데이트할 수 있습니다.
key가 없으면 React는 위치(인덱스)로만 항목을 구분합니다. 항목의 순서가 바뀌면 React는 항목 자체가 바뀐 것으로 착각하고 불필요한 DOM 조작을 수행합니다.
// React 경고: Each child in a list should have a unique "key" prop.
{items.map(item => <li>{item.name}</li>)}
// key를 추가하면 경고 해소 + 정확한 재조정
{items.map(item => <li key={item.id}>{item.name}</li>)}
인덱스 key의 위험성
많은 초보자가 key={index}를 씁니다. 리스트가 변하지 않는다면 문제가 없지만, 항목을 추가·삭제·정렬하면 심각한 버그가 생깁니다.
// ❌ 인덱스 key — 항목에 입력값이 있으면 버그
function TodoList() {
const [todos, setTodos] = useState([
{ id: 1, text: '리액트 공부' },
{ id: 2, text: '운동하기' },
]);
return (
<ul>
{todos.map((todo, index) => (
<li key={index}> {/* 위험! */}
<input defaultValue={todo.text} />
<button onClick={() => removeTodo(todo.id)}>삭제</button>
</li>
))}
</ul>
);
}
첫 번째 항목을 삭제하면 인덱스가 시프트됩니다. React는 key=0이 여전히 존재한다고 보고 기존 컴포넌트를 재사용하는데, 이때 input의 내부 상태(사용자가 입력한 값)는 이전 항목 것이 그대로 남습니다.
// ✅ 안정적인 고유 ID 사용
{todos.map(todo => (
<li key={todo.id}>
<input defaultValue={todo.text} />
<button onClick={() => removeTodo(todo.id)}>삭제</button>
</li>
))}
올바른 key 선택 기준
| 상황 | 권장 key |
|---|---|
| 데이터베이스에서 온 데이터 | DB의 고유 ID (id, uuid) |
| 클라이언트에서 생성 | crypto.randomUUID() 또는 Date.now() |
| 정적 목록 (순서 변경 없음) | 인덱스 허용 (경고만 없애는 경우) |
| 절대 피해야 할 경우 | 렌더링 중 생성한 랜덤 값 (Math.random()) |
// 새 항목 추가 시 클라이언트에서 ID 생성
function addTodo(text) {
setTodos(prev => [
...prev,
{ id: crypto.randomUUID(), text }
]);
}
key는 형제 사이에서만 고유하면 됨
key의 유일성은 같은 부모의 자식 사이에서만 보장하면 됩니다. 다른 리스트끼리는 같은 key 값을 써도 됩니다.
// list1의 key=1과 list2의 key=1은 서로 무관
<ul>
{list1.map(item => <li key={item.id}>{item.name}</li>)}
</ul>
<ul>
{list2.map(item => <li key={item.id}>{item.name}</li>)}
</ul>
key 변경으로 컴포넌트 강제 초기화
key는 리스트 전용이 아닙니다. 단일 컴포넌트에 key를 바꾸면 이전 인스턴스를 언마운트하고 새로 마운트합니다. 내부 상태를 완전히 초기화하는 데 유용합니다.
// userId가 바뀔 때마다 UserProfile을 완전히 리셋
function App({ userId }) {
return <UserProfile key={userId} userId={userId} />;
}
useEffect에서 상태를 초기화하는 것보다 훨씬 깔끔합니다. React 공식 문서도 이 패턴을 권장합니다.
key는 props로 받을 수 없다
key는 React가 내부에서 사용하는 예약 prop입니다. 컴포넌트 내부에서 props.key로 접근할 수 없습니다.
function Item({ id, name }) {
// props.key는 undefined — 사용 불가
// id를 별도 prop으로 받아서 사용
return <li data-id={id}>{name}</li>;
}
// 렌더링 시: key와 id를 둘 다 명시
{items.map(item => (
<Item key={item.id} id={item.id} name={item.name} />
))}
지난 글: 리스트 렌더링
다음 글: 이벤트 처리
읽어주셔서 감사합니다. 😊