지난 시간에는 이벤트 처리를 다루면서 마지막에 {lastSubmitted && ...} 같은 표현을 살짝 봤습니다. 이번 시간에는 화면을 상태에 따라 다르게 그리는 조건부 렌더링(Conditional Rendering) 패턴을 정리해보겠습니다.
조건부 렌더링이란
로그인 여부에 따라 다른 메뉴를 보여주거나, 데이터를 불러오는 동안 로딩 표시를 띄우거나, 입력 검증에 실패했을 때 에러 메시지를 보여주는 일은 모든 앱에서 일어납니다. 이렇게 어떤 조건에 따라 다른 JSX를 렌더링하는 것을 조건부 렌더링이라고 합니다.
리액트에서는 별도의 문법이 있는 게 아니라, 자바스크립트의 조건 표현을 그대로 활용합니다. 가장 자주 쓰이는 4가지 패턴을 살펴보겠습니다.
패턴 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이므로 안전합니다. && 왼쪽에는 명확한 불리언을 두는 습관을 들이면 이 함정에 안 빠집니다.
패턴 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 안에서 그대로 사용한다는 점이 핵심입니다.
직접 해보기
지난 글에서 만든 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>— 삼항으로 안내 메시지 분기
여러 패턴이 한 화면에 자연스럽게 섞여 쓰이는 예시입니다. 직접 입력해보면서 화면이 어떻게 반응하는지 관찰해보세요.
마무리
이번 글에서는 조건부 렌더링의 4가지 패턴(early return, 삼항, &&, null 반환)과 변수에 JSX 담기를 살펴봤습니다. 어느 하나가 정답인 것이 아니라 상황에 따라 가장 가독성 좋은 패턴을 고르는 감각이 중요합니다. 그리고 && 사용 시 숫자 0 함정만 잘 피하면 됩니다.
지금까지는 화면에 보여줄 데이터가 한두 개로 제한적이었습니다. 하지만 실제 앱에서는 글 목록, 상품 목록, 알림 목록처럼 여러 개의 데이터를 한꺼번에 그려야 합니다. 다음 글인 "리액트 기초 강좌 #8 리스트와 key"에서는 배열을 화면에 그리는 방법과, 그때 반드시 등장하는 **key**라는 특별한 prop의 의미를 다뤄보겠습니다.