리스트를 렌더링할 때마다 보이는 "key" 경고, 그냥 index를 넣어서 경고만 없애면 되는 걸까요? key는 React에서 정확히 어떤 역할을 하고 있을까요?

개념 정의

조건부 렌더링 은 특정 조건에 따라 다른 UI를 표시하는 기법이고, key 는 React가 리스트의 각 항목을 고유하게 식별 하여 효율적으로 DOM을 업데이트할 수 있게 하는 특수 prop입니다.

왜 필요한가

React의 Reconciliation(재조정) 알고리즘은 이전 트리와 새 트리를 비교할 때, 리스트의 각 항목이 어떤 것과 대응되는지 알아야 합니다. key가 없으면 React는 순서 기반으로만 비교 하므로, 항목이 삽입/삭제/이동될 때 불필요한 DOM 조작이 발생하거나 state가 꼬일 수 있습니다.

조건부 렌더링 패턴

if/else 문

JSX 바깥에서 사용하는 가장 직관적인 방식입니다.

JSX
function Greeting({ isLoggedIn }) {
  if (isLoggedIn) {
    return <h1>다시 오셨네요!</h1>;
  }
  return <h1>로그인해 주세요.</h1>;
}

삼항 연산자

JSX 내부에서 인라인으로 조건부 렌더링할 때 사용합니다.

JSX
function StatusBadge({ status }) {
  return (
    <span className={status === 'active' ? 'badge-green' : 'badge-gray'}>
      {status === 'active' ? '활성' : '비활성'}
    </span>
  );
}

논리 AND (&&) 연산자

조건이 true일 때만 렌더링하는 간결한 방식입니다.

JSX
function Notification({ messages }) {
  return (
    <div>
      {messages.length > 0 && (
        <p>{messages.length}개의 새 메시지가 있습니다.</p>
      )}
    </div>
  );
}

주의: falsy 값의 함정

JSX
// ❌ 위험 — count가 0이면 화면에 "0"이 표시됨
{count && <Message count={count} />}

// ✅ 안전 — 명시적으로 boolean 변환
{count > 0 && <Message count={count} />}
{!!count && <Message count={count} />}
{Boolean(count) && <Message count={count} />}

JavaScript에서 0 && <Component />의 결과는 0이며, React는 숫자 0을 렌더링합니다.

null 반환으로 숨기기

JSX
function WarningBanner({ warn }) {
  if (!warn) {
    return null; // 아무것도 렌더링하지 않음
  }
  return <div className="warning">경고!</div>;
}

key의 역할 — Reconciliation의 핵심

key가 없을 때의 문제

JSX
// ❌ key 없이 렌더링
function TodoList({ todos }) {
  return (
    <ul>
      {todos.map(todo => (
        <li>{todo.text}</li>  // 경고: key prop 누락
      ))}
    </ul>
  );
}

key가 없으면 React는 인덱스 기반으로 비교합니다. 리스트 맨 앞에 항목을 추가하면 다음과 같은 일이 벌어집니다.

PLAINTEXT
이전:  [항목B, 항목C]
이후:  [항목A, 항목B, 항목C]

key 없이 비교:
  인덱스 0: 항목B → 항목A (변경 — 불필요한 업데이트)
  인덱스 1: 항목C → 항목B (변경 — 불필요한 업데이트)
  인덱스 2: 없음 → 항목C (추가)

key가 있을 때

JSX
// ✅ 고유한 key 사용
function TodoList({ todos }) {
  return (
    <ul>
      {todos.map(todo => (
        <li key={todo.id}>{todo.text}</li>
      ))}
    </ul>
  );
}
PLAINTEXT
이전:  [key:2=항목B, key:3=항목C]
이후:  [key:1=항목A, key:2=항목B, key:3=항목C]

key로 비교:
  key:1 항목A (새로 추가)
  key:2 항목B (그대로 유지)
  key:3 항목C (그대로 유지)

index를 key로 쓰면 안 되는 이유

JSX
function EditableList() {
  const [items, setItems] = useState([
    { id: 1, text: '첫 번째' },
    { id: 2, text: '두 번째' },
    { id: 3, text: '세 번째' },
  ]);

  const addToTop = () => {
    setItems([{ id: Date.now(), text: '새 항목' }, ...items]);
  };

  return (
    <div>
      <button onClick={addToTop}>맨 위에 추가</button>
      <ul>
        {items.map((item, index) => (
          // ❌ index key: 입력 필드의 값이 꼬임
          <li key={index}>
            <input defaultValue={item.text} />
          </li>
        ))}
      </ul>
    </div>
  );
}

