React 리스트에서 key 경고가 나는 이유와 고치는 기준
React에서 리스트를 렌더링할 때 보이는 key 경고는 콘솔 문구를 지우는 문제로 끝나지 않습니다. React가 여러 항목을 비교할 때 어떤 항목이 같은 데이터인지 판단할 수 있도록, 형제 항목 사이에서 고유하고 렌더링마다 바뀌지 않는 안정적인 식별자를 주는 작업입니다.
- 콘솔 경고가 가리키는 위치부터 보기
- React가 리스트 항목을 다시 찾는 방식
- index key가 위험해지는 순간
- 코드에서 key 위치를 고치는 방식
- 수정 후 확인할 체크포인트
- 정리
콘솔 경고가 가리키는 위치부터 보기

React에서 배열을 map으로 렌더링하다 보면 콘솔에 Warning: Each child in a list should have a unique "key" prop. 경고가 나타날 수 있습니다. 화면은 정상적으로 보일 때도 있어서 처음에는 대수롭지 않게 넘기기 쉽습니다. 하지만 이 경고는 React가 리스트 항목을 구분할 값을 충분히 받지 못했다는 신호입니다.
상품 카드, 댓글 목록, 장바구니 항목, 관리자 테이블처럼 같은 컴포넌트를 반복해서 그리는 화면에서는 이 문제가 자주 나옵니다. 경고가 뜬 위치를 따라가 보면 보통 map 안에서 반환하는 가장 바깥 요소에 key가 없거나, 같은 배열 안에서 동일한 key를 반복해서 쓰고 있습니다.
처음 확인할 때는 “리스트 key 경고”와 “개발 중 로그가 두 번 찍히는 현상”을 분리해야 합니다. 두 현상이 같이 보이면 key 문제가 렌더링 횟수 문제처럼 느껴질 수 있습니다. 개발 환경에서 useEffect 로그가 반복되는 상황까지 겹쳤다면 React useEffect가 두 번 실행되는 이유를 따로 확인해두면 원인을 나누어 볼 수 있습니다.
React가 리스트 항목을 다시 찾는 방식
key는 화면에 출력되는 값이 아닙니다. React가 이전 렌더링 결과와 다음 렌더링 결과를 비교할 때, 리스트 안의 각 항목을 식별하기 위해 사용하는 특수한 값입니다. 같은 위치에 있는 항목이 정말 같은 데이터인지, 아니면 다른 데이터가 그 자리에 들어온 것인지 구분하는 데 쓰입니다.
예를 들어 댓글 목록에서 두 번째 댓글이 삭제되면, 기존 세 번째 댓글은 화면상 두 번째 위치로 올라옵니다. React가 위치만 보고 비교하면 “두 번째 자리에 있던 컴포넌트를 계속 재사용해도 된다”고 판단할 수 있습니다. 하지만 데이터 관점에서는 기존 두 번째 댓글이 사라지고 기존 세 번째 댓글이 이동한 상황입니다. 이 차이를 알려주는 값이 key입니다.
key는 전체 프로젝트에서 전역으로 유일할 필요는 없습니다. 같은 배열 안에서 형제 항목끼리만 유일하면 됩니다. 다만 렌더링할 때마다 새로 만들어지는 Math.random(), Date.now() 같은 값은 피해야 합니다. React 공식 문서도 key가 바뀌면 목적을 잃는다고 설명하므로, 데이터가 가진 안정적인 id를 우선으로 사용합니다.
key가 없는 기본 예시
{products.map((product) => (
<ProductCard product={product} />
))}
이 코드는 화면에 카드가 출력될 수는 있습니다. 다만 React 입장에서는 반복된 ProductCard 중 어떤 카드가 어떤 상품을 나타내는지 안정적으로 구분하기 어렵습니다. 필터를 적용하거나 정렬을 바꾸면 어떤 항목이 추가, 삭제, 이동되었는지 추적하는 근거가 부족해집니다.
데이터의 id를 key로 쓰는 예시
{products.map((product) => (
<ProductCard key={product.id} product={product} />
))}
서버 데이터나 로컬 데이터에 고유한 id가 있다면 그 값을 key로 쓰는 것이 가장 안정적입니다. product.id가 상품 하나를 계속 같은 상품으로 식별해준다면, 정렬 순서가 바뀌어도 React는 항목의 정체성을 유지할 수 있습니다.
index key가 위험해지는 순간
index를 key로 쓰면 당장 경고는 사라집니다. 그래서 처음에는 가장 빠른 해결처럼 보입니다. 문제는 index가 데이터의 정체성이 아니라 배열에서의 위치라는 점입니다. 위치는 정렬, 삭제, 삽입, 필터링이 들어가는 순간 쉽게 바뀝니다.
할 일 목록에서 첫 번째 항목을 삭제하는 상황을 떠올리면 차이가 분명해집니다. 기존 두 번째 항목의 index는 1에서 0으로 바뀝니다. React는 key를 기준으로 컴포넌트를 비교하므로, 이전에 key={0}이던 컴포넌트를 새 첫 번째 항목에 재사용할 수 있습니다. 단순 텍스트만 있는 목록에서는 티가 덜 나지만, 항목 안에 입력값이나 체크 상태가 있으면 다른 항목에 상태가 붙어 보일 수 있습니다.
| 상황 | index key 위험도 | 사용할 값 |
|---|---|---|
| 정적인 메뉴 목록 | 낮음 | 변하지 않는 문자열 key 또는 id |
| 삭제 가능한 할 일 목록 | 높음 | todo.id 같은 고유값 |
| 정렬되는 상품 카드 | 높음 | product.id |
| 필터가 적용되는 검색 결과 | 높음 | 검색 결과 항목의 원본 id |
정적인 약관 목록처럼 순서가 바뀌지 않고 항목 추가나 삭제도 없는 화면이라면 index key가 큰 문제로 드러나지 않을 수 있습니다. 다만 실무 화면에서는 처음에는 정적인 목록이었다가 나중에 정렬, 필터, 삭제 기능이 붙는 경우가 많습니다. 처음부터 데이터가 가진 고유값을 key로 잡아두면 이후 기능이 붙어도 수정 범위가 줄어듭니다.
코드에서 key 위치를 고치는 방식
key 경고를 고칠 때는 “어떤 값으로 key를 만들지”보다 “어디에 key를 붙였는지”를 먼저 확인해야 합니다. map 안에서 반환하는 가장 바깥 요소에 붙이는 것이 기본입니다. 내부 컴포넌트 깊은 곳에 key를 붙여도, 배열의 직접 결과물인 형제 요소를 React가 구분하지 못하면 경고가 계속 남을 수 있습니다.
잘못된 위치에 key를 둔 경우
{products.map((product) => (
<li>
<ProductCard key={product.id} product={product} />
</li>
))}
이 코드에서 배열이 직접 반환하는 요소는 ProductCard가 아니라 li입니다. 그런데 key는 그 안쪽 컴포넌트에 붙어 있습니다. React가 비교해야 하는 형제 요소는 li들이므로, 이 경우에는 li에 key를 붙여야 합니다.
반복되는 바깥 요소에 key를 둔 경우
{products.map((product) => (
<li key={product.id}>
<ProductCard product={product} />
</li>
))}
이렇게 작성하면 React는 각 li가 어떤 상품을 나타내는지 알 수 있습니다. ProductCard 안에서도 상품 id가 필요하다면 key에 의존하지 말고 productId={product.id}처럼 별도의 prop으로 넘겨야 합니다. React의 special props 기준에 따라 key는 내부 비교에 사용하는 힌트이며 컴포넌트의 일반 props로 전달되지 않습니다.
Fragment를 반복할 때의 처리
{sections.map((section) => (
<React.Fragment key={section.id}>
<h3>{section.title}</h3>
<p>{section.description}</p>
</React.Fragment>
))}
짧은 Fragment 문법인 <></>에는 key를 직접 붙일 수 없습니다. 여러 태그를 묶어서 반복해야 하고 별도의 DOM 요소를 추가하고 싶지 않다면 React.Fragment를 명시해서 key를 붙이면 됩니다.
수정 후 확인할 체크포인트

