티스토리 뷰

리액트로 개발을 하다 보면 콘솔창에서 가장 자주 마주치는 경고 중 하나가 바로 "Each child in a list should have a unique 'key' prop"입니다. 단순히 화면에 리스트를 뿌려주는 것뿐인데, 왜 리액트는 이토록 집요하게 Key 값을 요구하는 걸까요?
단순히 경고를 없애기 위해 index를 넣고 넘어갔다면, 여러분은 리액트의 가장 핵심적인 렌더링 최적화 메커니즘인 **재조정(Reconciliation)**의 기회를 놓치고 있는 것일지도 모릅니다.
1. Key의 본질: 리액트의 '기억력'을 돕는 이정표 (Deep Dive)
리액트는 상태가 변할 때마다 "가상 DOM(Virtual DOM)"을 만들어 이전 상태와 비교합니다. 이때 어떤 요소가 추가되었고, 삭제되었으며, 수정되었는지 파악하는 과정을 Diffing 알고리즘이라고 합니다.
왜 Key가 필요한가? (The Analogy)
호텔의 체크인 시스템을 상상해 보세요. 10명의 손님이 투숙 중인데, 갑자기 중간에 새로운 손님 한 명이 끼어들었습니다. 만약 손님들에게 방 번호(Key)가 없다면, 호텔 매니저는 모든 손님의 얼굴을 대조하며 누가 누구인지 처음부터 끝까지 다시 확인해야 합니다.
하지만 모든 손님에게 고유한 방 번호가 부여되어 있다면 어떨까요? 매니저는 1번부터 10번 방 손님은 그대로 두고, 새로 생긴 '1.5번 방' 손님만 확인하면 됩니다. 리액트에서 Key는 바로 이 방 번호와 같은 역할을 합니다.
작동 원리: $O(n^3)$을 $O(n)$으로 줄이는 마법
일반적인 트리 구조 비교 알고리즘은 $O(n^3)$의 시간 복잡도를 가집니다. 요소가 1,000개만 되어도 연산량이 10억 번에 달하죠. 리액트는 두 가지 가정을 통해 이를 **$O(n)$**으로 최적화합니다.
- 서로 다른 타입을 가진 두 엘리먼트는 서로 다른 트리를 형성한다.
- Key를 통해 여러 렌더링 사이에서 어떤 자식 엘리먼트가 변경되지 않아야 할지 식별할 수 있다.
2. 실전 예제: 스마트 쇼핑 카트 구현 (Hands-on)
단순한 문자열 배열이 아니라, 사용자가 수량을 변경하거나 항목을 삭제할 수 있는 쇼핑 카트 로직을 통해 Key의 중요성을 살펴보겠습니다.
import React, { useState } from 'react';
const ShoppingCart = () => {
const [items, setItems] = useState([
{ id: 'p1', name: '고성능 기계식 키보드', price: 150000, quantity: 1 },
{ id: 'p2', name: '에르고노믹 마우스', price: 80000, quantity: 1 },
]);
const addItem = () => {
const newItem = {
id: `p${Date.now()}`, // 고유 ID 생성
name: '신규 추가 상품',
price: 30000,
quantity: 1
};
// 리스트 상단에 추가하여 순서 변화 유도
setItems([newItem, ...items]);
};
return (
<div className="cart-container">
<h2>장바구니 리스트</h2>
<button onClick={addItem}>상품 추가</button>
<ul>
{items.map((item) => (
// key={item.id}를 통해 리액트가 각 항목의 정체성을 파악함
<li key={item.id} className="cart-item">
<span>{item.name}</span>
<input
type="number"
defaultValue={item.quantity}
onBlur={(e) => console.log(`${item.name} 수량 변경: ${e.target.value}`)}
/>
<strong>{item.price.toLocaleString()}원</strong>
</li>
))}
</ul>
</div>
);
};
export default ShoppingCart;
💡 트래킹 포인트: 왜 index를 Key로 쓰면 안 될까?
위 예제에서 addItem 함수는 새로운 아이템을 배열의 **맨 앞(0번 인덱스)**에 추가합니다.
- Index를 Key로 쓴 경우: 리액트는 0번 인덱스에 새로운 데이터가 온 것을 보고 "0번 방 손님이 바뀌었네?"라고 착각하여 모든 리스트 아이템을 새로 그립니다(Remount). 특히 input 태그에 입력 중이던 값들이 엉뚱한 칸으로 밀리거나 초기화되는 끔찍한 버그가 발생할 수 있습니다.
- Unique ID를 Key로 쓴 경우: 리액트는 기존 p1, p2 키를 가진 엘리먼트 위치가 바뀌었을 뿐 동일한 객체임을 인지합니다. 따라서 기존 엘리먼트는 그대로 두고 새 엘리먼트만 상단에 삽입합니다. 성능과 사용자 경험(UX) 모두를 잡는 비결입니다.
3. 트러블슈팅: Key 관련 흔한 실수들
- Math.random() 사용 금지: 렌더링이 일어날 때마다 Key가 새로 생성됩니다. 리액트는 매번 "완전히 새로운 컴포넌트가 나타났다"고 판단하여 매번 DOM을 파괴하고 다시 만듭니다. 이는 심각한 성능 저하와 포커스 손실을 야기합니다.
- 중복된 Key: 동일한 Key가 두 개 이상 존재하면 리액트는 어떤 업데이트를 어디에 적용할지 몰라 렌더링 오류를 뱉거나 리스트 일부를 누락시킵니다. 데이터베이스의 Primary Key를 사용하는 것이 가장 안전합니다.
4. 트레이드오프 (Trade-offs)
모든 상황에서 고유 ID가 필수일까요?
| 구분 | Index 사용 가능 상황 | 고유 ID(UUID/DB ID) 필수 상황 |
| 데이터 성격 | 정적인 리스트 (변경/필터링 없음) | 동적인 리스트 (추가/삭제/정렬 발생) |
| 복잡도 | 단순 텍스트 출력 위주 | 입력 폼, 체크박스 등 상태를 가진 컴포넌트 |
| 성능 | 무관함 | 리스트가 길수록 고유 ID가 압도적으로 유리 |
5. 요약 및 제언
리액트에서 Key는 단순한 경고 제거용 속성이 아니라, **컴포넌트의 정체성(Identity)**을 규정하는 식별자입니다.
- 재사용성 극대화: 고유한 Key는 불필요한 DOM 조작을 방지합니다.
- 상태 보존: 리스트의 순서가 바뀌어도 각 항목 내부의 state나 focus를 유지해 줍니다.
- 최적화: 리액트의 Diffing 알고리즘이 $O(n)$으로 동작하게 만드는 핵심 장치입니다.
여러분의 프로젝트에서는 현재 리스트 렌더링 시 어떤 값을 Key로 활용하고 계신가요? 혹시 API 설계 단계에서부터 고유 ID를 명확히 정의하고 있는지 다시 한번 점검해 보시기 바랍니다.
'Frontend > React' 카테고리의 다른 글
| CORS 에러? 리액트 Proxy 설정으로 간단하게 해결하기 (0) | 2026.03.01 |
|---|---|
| React Router v6: 선언적 라우팅으로 설계하는 견고한 SPA 내비게이션 (0) | 2026.03.01 |
| React 무한 루프의 늪, "Too many re-renders" 완벽하게 탈출하기 (0) | 2026.03.01 |
| Context API vs Redux: 실무에서 결정적인 선택을 만드는 차이점 (0) | 2026.03.01 |
| useRef 완벽 가이드: DOM 접근부터 데이터 저장까지 (0) | 2026.02.25 |
- Total
- Today
- Yesterday
- 협력
- 카카오
- 엣지컴퓨팅
- AI
- prompt engineering
- SSR
- 스마트안경
- sLLM
- HBM
- Javascript
- on-device ai
- Rag
- It용어
- LLM
- CSR
- TypeScript
- 멀티모달
- MSA
- react
- CSS
- 웹기초
- 구글
- java
- HTML
- Nextjs
| 일 | 월 | 화 | 수 | 목 | 금 | 토 |
|---|---|---|---|---|---|---|
| 1 | 2 | 3 | 4 | |||
| 5 | 6 | 7 | 8 | 9 | 10 | 11 |
| 12 | 13 | 14 | 15 | 16 | 17 | 18 |
| 19 | 20 | 21 | 22 | 23 | 24 | 25 |
| 26 | 27 | 28 | 29 | 30 |