맨 위에 항목을 추가하면, 모든 항목의 index가 밀립니다. React는 index가 같은 항목을 같은 컴포넌트로 인식하므로, input의 DOM 상태가 잘못된 항목과 매칭 됩니다.

index를 key로 써도 괜찮은 경우는 다음과 같습니다.

  • 리스트가 정적이고 변하지 않는 경우
  • 항목이 재정렬/삽입/삭제되지 않는 경우
  • 항목에 state가 없는 경우

key reset 패턴

key를 변경하면 React는 해당 컴포넌트를 완전히 새로 마운트 합니다. 이를 활용하면 컴포넌트의 state를 초기화할 수 있습니다.

JSX
function ProfilePage({ userId }) {
  // userId가 바뀌면 ProfileContent가 완전히 새로 마운트됨
  return <ProfileContent key={userId} userId={userId} />;
}

function ProfileContent({ userId }) {
  const [comment, setComment] = useState('');
  // key reset 덕분에 userId가 바뀌면 comment가 자동으로 ''로 초기화

  return (
    <div>
      <h2>사용자 {userId}의 프로필</h2>
      <textarea
        value={comment}
        onChange={(e) => setComment(e.target.value)}
        placeholder="댓글을 입력하세요"
      />
    </div>
  );
}

key reset 없이 같은 위치에서 props만 바뀌면, React는 같은 인스턴스를 재사용하므로 이전 state가 남아있게 됩니다.

올바른 key 선택

JSX
// ✅ 데이터베이스 ID — 가장 이상적
<li key={user.id}>{user.name}</li>

// ✅ 고유한 문자열
<li key={`${user.email}-${user.joinDate}`}>{user.name}</li>

// ❌ index — 순서 변경 시 문제
<li key={index}>{user.name}</li>

// ❌ Math.random() — 매 렌더마다 새로운 key → 항상 재마운트
<li key={Math.random()}>{user.name}</li>

// ❌ 렌더 중 생성한 값 — 의미 없음
<li key={crypto.randomUUID()}>{user.name}</li>

key는 안정적이고 고유하며 예측 가능한 값 이어야 합니다.

리스트 렌더링 실전 패턴

빈 리스트 처리

JSX
function UserList({ users }) {
  if (users.length === 0) {
    return <p className="empty-state">표시할 사용자가 없습니다.</p>;
  }

  return (
    <ul>
      {users.map(user => (
        <UserItem key={user.id} user={user} />
      ))}
    </ul>
  );
}

중첩 리스트

JSX
function CategoryList({ categories }) {
  return (
    <div>
      {categories.map(category => (
        <section key={category.id}>
          <h2>{category.name}</h2>
          <ul>
            {category.items.map(item => (
              <li key={item.id}>{item.name}</li>
            ))}
          </ul>
        </section>
      ))}
    </div>
  );
}

key는 형제 요소 사이에서만 고유 하면 됩니다. 다른 리스트의 key와 중복되어도 괜찮습니다.

주의할 점

Math.random()이나 crypto.randomUUID()를 key로 사용

매 렌더링마다 새로운 key가 생성되므로 React는 모든 항목을 매번 언마운트하고 재마운트 합니다. 리스트가 클수록 성능이 심각하게 저하되고, 입력 필드의 포커스가 매 렌더링마다 사라집니다.

복합 key에서의 순서 의존성

key={item.name + item.date}처럼 복합 key를 만들 때, 서로 다른 항목이 우연히 같은 key를 생성할 수 있습니다. 데이터베이스 ID처럼 보장된 고유값 을 우선 사용해야 합니다.

&& 연산자에서 0이 렌더링되는 문제

{count && <Message />}에서 count가 0이면 <Message />가 아닌 숫자 0 이 화면에 표시됩니다. JavaScript의 0 && anything0을 반환하고, React는 숫자를 렌더링하기 때문입니다. count > 0 && 또는 !!count &&로 명시적 boolean 변환이 필요합니다.

정리

항목설명
조건부 렌더링삼항 연산자, &&, 조기 return 등 상황에 맞게 선택
&& 주의점falsy 값(0, "")이 렌더링될 수 있음 — 명시적 boolean 변환 필요
key의 역할Reconciliation에서 항목을 고유 식별 하여 DOM 재사용 판단
index key순서 변경/삽입/삭제 시 state가 꼬임 — 정적 리스트에서만 허용
key reset 패턴key 변경으로 컴포넌트를 강제 재마운트하여 state 초기화
좋은 key안정적이고 고유하며 예측 가능한 값 (DB ID 등)

key는 단순한 경고 해소 수단이 아니라, React가 DOM을 효율적으로 업데이트하는 핵심 메커니즘입니다. key를 제대로 이해하면 리스트에서 발생하는 이상한 버그가 한번에 해결됩니다.

댓글 로딩 중...