React - 헤더 UI, 린캔버스 목록 UI

김명원·2025년 1월 8일
0

learnReact

목록 보기
13/26

헤더 UI

React Icon 설치

React Icons는 다양한 아이콘을 쉽게 사용할 수 있게 해주는 라이브러리입니다. 설치 명령어는 다음과 같습니다:

npm install react-icons --save

헤더 UI

1. src/components/Header.css

헤더의 기본 레이아웃을 설정하는 CSS 파일입니다.

.nav {
  display: flex;
  justify-content: space-between;
}

분석:

  • .nav 클래스는 Flexbox를 사용하여 자식 요소들을 수평으로 배치하고, justify-content: space-between을 통해 양 끝에 요소들을 정렬합니다. 이를 통해 네비게이션 링크와 로고가 양쪽에 배치됩니다.

2. src/components/Header.jsx

헤더 컴포넌트의 React 코드입니다. React Icons를 활용하여 아이콘을 추가하고, 반응형 메뉴를 구현합니다.

// import './Header.css';
import { Link, NavLink } from 'react-router-dom';
import {
  FaHome,
  FaInfoCircle,
  FaEnvelope,
  FaBars,
  FaTimes,
} from 'react-icons/fa';
import { useState } from 'react';

function Header() {
  const navItems = [
    { id: 'home', label: 'Home', icon: <FaHome />, to: '/' },
    { id: 'about', label: 'About', icon: <FaInfoCircle />, to: '/about' },
    { id: 'contact', label: 'Contact', icon: <FaEnvelope />, to: '/contact' },
  ];

  const [isMenuOpen, setIsMenuOpen] = useState(false);
  const toggleMenu = () => setIsMenuOpen(!isMenuOpen);

  return (
    <header className="sticky top-0 bg-gray-800 text-white px-4">
      <div className="container mx-auto flex justify-between items-center h-14">
        {/* 로고 */}
        <div>
          <Link to="/" className="text-xl font-bold">
            Lean Canvas
          </Link>
        </div>

        {/* 데스크탑 네비게이션 */}
        <nav className="hidden md:flex space-x-4">
          {navItems.map(item => (
            <NavLink
              key={item.id}
              to={item.to}
              className={({ isActive }) =>
                isActive ? 'text-blue-700' : 'hover:text-gray-300'
              }
            >
              {item.icon} {item.label}
            </NavLink>
          ))}
        </nav>

        {/* 모바일 메뉴 토글 버튼 */}
        <button className="md:hidden" onClick={toggleMenu}>
          <FaBars />
        </button>

        {/* 데스크탑 버튼 */}
        <button className="hidden md:block bg-blue-500 hover:bg-blue-600 text-white font-bold py-1.5 px-4 rounded transition-colors duration-300 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-opacity-50">
          짐코딩 강의
        </button>
      </div>

      {/* 모바일 메뉴 */}
      <aside
        className={`
          fixed top-0 left-0 w-64 h-full bg-gray-800 z-50
          ${isMenuOpen ? 'translate-x-0' : '-translate-x-full'}
          md:hidden transform transition-transform duration-300 ease-in-out
        `}
      >
        <div className="flex justify-end p-4">
          <button
            className="text-white focus:outline-none"
            aria-label="Close menu"
            onClick={toggleMenu}
          >
            <FaTimes className="h-6 w-6" />
          </button>
        </div>
        <nav className="flex flex-col space-y-4 p-4">
          {navItems.map(item => (
            <NavLink
              key={item.id}
              to={item.to}
              className="hover:text-gray-300"
              onClick={toggleMenu} // 메뉴 클릭 시 닫기
            >
              {item.icon} {item.label}
            </NavLink>
          ))}
        </nav>
      </aside>
    </header>
  );
}

export default Header;

분석:

  • React Icons 사용: FaHome, FaInfoCircle, FaEnvelope, FaBars, FaTimes 아이콘을 임포트하여 네비게이션 링크와 메뉴 토글 버튼에 사용합니다.
  • 반응형 네비게이션: hidden md:flex 클래스를 사용하여 데스크탑에서는 네비게이션이 보이고, 모바일에서는 숨겨집니다. 반대로, 모바일 메뉴 토글 버튼은 md:hidden 클래스로 데스크탑에서는 숨겨집니다.
  • 상태 관리: useState 훅을 사용하여 모바일 메뉴의 열림/닫힘 상태를 관리합니다. toggleMenu 함수로 상태를 전환합니다.
  • NavLink 활용: <NavLink>는 현재 활성화된 링크에 text-blue-700 클래스를 적용하여 시각적으로 강조합니다.
  • 모바일 메뉴 애니메이션: translate-x-0-translate-x-full 클래스를 사용하여 모바일 메뉴가 슬라이드 인/아웃되도록 애니메이션을 적용합니다.
  • 접근성: 버튼에 aria-label을 추가하여 스크린 리더 사용자에게 메뉴 토글 버튼의 목적을 명확히 전달합니다.

참고

헤더 UI 구현과 관련된 더 자세한 내용은 공식 React Router 문서와 React Icons 문서를 참고하세요.

린캔버스 목록 UI

