본문 바로가기

조건부 렌더링

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 정리

조건부 렌더링의 다양한 방법을 학습했습니다:

  1. if문, 삼항연산자, 논리연산자를 활용한 기본 조건부 렌더링
  2. 복잡한 조건 처리를 위한 switch문과 함수 분리
  3. 컴포넌트 기반 조건부 렌더링 패턴
  4. 필터링과 검색 기능 구현
  5. 가독성과 유지보수성을 위한 Best Practices

다음 장에서는 리스트와 키(Keys)에 대해 알아보겠습니다.