리액트 기초 강좌 #7 조건부 렌더링
지난 시간에는 이벤트 처리를 다루면서 마지막에 {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의 의미를 다뤄보겠습니다.