조건부 렌더링 && 함정 리스트 key index 주의 📝 문제풀이
◀ 이전 📋 목차 CH 07 ▶
🔀 CHAPTER 06 · 중급

조건에 따라 보여주고, 목록을 그리기

화면은 항상 똑같지 않아요. 로그인했으면 이걸, 아니면 저걸 보여주고, 배열 데이터를 목록으로 쭉 그려야 하죠. 이번 장에서 조건부 렌더링과 리스트 렌더링(map · key)을 확실히 잡아봐요.

🎯 이 장을 끝내면
🔀
조건부 렌더링
조건에 따라 다른 걸 보여주기
"if"를 JSX 안에서 쓰는 여러 방법을 배워요.

조건부 렌더링(Conditional Rendering)은 어떤 조건이냐에 따라 화면에 다른 걸 보여주는 것이에요. 예를 들어 로그인 상태면 "로그아웃 버튼"을, 아니면 "로그인 버튼"을 보여주는 식이죠. JSX 안에서는 if 문을 직접 쓸 수 없어서, 주로 삼항 연산자나 논리 &&를 써요.

🔀
삼항 ? :
둘 중 하나를 보여줄 때
✅
논리 &&
있을 때만 보여줄 때
🗂️
변수 · early return
복잡할 때 미리 담거나 먼저 반환
🧩 헷갈리지 않게 — 이 중 뭐가 "자바스크립트"고 뭐가 "리액트"예요? 결론부터요. 이 장의 판단 로직(삼항·&&·map…)은 전부 순수 자바스크립트예요. 리액트에서만 통하는 특별한 문법이 아니라 어디서든 쓰는 JS 기술이죠. 리액트가 더하는 건 딱 몇 가지 "규칙"뿐이에요.
🟦 순수 자바스크립트 (어디서나 씀)
· 삼항 조건 ? A : B
· 논리 && / ||
· 배열 .map() · .filter()
· 변수에 담기 · early return
→ JS 지식이라 바닐라JS·Node에서도 그대로 써요.
🟪 리액트만의 규칙 (여기서만)
· JSX { } 안에 넣어야 화면에 반영
· false·null·undefined는 안 그림, 그런데 0은 그림
· 목록엔 key를 붙여야 함
→ 리액트 지식이에요.
👉 실무에서 "이거 JS야 리액트야?" 싶으면: 값을 계산·판단하는 부분이면 JS, "화면에 어떻게 반영되나"는 리액트라고 보면 대체로 맞아요.
① 삼항 연산자 조건 ? 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>
  );
}
③ 변수에 담기 · ④ early return 🟦 순수 JS 조건이 복잡하면 미리 변수에 JSX를 담아 두고 {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>
  );
}
🧭
고르는 기준. 둘 중 하나면 삼항, 있을 때만이면 &&, 복잡하면 변수/early return. 상황에 맞게 골라 쓰면 돼요.
⚠️
&& 함정
숫자 0이 화면에 그대로 찍히는 함정
&&를 쓸 때 가장 자주 걸리는 실수예요.

조건 && A에서 조건 자리에 숫자 0이 오면 문제가 생겨요. 자바스크립트에서 0은 falsy지만, 리액트는 0이라는 값을 그대로 화면에 그려버려요. 그래서 아무것도 안 나오길 기대했는데 화면에 "0"이 덩그러니 찍힙니다.

🔤
falsy — 자바스크립트에서 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을 화면에 그리는 규칙)의 합작이에요.
🧩 왜 '0'은 그대로 반환되는 것일까? 자세히 알기 — 펼쳐 보기
{count && <p>항목이 있어요</p>}
count가 0일 때, 화면에 아무것도 안 뜨는 게 아니라 "0"이라는 숫자가 그대로 찍히는 현상에 대해 설명해볼게요.

왜 이런 일이 벌어질까

1단계: && 연산자의 동작 원리 🟦 순수 JS