실습 - 목록 UI 마크업


  • src/pages/Home.jsx

    import { useState } from 'react';
    import { Link } from 'react-router-dom';
    import { FaSearch, FaList, FaTh } from 'react-icons/fa';
    
    function Home() {
      const [searchText, setSearchText] = useState('');
      const [isGridView, setIsGridView] = useState(true);
      const dummyData = [
        {
          id: 1,
          title: '친환경 도시 농업 플랫폼',
          lastModified: '2023-06-15',
          category: '농업',
        },
        {
          id: 2,
          title: 'AI 기반 건강 관리 앱',
          lastModified: '2023-06-10',
          category: '헬스케어',
        },
        {
          id: 3,
          title: '온디맨드 물류 서비스',
          lastModified: '2023-06-05',
          category: '물류',
        },
        {
          id: 4,
          title: 'VR 가상 여행 서비스',
          lastModified: '2023-06-01',
          category: '여행',
        },
      ];
    
      const filteredData = dummyData.filter(item =>
        item.title.toLowerCase().includes(searchText.toLowerCase()),
      );
      return (
        <div className="container mx-auto px-4 py-16">
          <div className="mb-6 flex flex-col sm:flex-row items-center justify-between">
            <div className="relative w-full sm:w-64 mb-4 sm:mb-0">
              <input
                type="text"
                placeholder="검색"
                className="w-full pl-10 pr-4 py-2 border rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500"
                value={searchText}
                onChange={e => setSearchText(e.target.value)}
                aria-label="검색"
              />
              <FaSearch className="absolute left-3 top-3 text-gray-400" />
            </div>
            <div className="flex space-x-2">
              <button
                onClick={() => setIsGridView(true)}
                className={`p-2 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 ${isGridView ? 'bg-blue-500 text-white' : 'bg-gray-200'}`}
                aria-label="Grid view"
              >
                <FaTh />
              </button>
              <button
                onClick={() => setIsGridView(false)}
                className={`p-2 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 ${!isGridView ? 'bg-blue-500 text-white' : 'bg-gray-200'}`}
                aria-label="List view"
              >
                <FaList />
              </button>
            </div>
          </div>
          {filteredData.length === 0 ? (
            <div className="text-center py-10">
              <p className="text-xl text-gray-600">
                {searchText ? '검색 결과가 없습니다' : '목록이 없습니다'}
              </p>
            </div>
          ) : (
            <div
              className={`grid gap-6 ${isGridView ? 'grid-cols-1 sm:grid-cols-2 lg:grid-cols-3' : 'grid-cols-1'}`}
            >
              {filteredData.map(item => (
                <Link
                  key={item.id}
                  className="bg-white rounded-lg shadow-md overflow-hidden transition-transform duration-300 hover:scale-105"
                  to={`/canvases/${item.id}`}
                >
                  <div className="p-6">
                    <h2 className="text-2xl font-bold mb-2 text-gray-800">
                      {item.title}
                    </h2>
                    <p className="text-sm text-gray-600 mb-4">
                      최근 수정일: {item.lastModified}
                    </p>
                    <span className="inline-block px-3 py-1 text-sm font-semibold text-gray-700 bg-gray-200 rounded-full">
                      {item.category}
                    </span>
                  </div>
                </Link>
              ))}
            </div>
          )}
        </div>
      );
    }
    
    export default Home;

코드 분석

  • 검색 기능 추가 (useState 사용):

    • searchText 상태를 도입하여 사용자가 입력한 검색어를 관리합니다.
    • input 필드의 valueonChange 핸들러를 통해 실시간으로 searchText를 업데이트합니다.
    • filteredDatadummyDatasearchText에 따라 필터링하여 검색 결과를 보여줍니다.
  • 그리드 및 리스트 보기 토글 (isGridView 상태):

    • isGridView 상태를 사용하여 그리드 보기와 리스트 보기 사이를 전환할 수 있습니다.
    • 두 개의 버튼 (FaTh 아이콘과 FaList 아이콘)을 통해 사용자에게 보기 방식을 선택할 수 있는 인터페이스를 제공합니다.
    • 버튼 클릭 시 isGridView 상태가 변경되며, 이에 따라 CSS 클래스가 동적으로 적용되어 레이아웃이 변경됩니다.
  • 더미 데이터 (dummyData):

    • 예시 데이터를 dummyData 배열로 정의하여 UI에 표시할 항목들을 관리합니다.
    • 각 항목은 id, title, lastModified, category 속성을 포함합니다.
  • 조건부 렌더링:

    • filteredData의 길이에 따라 "목록이 없습니다" 또는 "검색 결과가 없습니다" 메시지를 표시합니다.
    • 데이터가 존재할 경우 그리드 또는 리스트 형식으로 데이터를 표시합니다.
  • 반응형 그리드 레이아웃:

    • Tailwind CSS 클래스를 사용하여 반응형 그리드 레이아웃을 구현합니다.
    • grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 클래스를 통해 화면 크기에 따라 열 수가 변경됩니다.
    • 리스트 보기는 grid-cols-1로 설정하여 모든 항목을 세로로 나열합니다.
  • 스타일링 및 애니메이션:

    • 각 항목은 bg-white, rounded-lg, shadow-md 등의 클래스로 스타일링되어 카드 형태로 표시됩니다.
    • transition-transform duration-300 hover:scale-105 클래스를 통해 호버 시 약간의 확대 효과를 줍니다.
  • 접근성:

    • aria-label 속성을 사용하여 버튼의 목적을 명확히 전달합니다.
    • 검색 입력 필드에 aria-label을 추가하여 스크린 리더 사용자에게 필드의 용도를 설명합니다.

profile
개발자가 되고 싶은 정치학도생의 기술 블로그

0개의 댓글