조건부 렌더링과 리스트 — key가 진짜로 하는 일
리스트를 렌더링할 때마다 보이는 "key" 경고, 그냥 index를 넣어서 경고만 없애면 되는 걸까요? key는 React에서 정확히 어떤 역할을 하고 있을까요?
개념 정의
조건부 렌더링 은 특정 조건에 따라 다른 UI를 표시하는 기법이고, key 는 React가 리스트의 각 항목을 고유하게 식별 하여 효율적으로 DOM을 업데이트할 수 있게 하는 특수 prop입니다.
왜 필요한가
React의 Reconciliation(재조정) 알고리즘은 이전 트리와 새 트리를 비교할 때, 리스트의 각 항목이 어떤 것과 대응되는지 알아야 합니다. key가 없으면 React는 순서 기반으로만 비교 하므로, 항목이 삽입/삭제/이동될 때 불필요한 DOM 조작이 발생하거나 state가 꼬일 수 있습니다.
조건부 렌더링 패턴
if/else 문
JSX 바깥에서 사용하는 가장 직관적인 방식입니다.
function Greeting({ isLoggedIn }) {
if (isLoggedIn) {
return <h1>다시 오셨네요!</h1>;
}
return <h1>로그인해 주세요.</h1>;
}
삼항 연산자
JSX 내부에서 인라인으로 조건부 렌더링할 때 사용합니다.
function StatusBadge({ status }) {
return (
<span className={status === 'active' ? 'badge-green' : 'badge-gray'}>
{status === 'active' ? '활성' : '비활성'}
</span>
);
}
논리 AND (&&) 연산자
조건이 true일 때만 렌더링하는 간결한 방식입니다.
function Notification({ messages }) {
return (
<div>
{messages.length > 0 && (
<p>{messages.length}개의 새 메시지가 있습니다.</p>
)}
</div>
);
}
주의: falsy 값의 함정
// ❌ 위험 — 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 반환으로 숨기기
function WarningBanner({ warn }) {
if (!warn) {
return null; // 아무것도 렌더링하지 않음
}
return <div className="warning">경고!</div>;
}
key의 역할 — Reconciliation의 핵심
key가 없을 때의 문제
// ❌ key 없이 렌더링
function TodoList({ todos }) {
return (
<ul>
{todos.map(todo => (
<li>{todo.text}</li> // 경고: key prop 누락
))}
</ul>
);
}
key가 없으면 React는 인덱스 기반으로 비교합니다. 리스트 맨 앞에 항목을 추가하면 다음과 같은 일이 벌어집니다.
이전: [항목B, 항목C]
이후: [항목A, 항목B, 항목C]
key 없이 비교:
인덱스 0: 항목B → 항목A (변경 — 불필요한 업데이트)
인덱스 1: 항목C → 항목B (변경 — 불필요한 업데이트)
인덱스 2: 없음 → 항목C (추가)
key가 있을 때
// ✅ 고유한 key 사용
function TodoList({ todos }) {
return (
<ul>
{todos.map(todo => (
<li key={todo.id}>{todo.text}</li>
))}
</ul>
);
}
이전: [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로 쓰면 안 되는 이유
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를 초기화할 수 있습니다.
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 선택
// ✅ 데이터베이스 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는 안정적이고 고유하며 예측 가능한 값 이어야 합니다.
리스트 렌더링 실전 패턴
빈 리스트 처리
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>
);
}
중첩 리스트
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 && anything은 0을 반환하고, 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를 제대로 이해하면 리스트에서 발생하는 이상한 버그가 한번에 해결됩니다.