조건부 렌더링
if · 삼항 · && · null 반환으로 UI를 갈라 그리는 패턴과 흔한 함정 — 특히 `&&` 왼쪽의 숫자 0 함정을 정리합니다.
6장 마지막에 {lastSubmitted && ...} 같은 표현을 살짝 봤습니다. 본 챕터에서는 화면을 상태에 따라 다르게 그리는 조건부 렌더링 (Conditional Rendering) 패턴을 정리하겠습니다.
조건부 렌더링이란 #
로그인 여부에 따라 다른 메뉴를 보여 주거나, 데이터를 불러오는 동안 로딩 표시를 띄우거나, 입력 검증에 실패했을 때 에러 메시지를 보여 주는 일은 모든 앱에서 일어납니다. 어떤 조건에 따라 다른 JSX를 렌더링하는 것을 조건부 렌더링이라고 합니다.
리액트에서는 별도의 문법이 있는 게 아니라, 자바스크립트의 조건 표현을 그대로 활용 합니다. 가장 자주 쓰이는 네 가지 패턴을 살펴보겠습니다.
패턴 1. if 문으로 분기 (early return) #
가장 직관적인 방법은 컴포넌트 함수 안에서 if로 분기해 다른 JSX를 반환하는 것입니다.
function Greeting({ user }) {
if (!user) {
return <p>로그인이 필요합니다.</p>;
}
return <h1>안녕하세요, {user.name}님!</h1>;
}
export default Greeting;user가 없으면 로그인 안내를 반환하고 함수가 끝납니다. 두 번째 return은 user가 있을 때만 실행됩니다. 이렇게 함수를 일찍 빠져나가는 방식을 early return이라고 부르고, 분기가 큼직할 때 가독성이 좋습니다.
패턴 2. 삼항 연산자 (JSX 안에서 두 갈래) #
JSX 한가운데서 두 가지 중 하나를 선택해야 한다면 자바스크립트의 삼항 연산자(조건 ? A : B)를 씁니다.
function LoginButton({ isLoggedIn }) {
return (
<button>
{isLoggedIn ? '로그아웃' : '로그인'}
</button>
);
}
export default LoginButton;JSX의 중괄호 안에는 표현식만 들어갈 수 있다는 점을 기억하시나요? if 문은 문 (statement)이라 못 들어가지만, 삼항 연산자는 표현식이라 가능합니다.
JSX 자체를 두 갈래로 나누는 데도 쓸 수 있습니다.
function UserStatus({ user }) {
return (
<div>
{user ? (
<p>안녕하세요, {user.name}님!</p>
) : (
<p>로그인이 필요합니다.</p>
)}
</div>
);
}다만 삼항이 길어지면 가독성이 떨어지므로, JSX 덩어리가 크면 패턴 1 (early return)이나 변수로 분리하는 쪽이 좋습니다.
패턴 3. && 연산자 (보이거나 안 보이거나) #
“조건이 참일 때만 무언가를 보여 주고, 거짓이면 아무것도 안 보여 주고 싶다"는 케이스가 가장 많습니다. 이때는 && 연산자가 잘 맞습니다.
function Notification({ unreadCount }) {
return (
<div>
<h2>알림</h2>
{unreadCount > 0 && (
<p>읽지 않은 메시지가 {unreadCount}개 있습니다.</p>
)}
</div>
);
}
export default Notification;A && B는 자바스크립트에서 A가 참이면 B를 반환하고, 거짓이면 A를 반환합니다. unreadCount > 0이 참이면 <p>...</p>가 그 위치에 들어가고, 거짓이면 false가 들어갑니다. 리액트는 false, null, undefined를 화면에 아무것도 그리지 않으므로 결과적으로 사라진 것처럼 보입니다.
&& 사용 시 함정 — 숫자 0 #
&&의 흔한 함정 하나가 있습니다. 다음 코드를 보세요.
function Cart({ count }) {
return (
<div>
{count && <p>장바구니에 {count}개 담겼습니다.</p>}
</div>
);
}count가 0일 때 의도는 “아무것도 안 보이는 것” 이지만, 실제로는 화면에 0이라는 숫자가 그대로 출력 됩니다. 0 && X는 0을 반환하기 때문이고, 리액트는 숫자 0은 화면에 진짜로 그립니다 (false, null, undefined만 안 그려요).
해결책은 명시적으로 불리언으로 바꾸는 것입니다.
{count > 0 && <p>장바구니에 {count}개 담겼습니다.</p>}count > 0은 항상 true 또는 false이므로 안전합니다. && 왼쪽에는 명확한 불리언을 두는 습관을 들이면 이 함정에 안 빠집니다. 빈 문자열 ''도 같은 함정이 있으니, name && ... 대신 name.length > 0 && ...처럼 명확한 비교를 쓰는 게 안전합니다.
패턴 4. null 반환 (컴포넌트 자체를 안 그리기) #
조건이 만족되지 않으면 컴포넌트 전체가 화면에 나타나지 않아야 할 때는 null을 반환합니다.
function Banner({ message }) {
if (!message) return null;
return (
<div style={{ background: '#fffbcc', padding: '12px' }}>
{message}
</div>
);
}
export default Banner;null을 반환하면 리액트는 그 컴포넌트 위치에 아무것도 그리지 않습니다. 부모 입장에서는 <Banner message={...} />라고 항상 작성해 두면 되고, 보일지 말지는 Banner 본인이 결정하는 구조입니다. 깔끔합니다.
변수에 JSX 담아두기 #
분기가 복잡해지면 JSX를 변수에 담아 두고 그 변수를 쓰는 방식이 가독성이 좋습니다.
function Page({ status, data, error }) {
let content;
if (status === 'loading') {
content = <p>불러오는 중...</p>;
} else if (status === 'error') {
content = <p style={{ color: 'red' }}>에러: {error}</p>;
} else {
content = <p>데이터: {data}</p>;
}
return (
<div>
<h1>페이지 제목</h1>
{content}
</div>
);
}JSX는 그냥 자바스크립트 값일 뿐이므로 변수에 담거나, 함수에서 반환받거나, 객체에 넣어 둘 수 있습니다.
패턴 정리 #
| 상황 | 추천 패턴 |
|---|---|
| 분기 결과가 컴포넌트 전체 JSX인 경우 | early return (if) |
| 두 가지 중 하나를 보여 주고 싶은 경우 | 삼항 연산자 (A ? B : C) |
| 조건이 참일 때만 보여 주고 싶은 경우 | && 연산자 (왼쪽은 불리언으로) |
| 컴포넌트가 아예 안 보여야 하는 경우 | return null |
| 분기가 3가지 이상이거나 복잡한 경우 | 변수에 JSX 담아 사용 |
리액트만의 특별한 문법이 있는 것이 아니라 자바스크립트의 조건 표현을 JSX 안에서 그대로 사용 한다는 점이 핵심입니다.
직접 해보기 #
6장에서 만든 MessageForm을 확장해 봅니다. “이름을 입력하지 않으면 경고 표시”, “메시지를 입력하지 않으면 경고 표시”, “둘 다 정상이면 제출 가능"으로 동작하도록 만듭니다.
import { useState } from 'react';
function MessageForm() {
const [name, setName] = useState('');
const [message, setMessage] = useState('');
const [lastSubmitted, setLastSubmitted] = useState(null);
const isValid = name.length > 0 && message.length > 0;
function handleSubmit(e) {
e.preventDefault();
if (!isValid) return;
setLastSubmitted({ name, message });
setName('');
setMessage('');
}
return (
<div style={{ padding: '16px', border: '1px solid #ccc', borderRadius: '8px' }}>
<form onSubmit={handleSubmit}>
<div>
<input
type="text"
placeholder="이름"
value={name}
onChange={(e) => setName(e.target.value)}
/>
{name.length === 0 && (
<span style={{ color: 'red', marginLeft: '8px' }}>이름을 입력하세요</span>
)}
</div>
<div style={{ marginTop: '8px' }}>
<input
type="text"
placeholder="메시지"
value={message}
onChange={(e) => setMessage(e.target.value)}
/>
{message.length === 0 && (
<span style={{ color: 'red', marginLeft: '8px' }}>메시지를 입력하세요</span>
)}
</div>
<button type="submit" disabled={!isValid} style={{ marginTop: '8px' }}>
{isValid ? '추가' : '입력을 완료해주세요'}
</button>
</form>
{lastSubmitted ? (
<p style={{ marginTop: '12px' }}>
마지막 입력: <strong>{lastSubmitted.name}</strong>. {lastSubmitted.message}
</p>
) : (
<p style={{ marginTop: '12px', color: '#888' }}>
아직 제출된 메시지가 없습니다.
</p>
)}
</div>
);
}
export default MessageForm;여기서 쓴 조건부 렌더링 패턴들:
name.length === 0 && <span>...</span>—&&로 입력 안 됐을 때만 경고 표시disabled={!isValid}— 유효성에 따라 버튼 활성 / 비활성isValid ? '추가' : '입력을 완료해주세요'— 삼항으로 버튼 텍스트 분기lastSubmitted ? <p>...</p> : <p>...</p>— 삼항으로 안내 메시지 분기
여러 패턴이 한 화면에 자연스럽게 섞여 쓰이는 예시입니다.
연습문제 #
Page컴포넌트를 만들고statusprop ('loading'/'error'/'success')에 따라 다른 화면을 그리도록 만들어 보세요.'loading'이면 “불러오는 중”,'error'이면 빨간색 에러 메시지,'success'이면 데이터 표시. early return / 삼항 / 변수에 JSX 담기 세 가지 방식으로 각각 작성해 보고, 어느 쪽이 가장 읽기 쉬운지 비교해 보세요.&&함정 직접 만나 보기.countstate를 0부터 시작하는 카운터에서{count && <p>장바구니: {count}</p>}패턴을 써 보세요. 처음 화면에 의도와 다르게0이 표시될 겁니다. 그 뒤count > 0 && ...으로 고쳐 함정에서 빠져나오세요.Banner컴포넌트를 만들고messageprop이 있으면 노란 박스로 표시, 없으면null반환으로 작성해 보세요. 부모에서<Banner message={errorMessage} />처럼 항상 호출해 두고,errorMessagestate가 비었다 차곤 하는 흐름에서 자연스럽게 보였다 사라졌다 하는지 확인합니다.
한 줄 요약: 리액트의 조건부 렌더링은 자바스크립트의 조건 표현 그대로다. early return / 삼항 /
&&/null반환 / 변수에 JSX 담기 — 다섯 가지 패턴을 상황에 따라 고른다.&&왼쪽에는 명확한 불리언을 두어 숫자 0 함정을 피한다.
다음 챕터 #
지금까지는 화면에 보여 줄 데이터가 한두 개로 제한적이었습니다. 실제 앱에서는 글 목록, 상품 목록, 알림 목록처럼 여러 개의 데이터를 한꺼번에 그려야 합니다. 다음 8장 리스트와 key에서는 배열을 화면에 그리는 방법과, 그때 반드시 등장하는 **key**라는 특별한 prop의 의미를 다루겠습니다. 14장 (성능 최적화)의 reconciliation 알고리즘 이야기로의 토대가 됩니다.