화면은 항상 똑같지 않아요. 로그인했으면 이걸, 아니면 저걸 보여주고, 배열 데이터를 목록으로 쭉 그려야 하죠. 이번 장에서 조건부 렌더링과 리스트 렌더링(map · key)을 확실히 잡아봐요.
조건부 렌더링(Conditional Rendering)은 어떤 조건이냐에 따라 화면에 다른 걸 보여주는 것이에요. 예를 들어 로그인 상태면 "로그아웃 버튼"을, 아니면 "로그인 버튼"을 보여주는 식이죠. JSX 안에서는 if 문을 직접 쓸 수 없어서, 주로 삼항 연산자나 논리 &&를 써요.
? :&&&&·map…)은 전부 순수 자바스크립트예요. 리액트에서만 통하는 특별한 문법이 아니라 어디서든 쓰는 JS 기술이죠. 리액트가 더하는 건 딱 몇 가지 "규칙"뿐이에요.
조건 ? A : B&& / ||.map() · .filter(){ } 안에 넣어야 화면에 반영false·null·undefined는 안 그림, 그런데 0은 그림key를 붙여야 함조건 ? A : B 🟦 순수 JS
둘 중 하나를 보여줄 때 딱이에요. "조건이 참이면 A, 아니면 B."
function Greeting({ isLogin }) {
return (
<div>
{isLogin ? <p>환영합니다!</p> : <p>로그인이 필요해요.</p>}
</div>
);
}
조건 && A 🟦 순수 JS
있을 때만 보여주고, 아니면 아무것도 안 보여줄 때 써요. 조건이 참이면 A가 렌더링되고, 거짓이면 아무것도 안 나와요("있으면 보여주고 없으면 말고").
function Inbox({ count }) {
return (
<div>
{count > 0 && <p>새 메시지 {count}개</p>}
</div>
);
}
{content}처럼 꽂거나, 컴포넌트 맨 위에서 early return(먼저 반환)으로 처리하면 깔끔해요.
function Profile({ user }) {
// early return: user가 없으면 여기서 끝
if (!user) return <p>로그인이 필요해요.</p>;
// 변수에 담기
const badge = user.isAdmin ? <span>👑 관리자</span> : null;
return (
<div>
<h2>{user.name}</h2>
{badge}
</div>
);
}
조건 && A에서 조건 자리에 숫자 0이 오면 문제가 생겨요. 자바스크립트에서 0은 falsy지만, 리액트는 0이라는 값을 그대로 화면에 그려버려요. 그래서 아무것도 안 나오길 기대했는데 화면에 "0"이 덩그러니 찍힙니다.
false처럼 "거짓"으로 취급되는 값이에요. 0, ""(빈 문자열), null, undefined, NaN이 여기에 속해요.function Cart({ count }) {
// ❌ count가 0이면 화면에 "0"이 그대로 찍힘!
return (
<div>
{count && <p>장바구니에 {count}개 담김</p>}
</div>
);
}
0 && <p>…</p>은 자바스크립트에서 왼쪽이 falsy면 그 값(0)을 그대로 반환해요. 즉 결과가 false가 아니라 숫자 0이에요. 리액트는 false·null·undefined는 화면에 안 그리지만, 0은 유효한 값이라 그대로 그려요.
🧩 이 함정은 절반은 JS(&&가 0을 반환하는 동작), 절반은 리액트(0을 화면에 그리는 규칙)의 합작이에요.
{count && <p>항목이 있어요</p>}
count가 0일 때, 화면에 아무것도 안 뜨는 게 아니라 "0"이라는 숫자가 그대로 찍히는 현상에 대해 설명해볼게요.
&& 연산자의 동작 원리 🟦 순수 JS
&&는 첫 번째 값이 falsy면 그 값 자체를 그대로 반환해요. (참/거짓으로 "변환"해서 반환하는 게 아니라, 원래 값 그대로 반환)0 && "안녕" // → 0 (0을 그대로 반환)
false && "안녕" // → false
null && "안녕" // → null
"" && "안녕" // → ""
count && <p>...</p>에서 count가 0이면, 이 표현식 전체의 결과값은 0이라는 숫자 자체예요. false가 아니에요.
{} 안에 들어온 값에 대해 이런 규칙을 가져요:| 값 | 화면에 표시되나? |
|---|---|
false | ❌ 안 보임 |
null | ❌ 안 보임 |
undefined | ❌ 안 보임 |
true | ❌ 안 보임 |
0 | ✅ "0"이 그대로 보임 |
"" (빈 문자열) | ❌ 안 보임 (화면상 안 보이지만 사실 렌더링은 됨, 그냥 빈 문자열이라 안 보이는 것뿐) |
| 일반 숫자/문자열 | ✅ 보임 |
false, null, undefined, true만 특별히 "숨겨주는" 예외로 만들었을까?
{user.isLoggedIn && <p>환영합니다</p>}
{errorMessage && <p>{errorMessage}</p>}
false거나 null일 때 리액트가 알아서 "아, 이건 그냥 표시하지 말라는 뜻이구나" 하고 자동으로 숨겨주는 게 편해서, false/null/undefined/true는 렌더링에서 아예 제외하도록 의도적으로 예외 처리를 해둔 거예요.
0은 이 예외 목록에 없어요
0은 falsy 값이긴 하지만, "진짜 화면에 보여줘야 하는 유효한 데이터"일 수 있어요. 예를 들어:<p>{count}개 남음</p> // count가 0이면 → "0개 남음"이 정상적으로 보여야 함
0도 자동으로 숨겨버리면, "수량이 0개"라는 정상적인 정보조차 화면에서 사라지는 심각한 부작용이 생겨요. 그래서 리액트는 0을 특별 취급 목록에서 일부러 빼놓았어요.| falsy 값 | JSX에서 취급 |
|---|---|
false, null, undefined, ture | "조건부 렌더링용 신호"로 간주 → 화면에 안 보이게 특별 처리 |
0 | "실제 데이터일 수 있다"고 간주 → 그대로 렌더링됨 |
count && <p>...</p>는 count가 falsy면 그 값 자체(0)를 반환하는데, 리액트는 false, null, undefined, ture만 "표시하지 말라"는 신호로 특별 취급하고, 0은 유의미한 데이터일 수 있어서 이 예외에서 제외했기 때문이에요.{count > 0 && <p>항목이 있어요</p>} // 명시적으로 boolean으로 만들기
{Boolean(count) && <p>...</p>}
{count ? <p>항목이 있어요</p> : null}
해결책은 조건을 진짜 불리언(true/false)으로 만들어 주는 거예요. count > 0처럼 비교식을 쓰면 결과가 true/false라 안전합니다.
function Cart({ count }) {
// ✅ count > 0 은 true/false → 0일 땐 아무것도 안 나옴
return (
<div>
{count > 0 && <p>장바구니에 {count}개 담김</p>}
</div>
);
}
{배열.length && …}도 같은 함정이에요. 배열이 비면 length가 0이라 화면에 "0"이 찍혀요. 항상 length > 0 && …처럼 비교식으로 감싸세요.화면에 여러 개를 반복해서 그릴 때는 배열의 map() 메서드를 써요. map은 배열의 각 항목을 JSX 요소로 변환해서 JSX 배열을 만들어요. 리액트는 그 배열을 받아 하나하나 화면에 그립니다.
.map()은 순수 자바스크립트 배열 메서드예요 — 리액트 전용이 아니라 어디서나 쓰는 JS 기능이죠. 리액트가 여기에 더하는 건 뒤에 나올 key 하나뿐이에요(그건 🟪 리액트 규칙).function FruitList() {
const fruits = ['사과', '바나나', '포도'];
return (
<ul>
{fruits.map((fruit) => (
<li>{fruit}</li>
))}
</ul>
);
}
// 결과: 사과 · 바나나 · 포도 세 개의 <li>
객체 배열이면 원하는 속성을 꺼내 쓰면 돼요.
const items = [
{ id: 1, name: '키보드' },
{ id: 2, name: '마우스' },
];
function ProductList() {
return (
<ul>
{items.map((item) => (
<li>{item.name}</li>
))}
</ul>
);
}
map." for 문 대신 map으로 데이터 배열 → JSX 배열을 만들어 꽂는다고 기억하세요.map으로 만든 각 요소에는 고유한 key를 붙여야 해요. key는 리액트가 "어떤 항목이 추가·삭제·변경됐는지"를 효율적으로 추적하기 위한 이름표예요(재조정 과정에서 사용). key가 없으면 콘솔에 경고가 뜨고, 목록이 바뀔 때 비효율적으로 다시 그려질 수 있어요.
const items = [
{ id: 1, name: '키보드' },
{ id: 2, name: '마우스' },
];
function ProductList() {
return (
<ul>
{items.map((item) => (
<li key={item.id}>{item.name}</li>
))}
</ul>
);
}
고유 id가 없다고 map((item, index) => <li key={index}>)처럼 배열 index를 key로 쓰는 경우가 많아요. 목록이 절대 안 바뀌면(고정 순서, 추가·삭제 없음) 괜찮지만, 목록 순서가 바뀌거나 항목을 추가·삭제하면 버그가 생겨요.
// ❌ 순서가 바뀌거나 추가/삭제되면 버그 위험
{items.map((item, index) => (
<li key={index}>{item.name}</li>
))}
// ✅ 데이터의 고유 id를 key로
{items.map((item) => (
<li key={item.id}>{item.name}</li>
))}
crypto.randomUUID()). 정말 어쩔 수 없고 목록이 정적(추가·삭제·정렬 없음)이라면 그때만 index를 key로 써도 괜찮아요.
? :, 있을 때만 → 논리 &&, 복잡하면 → 변수/early return.{count && …}에서 count가 0이면 화면에 "0"이 찍혀요 → {count > 0 && …}로 감싸기.map()으로 JSX 배열로 만들어 그려요.