key를 수정한 뒤에는 콘솔 경고가 사라졌는지만 보지 말고, 리스트가 바뀌는 상황을 같이 확인해야 합니다. 새 항목 추가, 중간 항목 삭제, 정렬 변경, 필터 적용을 순서대로 눌러보면 key가 안정적인지 빠르게 드러납니다.
입력 폼이 포함된 리스트에서는 더 꼼꼼하게 봐야 합니다. 예를 들어 장바구니 옵션 입력값, 체크박스 선택 상태, 펼침/접힘 상태가 있는 항목은 잘못된 key를 썼을 때 다른 항목으로 상태가 옮겨 붙은 것처럼 보일 수 있습니다.
리스트 수정 중 상태 업데이트가 반복되면서 별도의 렌더링 오류가 같이 생긴다면 key만 볼 문제가 아닐 수 있습니다. 무한 업데이트 경고가 함께 나온다면 React Maximum update depth 오류 해결처럼 상태 변경 위치를 따로 점검해야 합니다.
확인 순서
1. map 안에서 반환하는 가장 바깥 요소에 key가 있는지 확인
2. key가 배열 index가 아니라 데이터의 고유 id인지 확인
3. Math.random(), Date.now()처럼 렌더링마다 바뀌는 key를 쓰지 않았는지 확인
4. 같은 배열 안에서 key가 중복되지 않는지 확인
5. 삭제, 삽입, 정렬, 필터 상황에서 상태가 엉키지 않는지 확인
6. key가 컴포넌트 내부 props로 필요하면 별도 prop으로 전달
이 순서로 보면 경고를 없애는 작업과 실제 UI 안정성을 확인하는 작업을 분리할 수 있습니다. 콘솔 경고만 없애려면 index를 붙이는 것으로 끝날 수 있지만, 사용자가 데이터를 추가하고 삭제하는 화면에서는 그 정도로는 부족합니다.
마무리 점검
React의 key 경고는 리스트 항목을 식별할 값이 부족할 때 나오는 신호입니다. 반복되는 가장 바깥 요소에 붙이고, 같은 배열 안에서 형제 요소끼리 유일하게 만들고, 가능하면 데이터가 가진 안정적인 id를 사용하면 대부분의 경고를 정리할 수 있습니다.
index key는 경고를 빠르게 없앨 수는 있지만, 정렬이나 삭제가 들어가는 화면에서는 UI 상태가 꼬이는 원인이 될 수 있습니다. 카드, 댓글, 장바구니, 관리자 목록처럼 데이터가 움직이는 화면이라면 위치가 아니라 항목 자체를 나타내는 값을 key로 잡아두는 쪽이 이후 수정에 더 안정적입니다. 자세한 기준은 React 공식 문서의 Rendering Lists, Preserving and Resetting State, Fragment, Special Props Warning을 함께 확인하면 됩니다.
함께 확인하면 좋은 기준
이 글과 관련해 실제 작업에서 같이 확인하면 좋은 기준입니다.
- React Rendering Lists: 리스트 key의 기본 규칙과 고유한 id 사용 기준을 확인합니다.
- React Special Props Warning:
key가 일반 props로 전달되지 않는 이유를 확인합니다. - React Fragment: Fragment를 반복 렌더링할 때 명시적
Fragment에 key를 붙이는 기준을 확인합니다.