전체 목록
ReactMedium#017

React의 key prop은 왜 중요하며, index를 key로 사용하면 안 되는 이유는?

#React#key#성능#리스트

답변 포인트

Reconciliation 과정에서 key의 역할을 생각해보세요.

정답 및 해설

빠른 요약

key는 React가 리스트에서 어떤 항목이 변경/추가/삭제되었는지 식별하는 데 사용됩니다. 안정적이고 유일한 key가 있으면 DOM 재사용이 가능해 성능이 향상됩니다.

key는 React가 리스트를 렌더링할 때 각 항목을 고유하게 식별하기 위해 사용하는 특별한 prop입니다. React는 상태가 변경되어 리렌더링이 발생할 때 key를 기반으로 어떤 요소가 추가/수정/삭제되었는지 판단하여 최소한의 DOM 조작을 수행합니다. key가 안정적이지 않거나 indexkey로 사용하면 컴포넌트 상태가 의도치 않게 유지되거나 초기화되는 버그가 발생할 수 있습니다.


key prop의 역할

Reconciliation(재조정) 과정에서의 역할

React는 Diffing 알고리즘을 통해 이전 Virtual DOM과 새 Virtual DOM을 비교합니다. 리스트의 경우 key가 없으면 순서에만 의존해 비교하므로 비효율적입니다.

JSX
// key 없이 리스트 렌더링 (권장하지 않음)
function BadList({ items }) {
  return (
    <ul>
      {items.map(item => (
        <li>{item.name}</li> // Warning: Each child should have a unique "key" prop
      ))}
    </ul>
  );
}

// key를 올바르게 사용한 리스트
function GoodList({ items }) {
  return (
    <ul>
      {items.map(item => (
        <li key={item.id}>{item.name}</li>
      ))}
    </ul>
  );
}

key가 있을 때와 없을 때의 동작 비교

TSX
 :
  <li key="a"></li>
  <li key="b"></li>
  <li key="c"></li>

  "포도"  :
  <li key="d"></li>     
  <li key="a"></li>    ()
  <li key="b"></li>   ()
  <li key="c"></li>    ()

key  // DOM    
TSX
key    "포도" :
  "포도"  ( 1 "사과" )  textContent 
  "사과"  ( 2 "바나나" )  textContent 
  "바나나"  ( 3 "체리" )  textContent 
  "체리"  ( )   

key       

index를 key로 사용하면 안 되는 이유

문제 1: 컴포넌트 상태가 잘못 유지됨

JSX
function TodoItem({ todo }) {
  const [isEditing, setIsEditing] = useState(false);
  const [editText, setEditText] = useState(todo.text);

  return (
    <li>
      {isEditing ? (
        <input value={editText} onChange={e => setEditText(e.target.value)} />
      ) : (
        <span>{todo.text}</span>
      )}
      <button onClick={() => setIsEditing(e => !e)}></button>
    </li>
  );
}

function BadTodoList() {
  const [todos, setTodos] = useState([
    { id: 1, text: '할일 1' },
    { id: 2, text: '할일 2' },
    { id: 3, text: '할일 3' },
  ]);

  const deleteFirst = () => {
    setTodos(prev => prev.slice(1)); // 첫 번째 항목 삭제
  };

  return (
    <ul>
      {todos.map((todo, index) => (
        // 문제: index를 key로 사용
        <TodoItem key={index} todo={todo} />
      ))}
    </ul>
  );
}
TSX
: "할일 2"  (isEditing=true)  
         "할일 1" ?

 :
  key=0: TodoItem (todo="할일 1", isEditing=false)
  key=1: TodoItem (todo="할일 2", isEditing=true)    
  key=2: TodoItem (todo="할일 3", isEditing=false)

  (index key):
  key=0: TodoItem (todo="할일 2")   key=0  !
  key=1: TodoItem (todo="할일 3")   key=1  !

: "할일 2"   key=0 
      key=0  (isEditing=false)  
        ! ()

올바른 해결법: 고유한 id를 key로 사용

JSX
function GoodTodoList() {
  const [todos, setTodos] = useState([
    { id: 1, text: '할일 1' },
    { id: 2, text: '할일 2' },
    { id: 3, text: '할일 3' },
  ]);

  const deleteFirst = () => {
    setTodos(prev => prev.slice(1));
  };

  return (
    <ul>
      {todos.map(todo => (
        // 올바른: todo.id를 key로 사용
        <TodoItem key={todo.id} todo={todo} />
      ))}
    </ul>
  );
}
TSX
  (id key):
  key=2: TodoItem (todo="할일 2", isEditing=true)     !
  key=3: TodoItem (todo="할일 3", isEditing=false)

React key=2      

문제 2: 입력 필드의 값이 잘못 유지됨

JSX
function InputList({ items }) {
  return (
    <div>
      {items.map((item, index) => (
        // index를 key로 사용: 순서 변경 시 입력값이 섞임
        <input key={index} defaultValue={item.text} />
      ))}
    </div>
  );
}

