React key prop 정리

park·2026년 7월 10일

React key prop은 정확히 무슨 일을 할까

코드 리뷰에서 이런 질문을 받았습니다.

"React에서 key는 어떤 역할일까요?"

면접에서도 자주 나오는 주제라 이참에 제대로 정리했습니다. key만 따로 떼어 보면 이해가 잘 안 되고, Virtual DOM과 Reconciliation이라는 배경까지 같이 봐야 "아, 그래서 key가 필요하구나"가 됩니다. 그래서 배경부터 짚고 갑니다.

흔한 오해부터

"반복문에서 key를 안 쓰면 에러가 난다"고 알고 있는 경우가 많은데, 정확히는 에러가 아니라 경고(warning)입니다.

Warning: Each child in a list should have a unique "key" prop.

경고가 떠도 화면은 일단 동작합니다. 하지만 key를 제대로 주지 않으면 성능 저하상태(state) 관련 버그가 따라옵니다. key는 "경고를 끄는 용도"가 아니라, React가 리스트의 각 요소를 식별하는 핵심 장치입니다.

배경지식 3가지

key를 이해하려면 이 세 개념이 먼저 필요합니다.

Virtual DOM은 실제 DOM을 흉내 낸 메모리 상의 가벼운 JS 객체 트리입니다. 화면에 직접 그려지지는 않습니다. 실제 DOM을 조작하는 건 웹에서 가장 느린 작업이기 때문에, 일단 메모리에서 먼저 계산해 보려는 것입니다.

Reconciliation(재조정)은 state나 props가 바뀌었을 때, React가 새 Virtual DOM 트리를 만들고 이전 트리와 비교해서, 실제 DOM에 적용할 최소한의 변경만 계산해 내는 과정입니다.

Diffing 알고리즘은 그 두 트리를 비교하는 방법입니다. 원래 트리를 통째로 비교하면 O(n³)이지만, React는 몇 가지 가정을 두고 이걸 O(n)까지 낮췄습니다.

  1. 타입이 다르면 통째로 교체한다. (<div><span>으로 바뀌면 옛 트리는 버리고 새로 만든다)
  2. 같은 타입이면 속성만 업데이트하고 DOM 노드는 재사용한다.
  3. 리스트에서는 key로 "어떤 요소가 그대로 유지되는지" 개발자가 힌트를 준다.

세 번째가 바로 key가 등장하는 지점입니다.

key의 진짜 역할

한 문장으로 정리하면 이렇습니다.

key는 "이전 렌더의 어떤 컴포넌트가 = 이번 렌더의 이 컴포넌트인가?"를 알려주는 식별자다.

리스트에서 같은 타입의 요소(<li> 여러 개)가 나열되면, React는 각각을 구분할 방법이 필요합니다. 그 역할을 key가 합니다. 이때 key는 형제 요소들 사이에서만 유일하면 되고, 전역에서 유일할 필요는 없습니다.

한 가지 중요한 점. key는 props가 아닙니다. 컴포넌트 안에서 props.key로 읽을 수 없고, 오직 React 내부의 재조정용으로만 쓰입니다.

비유 — 데스크탑 파일 (공식 문서)

파일에 이름이 없고 "순서"로만 부른다고 상상해 봅시다. "첫 번째 파일, 두 번째 파일..." 이렇게요. 쓸 수는 있지만, 파일 하나를 지우는 순간 순서가 전부 밀려서 헷갈립니다. 반면 파일에 이름(= 안정적인 고유 key)이 있으면, 순서가 바뀌어도 어떤 게 어떤 건지 정확히 식별할 수 있습니다.

왜 index를 key로 쓰면 안 되나

key={index}를 쓰면 린트 경고는 사라집니다. 하지만 리스트가 바뀌는 순간(추가·삭제·정렬) 문제가 터집니다.

성능 문제

[Alice, Bob, Charlie]에 index로 key를 준다고 해봅시다.

<li key="0">Alice</li>
<li key="1">Bob</li>
<li key="2">Charlie</li>

여기서 맨 앞 Alice를 삭제하면 [Bob, Charlie]가 됩니다. 그런데 key가 index 기준이라 React는 이렇게 해석합니다.

key="0": Alice → Bob 으로 바뀜    → 텍스트 업데이트
key="1": Bob → Charlie 로 바뀜    → 텍스트 업데이트
key="2": 사라짐                   → <li> 제거

실제로 필요한 작업은 "맨 앞 노드 1개 삭제"뿐인데, 업데이트 2번 + 삭제 1번을 하고 있습니다. 리스트가 크고 각 항목이 무거우면 이 낭비가 커집니다.

고유 id를 key로 쓰면 이렇게 됩니다.

<li key="a1">Alice</li>  ← 이 key가 사라짐 → Alice만 정확히 제거
<li key="b2">Bob</li>
<li key="c3">Charlie</li>

상태 버그 — 이쪽이 더 무섭다

각 항목이 자체 상태를 가진 컴포넌트일 때(펼침/접힘, 체크박스, 입력값 등) 진짜 문제가 생깁니다.

리스트를 뒤집거나 정렬하면 index는 그대로인데 그 자리에 들어오는 내용물이 바뀝니다. React는 key=1이면 같은 컴포넌트라고 판단해서 상태를 그대로 유지합니다. 그 결과 Alice에 입력한 값이 엉뚱하게 Bob 자리에 남는 버그가 생깁니다.

여기서 반드시 기억할 규칙이 있습니다.

같은 key = 같은 컴포넌트 인스턴스 = 상태 유지
다른 key = 새 인스턴스 = 상태 초기화

그래서 어떻게 해야 하나

상황key로 쓸 것
데이터에 고유 id가 있음 (DB에서 온 데이터 등)그 id 사용 ← 최선
고유 id가 없음 (로컬에서 만든 데이터)생성 시점에 고유 id 부여 (crypto.randomUUID(), nanoid 등)
리스트가 절대 바뀌지 않음 (정렬·추가·삭제·필터 전혀 없음)index도 기술적으로는 안전 (예외적인 경우)
// 권장: 안정적이고 고유한 값
{products.map((product) => (
  <ProductCard key={product.id} {...product} />
))}

좋은 key가 갖춰야 할 조건은 세 가지입니다.

  1. 고유하다 — 형제 요소들 사이에서.
  2. 안정적이다 — 매 렌더마다 바뀌지 않는다. key={Math.random()}은 절대 금지입니다. 매번 새 값이 나와서 모든 항목이 매 렌더마다 새로 만들어집니다.
  3. 예측 가능하다 — 같은 데이터면 항상 같은 key가 나온다.

이 프로젝트에서의 적용

상품 리스트는 정렬·필터·무한스크롤로 항목이 계속 바뀌는 리스트입니다. 그래서 index를 key로 쓰면 정렬할 때 상태 버그가 날 위험이 있고, product.id를 key로 쓰는 게 정답입니다.

<ProductCard key={`product-card-${product.id}`} {...product} />

요약

  • key의 역할: 리스트에서 "이전 렌더의 어떤 요소가 이번 요소와 같은지"를 React가 식별하는 값. Reconciliation에서 최소한의 DOM 변경을 계산하는 근거가 된다.
  • index anti-pattern: 리스트가 바뀌면 index와 내용의 매칭이 어긋나서 ① 불필요한 DOM 연산(성능 저하) ② 상태가 엉뚱한 항목에 남는 버그가 생긴다.
  • 정답: 데이터의 안정적이고 고유한 id를 key로 쓴다. (같은 key면 상태 유지, 다른 key면 상태 초기화)
profile
park

0개의 댓글