&&는 첫 번째 값이 falsy면 그 값 자체를 그대로 반환해요. (참/거짓으로 "변환"해서 반환하는 게 아니라, 원래 값 그대로 반환)
0 && "안녕"        // → 0  (0을 그대로 반환)
false && "안녕"    // → false
null && "안녕"     // → null
"" && "안녕"       // → ""
즉 count && <p>...</p>에서 count가 0이면, 이 표현식 전체의 결과값은 0이라는 숫자 자체예요. false가 아니에요.

2단계: JSX가 각 값을 렌더링(화면 표시)하는 규칙

JSX는 {} 안에 들어온 값에 대해 이런 규칙을 가져요:
값화면에 표시되나?
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"실제 데이터일 수 있다"고 간주 → 그대로 렌더링됨

JS 문법상 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() 메서드를 써요. 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 배열을 만들어 꽂는다고 기억하세요.
🔑
key
각 항목엔 고유한 key가 필요해요
리액트가 항목을 추적하는 이름표예요.

map으로 만든 각 요소에는 고유한 key를 붙여야 해요. key는 리액트가 "어떤 항목이 추가·삭제·변경됐는지"를 효율적으로 추적하기 위한 이름표예요(재조정 과정에서 사용). key가 없으면 콘솔에 경고가 뜨고, 목록이 바뀔 때 비효율적으로 다시 그려질 수 있어요.

🔤
재조정(reconciliation) — 리액트가 이전 화면과 새 화면을 비교해서 실제로 바뀐 부분만 골라 다시 그리는 과정이에요. 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
→
🔑
key 부여
key={item.id}
→
🧮
재조정
바뀐 항목만 추적
→
⚡
효율적 갱신
최소한만 다시 그림
🔑 key 규칙 • key는 형제 요소 사이에서만 고유하면 돼요(전체에서 유일할 필요는 없어요).
• key는 변하지 않는 값이 좋아요 — 보통 데이터의 고유 id를 씁니다.
• key는 리액트 내부용이라 컴포넌트 안에서 props처럼 읽을 수는 없어요.
🧭
왜 필요한가. 리액트는 이전 목록과 새 목록을 key로 짝지어 비교해요. key가 있으면 "3번 항목만 바뀌었네" 하고 그것만 갱신하지만, key가 없거나 불안정하면 전부 다시 그릴 수 있어요.
🚫
index 주의
index를 key로 쓰면 안 되는 경우
편하다고 배열 index를 key로 쓰면 버그가 나요.

고유 id가 없다고 map((item, index) => <li key={index}>)처럼 배열 index를 key로 쓰는 경우가 많아요. 목록이 절대 안 바뀌면(고정 순서, 추가·삭제 없음) 괜찮지만, 목록 순서가 바뀌거나 항목을 추가·삭제하면 버그가 생겨요.

🐛 왜 문제가 되나 맨 앞에 항목을 추가하면 모든 항목의 index가 한 칸씩 밀려요. 리액트는 key(=index)로 짝을 맞추는데, 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>
))}
🧩 고유 id가 정말 없다면?
데이터에 id가 없으면 데이터를 만들 때 id를 부여하는 게 가장 좋아요(예: crypto.randomUUID()). 정말 어쩔 수 없고 목록이 정적(추가·삭제·정렬 없음)이라면 그때만 index를 key로 써도 괜찮아요.
⚠️
정리. 가급적 고유 id를 key로 쓰세요. index는 "목록이 절대 안 바뀔 때"의 최후 수단이에요.
🧠 이 장 핵심 요약
📝
문제풀이 · 점검
배운 걸 가볍게 점검해봐요
시험이 아니라 "내가 이해했나" 확인용이에요. 틀려도 바로 해설이 나와요.
🧪
문제를 풀면 즉시 정답과 해설이 나오고, 위쪽 바에 점수가 쌓여요. 네 가지 유형(객관식 · O/X · 빈칸 · 개념결과)을 섞어 두었어요.
CH 07 ⏳ useEffect — 렌더링 후 부수효과 다루기 →