이번 글에서는 React의 렌더링 생명주기(Lifecycle)와 CSS 애니메이션 예시를 추가하여 key={index}
가 실제 애플리케이션에서 어떻게 예기치 않은 문제를 일으키는지 구체적으로 보여드리겠습니다.
React key
심층 분석: 당신이 index
를 key
로 써서는 안 되는 진짜 이유
"React 리스트 렌더링 시 key
prop에 index
를 사용하지 마세요." 이 조언은 React 개발자라면 누구나 한 번쯤 들어봤을 법한, 일종의 '관습'과도 같습니다. 하지만 왜 안되는 걸까요? '그냥 쓰면 안 좋다더라' 수준을 넘어, 그 내부 동작 원리와 실제 애플리케이션에 미치는 영향을 깊이 있게 파헤쳐 보겠습니다.
이 글에서는 컴포넌트의 생명주기(Lifecycle)와 실제 DOM 조작, 그리고 애니메이션 깨짐 현상까지, key={index}
가 초래하는 문제들을 구체적인 코드로 증명합니다.
1. key
의 본질: React의 '신분증'
React가 화면을 업데이트하는 과정인 재조정(Reconciliation)에서 key
는 각 엘리먼트를 식별하는 고유한 '신분증'입니다. React는 이 신분증을 보고 두 개의 Virtual DOM 트리에서 어떤 항목이 동일한 엘리먼트인지 빠르고 정확하게 판단합니다.|
이 과정을 시각적으로 이해해 봅시다.
- 안정적인
key
(id
) 사용 시: React는key="id-2"
인 컴포넌트 B가 위치만 이동했음을 정확히 인지하고 DOM 노드를 재사용합니다.key="id-3"
인 컴포넌트 C만 새로 추가됩니다. 이는 매우 효율적인 업데이트 방식입니다. - 불안정한
key
(index
) 사용 시: 배열 중간에 항목이 추가되면, 그 뒤의 모든 항목들의index
가 밀려납니다. React는key
가 모두 바뀌었다고 착각하여, 실제 내용은 그대로인 멀쩡한 컴포넌트들을 전부 파괴(Unmount)하고 새로 생성(Mount)하는 끔찍한 비효율을 초래합니다.
이제 이 '파괴와 재창조'가 실제 어떤 문제를 일으키는지 보겠습니다.
2. 치명적 오류: 컴포넌트 생명주기의 붕괴
컴포넌트는 생성(Mount)되고, 업데이트(Update)되고, 소멸(Unmount)되는 생명주기를 가집니다. useEffect
는 이 생명주기 과정에 개입하여 특정 로직을 수행하게 해주는 Hook입니다.
아래 예제는 각 아이템이 마운트될 때와 언마운트될 때를 콘솔에 기록하는 Item
컴포넌트입니다.
예제 코드:
ItemWithLifecycle.jsx
import React, { useEffect } from 'react';
const ItemWithLifecycle = ({ item }) => {
// 컴포넌트가 생성(Mount)되거나 소멸(Unmount)될 때 로그를 남깁니다.
useEffect(() => {
// Mount 시점: 컴포넌트가 처음 렌더링될 때 실행
console.log(`✅ [Mount] "${item.content}" 컴포넌트가 생성되었습니다.`);
// Unmount 시점: 컴포넌트가 사라지기 직전에 실행 (Cleanup 함수)
return () => {
console.log(`❌ [Unmount] "${item.content}" 컴포넌트가 소멸합니다.`);
};
}, [item.content]); // item.content가 바뀔 때마다 effect를 재실행하지 않도록 설정
return (
<li style={{ border: '1px solid #ddd', padding: '10px', margin: '5px', borderRadius: '5px' }}>
{item.content}
</li>
);
};
export default ItemWithLifecycle;
AppWithLifecycle.jsx
- key={index}
를 사용
import React, { useState } from 'react';
import ItemWithLifecycle from './ItemWithLifecycle';
const initialItems = [
{ id: 1, content: '항목 1' },
{ id: 2, content: '항목 2' },
];
function AppWithLifecycle() {
const [items, setItems] = useState(initialItems);
const addItemToTop = () => {
const newItem = { id: Date.now(), content: '새로운 항목' };
setItems([newItem, ...items]);
};
return (
<div>
<h2>생명주기 붕괴 예제 (key=index)</h2>
<button onClick={addItemToTop}>맨 위에 항목 추가</button>
<ul>
{items.map((item, index) => (
<ItemWithLifecycle key={index} item={item} />
))}
</ul>
</div>
);
}
export default AppWithLifecycle;
실행 결과 및 분석:
- 초기 렌더링:
✅ [Mount] "항목 1" 컴포넌트가 생성되었습니다.
✅ [Mount] "항목 2" 컴포넌트가 생성되었습니다.맨 위에 항목 추가
버튼 클릭 후:❌ [Unmount] "항목 1" 컴포넌트가 소멸합니다.
❌ [Unmount] "항목 2" 컴포넌트가 소멸합니다.
✅ [Mount] "새로운 항목" 컴포넌트가 생성되었습니다.
✅ [Mount] "항목 1" 컴포넌트가 생성되었습니다.
✅ [Mount] "항목 2" 컴포넌트가 생성되었습니다.
문제점: 단지 맨 앞에 새 항목 하나를 추가했을 뿐인데, 기존의 "항목 1", "항목 2" 컴포넌트가 모두 소멸(Unmount)된 후 다시 생성(Mount)되는 것을 볼 수 있습니다. index
가 key
로 사용되면서 React가 기존 컴포넌트들을 완전히 다른 것으로 오인했기 때문입니다.
만약 컴포넌트가 내부적으로 비동기 데이터 요청, 소켓 연결, 복잡한 상태 등을 관리하고 있었다면, 이 불필요한 소멸과 재생성 과정에서 모든 것이 초기화되고 심각한 버그를 유발할 것입니다.
해결책: key={item.id}
사용 시
AppWithLifecycle.jsx
의 map
부분만 key={item.id}
로 바꾸고 다시 테스트하면, 버튼 클릭 시 아래와 같은 로그만 찍힙니다.
✅ [Mount] "새로운 항목" 컴포넌트가 생성되었습니다.
기존 컴포넌트는 전혀 건드리지 않고, 새로운 컴포넌트만 효율적으로 추가됩니다. 이것이 key
를 올바르게 사용했을 때의 정상적인 동작입니다.
3. 시각적 오류: CSS 애니메이션의 실종
key={index}
의 문제는 여기서 그치지 않고 사용자 경험(UX)에도 직접적인 악영향을 줍니다. 부드러운 UI를 위해 적용한 CSS 트랜지션 및 애니메이션이 깨지는 현상이 대표적입니다.
예제 코드:
styles.css
.item-enter {
opacity: 0;
transform: translateY(-20px);
}
.item-enter-active {
opacity: 1;
transform: translateY(0);
transition: opacity 300ms, transform 300ms;
}
.item-exit {
opacity: 1;
}
.item-exit-active {
opacity: 0;
transform: scale(0.9);
transition: opacity 300ms, transform 300ms;
}
아이템이 삭제될 때, 해당 DOM 노드에 item-exit-active
클래스를 추가하여 부드럽게 사라지는 애니메이션을 기대한다고 가정해봅시다.
key={id}
사용 시: 리스트 중간의 아이템을 삭제하면, React는 정확히 해당key
를 가진 컴포넌트만 식별하여 DOM에서 제거합니다. 우리는 이 시점에 맞춰 애니메이션 클래스를 적용하여 부드러운 효과를 줄 수 있습니다. 나머지 아이템들은 그대로 유지됩니다.key={index}
사용 시: 중간 아이템을 삭제하면, 삭제된 아이템 뒤에 있던 모든 아이템들의index
가 변경됩니다. React는 이들을 모두 다른 컴포넌트로 인식하여 즉시 DOM에서 제거하고 새로운 컴포넌트를 그 자리에 다시 만듭니다. 결국, 삭제 애니메이션은 커녕, 리스트 전체가 번쩍이며 재정렬되는 어색한 화면을 마주하게 됩니다.
결론: key
는 타협의 대상이 아니다
key={index}
는 다음과 같은 심각한 문제를 야기합니다.
- 상태 불일치: 컴포넌트의 내부 상태(State)와 데이터가 뒤섞여 예측 불가능한 버그를 만듭니다.
- 생명주기 붕괴: 불필요한 컴포넌트의 소멸과 재생성을 유발하여 성능을 저하시키고,
useEffect
의 클린업 함수 등을 엉망으로 만듭니다. - 애니메이션 및 UX 저하: 부드러운 UI 전환을 방해하고 시각적인 결함을 초래합니다.
key
prop은 단순한 경고 메시지 제거용 옵션이 아니라, React 렌더링 시스템의 근간을 이루는 핵심적인 개념입니다. 항상 데이터가 가진 고유하고, 안정적이며, 예측 가능한 값을 key
로 사용하십시오. 만약 그런 값이 없다면, 데이터를 렌더링하기 전에 uuid
와 같은 라이브러리로 ID를 생성하여 부여해야 합니다.
안정적인 key
사용은 선택이 아닌 필수입니다. 이 원칙을 지키는 것만으로도 여러분의 React 애플리케이션은 훨씬 더 안정적이고 효율적으로 동작할 것입니다.
'개발 분야 (Development Area) > 프론트엔드 (Frontend)' 카테고리의 다른 글
[React] React Query와 Storybook: "No QueryClient set" 오류 해결 방법 (0) | 2025.02.24 |
---|---|
React useEffect 완벽 가이드: 햄버거 메뉴 구현에서 시작된 여정 (SSR 환경에서의 활용과 최적화)🚀 (2) | 2025.01.11 |
[React] React의 `createRoot`와 `hydrateRoot`이해하기 (0) | 2024.08.25 |
[React] 🛑 React에서 key={index} 사용의 위험성: 쉽게 이해하는 key 값과 React Reconciliation (4) | 2024.07.30 |
JavaScript reduce 메서드 활용하기 (2) | 2024.07.20 |