조건부 렌더링
2. 조건부 렌더링
React에서는 JavaScript의 조건문을 사용해서 상황에 따라 다른 컴포넌트나 요소를 렌더링할 수 있습니다. 이를 통해 동적이고 상호작용적인 UI를 만들 수 있습니다.
2.1 기본 조건부 렌더링
2.1.1 if문 사용
import { useState } from 'react';
function UserGreeting({ isLoggedIn, username }) {
if (isLoggedIn) {
return <h1>안녕하세요, {username}님!</h1>;
}
return <h1>로그인해주세요.</h1>;
}
function App() {
const [isLoggedIn, setIsLoggedIn] = useState(false);
const username = '김개발';
return (
<div>
<UserGreeting isLoggedIn={isLoggedIn} username={username} />
<button onClick={() => setIsLoggedIn(!isLoggedIn)}>
{isLoggedIn ? '로그아웃' : '로그인'}
</button>
</div>
);
}
2.1.2 삼항 연산자 사용
import { useState } from 'react';
function LoginForm() {
const [isLoginMode, setIsLoginMode] = useState(true);
return (
<div>
<h2>{isLoginMode ? '로그인' : '회원가입'}</h2>
<form>
<div>
<input
type="email"
placeholder="이메일"
/>
</div>
<div>
<input
type="password"
placeholder="비밀번호"
/>
</div>
{!isLoginMode && (
<div>
<input
type="password"
placeholder="비밀번호 확인"
/>
</div>
)}
<button
type="submit"
>
{isLoginMode ? '로그인' : '회원가입'}
</button>
</form>
<p>
{isLoginMode ? '계정이 없으신가요? ' : '이미 계정이 있으신가요? '}
<button
onClick={() => setIsLoginMode(!isLoginMode)}
>
{isLoginMode ? '회원가입' : '로그인'}
</button>
</p>
</div>
);
}
2.2 논리 AND 연산자 (&&) 활용
import { useState } from 'react';
function NotificationCenter() {
const [notifications, setNotifications] = useState([
{ id: 1, type: 'info', message: '새로운 업데이트가 있습니다.' },
{ id: 2, type: 'warning', message: '저장하지 않은 변경사항이 있습니다.' },
{ id: 3, type: 'error', message: '네트워크 연결에 문제가 있습니다.' }
]);
const [showNotifications, setShowNotifications] = useState(true);
const removeNotification = (id) => {
setNotifications(prev => prev.filter(notif => notif.id !== id));
};
const getNotificationColor = (type) => {
switch (type) {
case 'info': return '#007bff';
case 'warning': return '#ffc107';
case 'error': return '#dc3545';
default: return '#6c757d';
}
};
return (
<div>
<div>
<h2>알림 센터</h2>
<button onClick={() => setShowNotifications(!showNotifications)}>
{showNotifications ? '숨기기' : '보이기'}
</button>
</div>
{/* 알림이 있을 때만 카운터 표시 */}
{notifications.length > 0 && (
<p>
{notifications.length}개의 알림이 있습니다.
</p>
)}
{/* 알림이 없을 때만 메시지 표시 */}
{notifications.length === 0 && (
<div>
<p>🔔</p>
<p>새로운 알림이 없습니다.</p>
</div>
)}
{/* 알림 표시가 활성화되고 알림이 있을 때만 리스트 표시 */}
{showNotifications && notifications.length > 0 && (
<div>
{notifications.map(notif => (
<div
key={notif.id}
>
<span>{notif.message}</span>
<button
onClick={() => removeNotification(notif.id)}
>
×
</button>
</div>
))}
{/* 전체 삭제 버튼 */}
{notifications.length > 1 && (
<button
onClick={() => setNotifications([])}
>
모든 알림 삭제
</button>
)}
</div>
)}
</div>
);
}
2.3 복잡한 조건부 렌더링
2.3.1 switch문을 활용한 다중 조건
import { useState } from 'react';
function StatusDisplay() {
const [status, setStatus] = useState('loading');
const renderContent = () => {
switch (status) {
case 'loading':
return (
<div>
<div>
</div>
<p>데이터를 불러오는 중...</p>
</div>
);
case 'success':
return (
<div>
<h3>✅ 성공!</h3>
<p>데이터를 성공적으로 불러왔습니다.</p>
<ul>
<li>사용자: 김개발</li>
<li>이메일: kim@example.com</li>
<li>역할: 개발자</li>
</ul>
</div>
);
case 'error':
return (
<div>
<h3>❌ 오류 발생</h3>
<p>데이터를 불러올 수 없습니다.</p>
<button
onClick={() => setStatus('loading')}
>
다시 시도
</button>
</div>
);
case 'empty':
return (
<div>
<h3>📭 데이터 없음</h3>
<p>표시할 데이터가 없습니다.</p>
</div>
);
default:
return <p>알 수 없는 상태입니다.</p>;
}
};
return (
<div>
<h2>상태별 UI 표시</h2>
<div>
<button
onClick={() => setStatus('loading')}
>
로딩
</button>
<button
onClick={() => setStatus('success')}
>
성공
</button>
<button
onClick={() => setStatus('error')}
>
오류
</button>
<button
onClick={() => setStatus('empty')}
>
비어있음
</button>
</div>
{renderContent()}
<style jsx>{`
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
`}</style>
</div>
);
}
2.4 컴포넌트 기반 조건부 렌더링
2.4.1 고차 컴포넌트(HOC) 패턴
import { useState } from 'react';
function withAuth(WrappedComponent) {
return function AuthComponent(props) {
const [isAuthenticated, setIsAuthenticated] = useState(false);
if (!isAuthenticated) {
return (
<div>
<h3>🔒 로그인이 필요합니다</h3>
<p>이 페이지에 접근하려면 로그인해야 합니다.</p>
<button
onClick={() => setIsAuthenticated(true)}
>
로그인
</button>
</div>
);
}
return <WrappedComponent {...props} isAuthenticated={isAuthenticated} />;
};
}
function Dashboard({ isAuthenticated }) {
return (
<div>
<h2>대시보드</h2>
<p>환영합니다! 대시보드에 접근하셨습니다.</p>
<div>
<div>
<h4>사용자</h4>
<p>1,234</p>
</div>
<div>
<h4>매출</h4>
<p>₩5,678,900</p>
</div>
<div>
<h4>주문</h4>
<p>89</p>
</div>
</div>
</div>
);
}
const AuthenticatedDashboard = withAuth(Dashboard);
2.5 실습: 쇼핑몰 제품 필터
조건부 렌더링을 활용한 제품 필터링 시스템을 만들어봅시다:
import { useState } from 'react';
function ProductFilter() {
const [products] = useState([
{ id: 1, name: 'MacBook Pro', price: 2500000, category: 'laptop', brand: 'Apple', inStock: true },
{ id: 2, name: 'Galaxy Book', price: 1200000, category: 'laptop', brand: 'Samsung', inStock: true },
{ id: 3, name: 'iPad Air', price: 800000, category: 'tablet', brand: 'Apple', inStock: false },
{ id: 4, name: 'Galaxy Tab', price: 600000, category: 'tablet', brand: 'Samsung', inStock: true },
{ id: 5, name: 'iPhone 15', price: 1300000, category: 'phone', brand: 'Apple', inStock: true },
{ id: 6, name: 'Galaxy S24', price: 1100000, category: 'phone', brand: 'Samsung', inStock: true },
{ id: 7, name: 'ThinkPad', price: 1800000, category: 'laptop', brand: 'Lenovo', inStock: false },
{ id: 8, name: 'Surface Pro', price: 1500000, category: 'tablet', brand: 'Microsoft', inStock: true },
]);
const [filters, setFilters] = useState({
category: 'all',
brand: 'all',
priceRange: 'all',
inStock: false
});
const [sortBy, setSortBy] = useState('name');
const handleFilterChange = (filterType, value) => {
setFilters(prev => ({
...prev,
[filterType]: value
}));
};
const filteredProducts = products.filter(product => {
// 카테고리 필터
if (filters.category !== 'all' && product.category !== filters.category) {
return false;
}
// 브랜드 필터
if (filters.brand !== 'all' && product.brand !== filters.brand) {
return false;
}
// 가격 범위 필터
if (filters.priceRange !== 'all') {
const [min, max] = filters.priceRange.split('-').map(Number);
if (product.price < min || product.price > max) {
return false;
}
}
// 재고 필터
if (filters.inStock && !product.inStock) {
return false;
}
return true;
});
const sortedProducts = [...filteredProducts].sort((a, b) => {
switch (sortBy) {
case 'name':
return a.name.localeCompare(b.name);
case 'price-low':
return a.price - b.price;
case 'price-high':
return b.price - a.price;
default:
return 0;
}
});
const formatPrice = (price) => {
return price.toLocaleString() + '원';
};
return (
<div>
<h2>제품 필터</h2>
{/* 필터 컨트롤 */}
<div>
<div>
<label>카테고리: </label>
<select
value={filters.category}
onChange={(e) => handleFilterChange('category', e.target.value)}
>
<option value="all">전체</option>
<option value="laptop">노트북</option>
<option value="tablet">태블릿</option>
<option value="phone">스마트폰</option>
</select>
</div>
<div>
<label>브랜드: </label>
<select
value={filters.brand}
onChange={(e) => handleFilterChange('brand', e.target.value)}
>
<option value="all">전체</option>
<option value="Apple">Apple</option>
<option value="Samsung">Samsung</option>
<option value="Lenovo">Lenovo</option>
<option value="Microsoft">Microsoft</option>
</select>
</div>
<div>
<label>가격대: </label>
<select
value={filters.priceRange}
onChange={(e) => handleFilterChange('priceRange', e.target.value)}
>
<option value="all">전체</option>
<option value="0-700000">70만원 이하</option>
<option value="700000-1500000">70만원 - 150만원</option>
<option value="1500000-999999999">150만원 이상</option>
</select>
</div>
<div>
<label>
<input
type="checkbox"
checked={filters.inStock}
onChange={(e) => handleFilterChange('inStock', e.target.checked)}
/>
재고 있는 상품만
</label>
</div>
<div>
<label>정렬: </label>
<select
value={sortBy}
onChange={(e) => setSortBy(e.target.value)}
>
<option value="name">이름순</option>
<option value="price-low">가격 낮은순</option>
<option value="price-high">가격 높은순</option>
</select>
</div>
</div>
{/* 결과 요약 */}
{filteredProducts.length > 0 ? (
<p>
총 {products.length}개 중 {filteredProducts.length}개 제품이 검색되었습니다.
</p>
) : (
<div>
<p>검색 조건에 맞는 제품이 없습니다.</p>
<button
onClick={() => setFilters({
category: 'all',
brand: 'all',
priceRange: 'all',
inStock: false
})}
>
필터 초기화
</button>
</div>
)}
{/* 제품 목록 */}
{sortedProducts.length > 0 && (
<div>
{sortedProducts.map(product => (
<div
key={product.id}
>
<h3>{product.name}</h3>
<p>
{product.brand} · {product.category === 'laptop' ? '노트북' :
product.category === 'tablet' ? '태블릿' : '스마트폰'}
</p>
<p>
{formatPrice(product.price)}
</p>
{/* 재고 상태 표시 */}
{product.inStock ? (
<span>
재고 있음
</span>
) : (
<span>
품절
</span>
)}
</div>
))}
</div>
)}
</div>
);
}
export default ProductFilter;
2.6 조건부 렌더링 Best Practices
2.6.1 복잡한 조건은 변수로 분리
// ✅ 좋은 예
function UserProfile({ user, isOwner, isPublic }) {
const canViewProfile = isPublic || isOwner;
const canEditProfile = isOwner;
const showContactInfo = user.showContact || isOwner;
return (
<div>
{canViewProfile && (
<div>
<h2>{user.name}</h2>
{showContactInfo && <p>{user.email}</p>}
{canEditProfile && <button>편집</button>}
</div>
)}
</div>
);
}
// ❌ 나쁜 예
function UserProfile({ user, isOwner, isPublic }) {
return (
<div>
{(isPublic || isOwner) && (
<div>
<h2>{user.name}</h2>
{(user.showContact || isOwner) && <p>{user.email}</p>}
{isOwner && <button>편집</button>}
</div>
)}
</div>
);
}
2.6.2 Early Return 활용
// ✅ 좋은 예
function LoadingComponent({ isLoading, error, data }) {
if (isLoading) {
return <div>로딩 중...</div>;
}
if (error) {
return <div>오류 발생: {error.message}</div>;
}
if (!data) {
return <div>데이터가 없습니다.</div>;
}
return (
<div>
{data.map(item => (
<div key={item.id}>{item.name}</div>
))}
</div>
);
}
2.7 정리
조건부 렌더링의 다양한 방법을 학습했습니다:
- if문, 삼항연산자, 논리연산자를 활용한 기본 조건부 렌더링
- 복잡한 조건 처리를 위한 switch문과 함수 분리
- 컴포넌트 기반 조건부 렌더링 패턴
- 필터링과 검색 기능 구현
- 가독성과 유지보수성을 위한 Best Practices
다음 장에서는 리스트와 키(Keys)에 대해 알아보겠습니다.