// 예시:
// items = ["사과", "바나나"]
// 두 번째 입력칸에 "바나나 수정"을 입력함
// 배열을 역순으로 바꾸면:
// items = ["바나나", "사과"]
// React는 key=0 컴포넌트에 "사과"를 주지만
// key=0 컴포넌트의 DOM 상태는 아직 "바나나 수정"을 기억함 (controlled가 아닌 경우)

문제 3: 애니메이션/트랜지션 오동작

JSX
import { CSSTransition, TransitionGroup } from 'react-transition-group';

function AnimatedList({ items }) {
  return (
    <TransitionGroup>
      {items.map((item, index) => (
        // index를 key로 사용 시: 항목 삭제/추가 애니메이션이 엉뚱한 요소에 적용됨
        <CSSTransition key={index} timeout={300} classNames="fade">
          <li>{item.text}</li>
        </CSSTransition>
      ))}
    </TransitionGroup>
  );
}

index를 key로 써도 되는 경우

모든 index 사용이 잘못된 것은 아닙니다. 다음 조건을 모두 만족할 때는 index를 사용해도 안전합니다.

JSX
// 안전한 index 사용 조건:
// 1. 리스트가 재정렬되지 않음
// 2. 필터링되지 않음 (삭제나 추가가 맨 뒤에서만 발생)
// 3. 항목에 컴포넌트 로컬 상태가 없음

// 안전한 예: 정적 목록
function StaticMenu({ menuItems }) {
  return (
    <nav>
      {menuItems.map((item, index) => (
        // 메뉴는 재정렬/삭제되지 않으므로 index 사용 가능
        <a key={index} href={item.href}>{item.label}</a>
      ))}
    </nav>
  );
}

// 안전한 예: 읽기 전용 단순 리스트
function ReadOnlyList({ names }) {
  return (
    <ul>
      {names.map((name, index) => (
        <li key={index}>{name}</li> // 로컬 상태 없음, 재정렬 없음
      ))}
    </ul>
  );
}

고유한 key 생성 전략

JSX
// 1. 데이터베이스 ID (가장 이상적)
items.map(item => <Item key={item.id} />)

// 2. 복합 키 (두 필드를 조합)
items.map(item => <Item key={`${item.userId}-${item.postId}`} />)

// 3. 새 항목 추가 시 ID 생성
import { v4 as uuidv4 } from 'uuid';

function AddItemForm() {
  const [items, setItems] = useState([]);

  const addItem = (text) => {
    setItems(prev => [...prev, { id: uuidv4(), text }]);
    // 또는: { id: crypto.randomUUID(), text }  (브라우저 내장)
  };
}

// 4. 안정적인 해시값 사용
// 주의: Math.random()은 매 렌더링마다 달라지므로 절대 사용 금지
items.map(item => <Item key={Math.random()} />) // 절대 금지!

key prop의 추가 활용: 컴포넌트 강제 리셋

key가 변경되면 React는 해당 컴포넌트를 언마운트하고 새로 마운트합니다. 이를 활용해 컴포넌트 상태를 의도적으로 초기화할 수 있습니다.

JSX
function UserProfile({ userId }) {
  const [formData, setFormData] = useState({});

  // userId가 바뀌어도 formData가 이전 사용자 것으로 유지되는 버그
  useEffect(() => {
    setFormData({}); // 이 방법은 깜빡임이 생길 수 있음
  }, [userId]);

  return <form>...</form>;
}

// key를 활용한 강제 리셋
function App() {
  const [selectedUserId, setSelectedUserId] = useState(1);

  return (
    // userId가 바뀌면 UserProfile이 완전히 새로 마운트됨
    <UserProfile key={selectedUserId} userId={selectedUserId} />
  );
}
JSX
// 폼 초기화에 활용
function FormContainer() {
  const [formKey, setFormKey] = useState(0);

  const resetForm = () => {
    setFormKey(prev => prev + 1); // key를 변경해 폼 컴포넌트 리마운트
  };

  return (
    <div>
      <ComplexForm key={formKey} />
      <button onClick={resetForm}> </button>
    </div>
  );
}

key prop 사용 규칙 요약

상황올바른 key잘못된 key
DB에서 온 데이터 목록item.idindex
재정렬 가능한 목록고유 식별자index
삭제/추가가 중간에서 일어나는 목록고유 식별자index
상태가 있는 자식 컴포넌트 목록고유 식별자index
완전히 정적이고 변경 없는 목록index 가능Math.random()
컴포넌트 강제 리셋변경되는 키 값항상 동일한 값

핵심 정리

  • key는 React가 리스트에서 어떤 항목이 변경/추가/삭제되었는지 판단하는 유일한 단서입니다.
  • indexkey로 사용하면 순서 변경, 삭제, 삽입 시 컴포넌트 상태가 잘못된 항목에 연결되는 버그가 발생합니다.
  • key는 반드시 안정적(stable)이고, 예측 가능하며(predictable), 형제 요소 간에 유일(unique)해야 합니다.
  • key 변경을 의도적으로 활용하면 컴포넌트를 강제로 언마운트/리마운트하여 상태를 초기화할 수 있습니다.

관련 질문

같은 카테고리/태그 기준