IT

제가 직접 Qwen으로 Vite+React 웹앱 만든 후기

메타인지 월드 2025. 10. 5. 15:38
반응형

Vite+React+Tailwind CSS를 사용한 현대적인 웹 애플리케이션 개발기

개요

최근 웹 개발 생태계는 빠르게 변화하고 있으며, 개발자들은 더 빠르고 효율적인 도구들을 찾고 있습니다. 이 글에서는 Vite, React, Tailwind CSS를 조합하여 현대적인 웹 애플리케이션을 처음부터 구축한 경험을 공유합니다. 특히, 메뉴가 많은 네비게이션 바를 드롭다운으로 구현하고 사용자 경험을 향상시키는 과정에서 겪은 시행착오와 해결 방법을 중심으로 소개합니다.

기술 스택 선택

Vite

Vite는 에반 유님께서 개발한 차세대 프론트엔드 빌드 도구입니다. Webpack과 같은 전통적인 번들러와 달리, Vite는 개발 서버 구동 속도와 핫 모듈 리플레이스먼트(HMR) 속도가 매우 빠릅니다. ES 모듈을 기반으로 작동하여 초기 로딩 시간을 크게 줄여줍니다.

React

React는 사용자 인터페이스를 구축하기 위한 JavaScript 라이브러리로, 구성 요소 기반 아키텍처를 통해 코드의 재사용성과 유지 보수성을 향상시킵니다. hooks API를 통해 함수형 컴포넌트에서도 상태 관리와 사이드 이펙트를 효과적으로 처리할 수 있습니다.

Tailwind CSS

Tailwind CSS는 유틸리티 우선 CSS 프레임워크로, 프로젝트 전반에 걸쳐 일관된 디자인 시스템을 구축할 수 있도록 도와줍니다. 전통적인 CSS 프레임워크와 달리, Tailwind는 재사용 가능한 유틸리티 클래스를 제공하여 스타일링을 매우 직관적으로 만들어 줍니다.

초기 프로젝트 설정

Vite를 사용한 프로젝트 생성

npm create vite@latest my-project -- --template react
cd my-project
npm install

Tailwind CSS 설정

npm install -D tailwindcss postcss autoprefixer
npx tailwindcss init -p

tailwind.config.js 파일에 다음 내용을 추가합니다:

module.exports = {
  content: [
    "./index.html",
    "./src/**/*.{js,ts,jsx,tsx}",
  ],
  theme: {
    extend: {},
  },
  plugins: [],
}

네비게이션 바 구현

기본 네비게이션 구조

React Router DOM을 사용하여 클라이언트 측 라우팅을 구현했습니다. 초기에는 모든 메뉴 항목을 수평으로 배치하여 데스크탑에서만 잘 동작하는 구조를 만들었습니다.

import React from 'react';
import { Link } from 'react-router-dom';

function Navbar() {
  return (
    <nav className="bg-blue-600 text-white shadow-lg">
      <div className="container mx-auto px-4">
        <div className="flex justify-between items-center h-16">
          <div className="flex items-center">
            <span className="text-xl font-bold">Vite+React+Tailwind</span>
          </div>
          <div className="hidden md:flex space-x-8">
            <Link to="/" className="hover:bg-blue-700 px-3 py-2 rounded">Home</Link>
            <Link to="/navigation" className="hover:bg-blue-700 px-3 py-2 rounded">Navigation</Link>
            <Link to="/page-composition" className="hover:bg-blue-700 px-3 py-2 rounded">Page Composition</Link>
            {/* 더 많은 메뉴 항목들 */}
          </div>
        </div>
      </div>
    </nav>
  );
}

export default Navbar;

드롭다운 네비게이션 구현

문제 인식

기존 방식은 메뉴 항목이 많아질수록 네비게이션 바가 화면을 벗어나는 문제가 발생했습니다. 특히 모바일 환경에서는 사용자 경험에 부정적인 영향을 미쳤습니다.

솔루션 설계

  1. 관련 메뉴 항목들을 그룹화
  2. 그룹별 드롭다운을 클릭으로 토글
  3. 드롭다운 외부 클릭 시 닫히도록 구현
  4. 모바일 환경에서도 최적화된 UX 제공

드롭다운 네비게이션 구현 코드

import React, { useState, useEffect } from 'react';
import { Link } from 'react-router-dom';

function Navbar() {
  const [isMenuOpen, setIsMenuOpen] = useState(false);
  const [activeDropdown, setActiveDropdown] = useState(null);

  // 외부 클릭 시 드롭다운 닫기
  useEffect(() => {
    const handleClickOutside = (event) => {
      if (activeDropdown) {
        const dropdowns = document.querySelectorAll('.dropdown-container');
        const isClickInside = Array.from(dropdowns).some(dropdown => 
          dropdown.contains(event.target)
        );
        if (!isClickInside) {
          setActiveDropdown(null);
        }
      }
    };

    document.addEventListener('click', handleClickOutside);
    return () => {
      document.removeEventListener('click', handleClickOutside);
    };
  }, [activeDropdown]);

  // 메뉴 그룹
  const menuGroups = {
    '시작하기': [
      { name: '프로젝트 설정', path: '/project-setup' },
    ],
    '기초 구성': [
      { name: '내비게이션', path: '/navigation' },
      { name: '컴포넌트 설계', path: '/components' },
      { name: '페이지 구성', path: '/page-composition' },
    ],
    '기능 구현': [
      { name: '폼 처리 및 검증', path: '/form-handling' },
      { name: '성능 최적화', path: '/performance-optimization' },
      { name: '기능 구현', path: '/feature-implementation' },
    ],
    '스타일링': [
      { name: '스타일링 및 테마', path: '/styling-theming' },
    ],
    '문서': [
      { name: '트러블슈팅', path: '/troubleshooting' },
    ]
  };

  return (
    <nav className="bg-blue-600 text-white shadow-lg fixed top-0 left-0 right-0 z-50">
      <div className="container mx-auto px-4">
        <div className="flex justify-between items-center h-16">
          <div className="flex items-center">
            <Link to="/" className="text-xl font-bold">Vite+React+Tailwind</Link>
          </div>

          {/* 데스크탑 메뉴 - 드롭다운 포함 */}
          <div className="hidden md:flex items-center space-x-1">
            <Link to="/" className="hover:bg-blue-700 px-3 py-2 rounded">Home</Link>

            {Object.entries(menuGroups).map(([groupName, items]) => (
              <div key={groupName} className="relative dropdown-container">
                <button
                  className={`px-3 py-2 rounded flex items-center ${activeDropdown === groupName ? 'bg-blue-700' : 'hover:bg-blue-700'}`}
                  onClick={(e) => {
                    e.stopPropagation();
                    setActiveDropdown(activeDropdown === groupName ? null : groupName);
                  }}
                >
                  {groupName}
                  <svg 
                    className={`ml-1 w-4 h-4 transition-transform duration-200 ${activeDropdown === groupName ? 'rotate-180' : ''}`} 
                    fill="none" 
                    stroke="currentColor" 
                    viewBox="0 0 24 24"
                  >
                    <path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M19 9l-7 7-7-7" />
                  </svg>
                </button>

                {activeDropdown === groupName && (
                  <div className="absolute left-0 mt-2 w-48 bg-blue-700 rounded-md shadow-lg py-1 z-50">
                    {items.map((item, index) => (
                      <Link
                        key={index}
                        to={item.path}
                        className="block px-4 py-2 text-sm hover:bg-blue-800"
                        onClick={() => setActiveDropdown(null)}
                      >
                        {item.name}
                      </Link>
                    ))}
                  </div>
                )}
              </div>
            ))}
          </div>

          {/* 모바일 메뉴 버튼 */}
          <div className="md:hidden flex items-center">
            <button 
              onClick={() => setIsMenuOpen(!isMenuOpen)}
              className="text-white focus:outline-none"
            >
              <svg className="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
                {isMenuOpen ? (
                  <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
                ) : (
                  <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 6h16M4 12h16M4 18h16" />
                )}
              </svg>
            </button>
          </div>
        </div>

        {/* 모바일 메뉴 */}
        {isMenuOpen && (
          <div className="md:hidden">
            <div className="px-2 pt-2 pb-3 space-y-1 bg-blue-700 rounded-lg mt-2 absolute left-4 right-4">
              <Link to="/" className="block px-3 py-2 rounded hover:bg-blue-800" onClick={() => setIsMenuOpen(false)}>
                Home
              </Link>

              {Object.entries(menuGroups).map(([groupName, items]) => (
                <div key={groupName}>
                  <div className="px-3 py-2 font-medium border-b border-blue-600">
                    {groupName}
                  </div>
                  {items.map((item, index) => (
                    <Link
                      key={index}
                      to={item.path}
                      className="block px-6 py-2 rounded hover:bg-blue-800"
                      onClick={() => setIsMenuOpen(false)}
                    >
                      {item.name}
                    </Link>
                  ))}
                </div>
              ))}
            </div>
          </div>
        )}
      </div>
    </nav>
  );
}

export default Navbar;

시행착오와 해결 과정

1. 드롭다운 이벤트 처리 문제

처음에는 onMouseEnteronMouseLeave 이벤트를 사용하여 드롭다운을 제어했습니다. 하지만 이 방식은 드롭다운 영역을 벗어나는 순간 메뉴가 닫히는 문제가 발생했습니다.

  • 문제: onMouseEnter로 드롭다운을 열고, onMouseLeave로 드롭다운을 닫는 구조
  • 결과: 드롭다운 영역 밖으로 마우스가 이동하면 드롭다운이 즉시 사라짐
  • 해결: click 이벤트를 사용하여 드롭다운 열기/닫기 토글, click outside 기능 추가

2. 템플릿 리터럴 문법 오류

JSX 코드 예제 내에서 템플릿 리터럴 문법(${})을 사용하면서 빌드 오류가 발생했습니다.

  • 문제: className 속성에 템플릿 리터럴 문법 사용
  • 결과: "ERROR: Expected "}" but found "w"" 메시지 발생
  • 해결: 코드 예제에서는 일반 문자열로 표현, 템플릿 리터럴 표현 필요 시 이스케이프 처리

성능 최적화

React.memo를 사용한 렌더링 최적화

불필요한 리렌더링을 방지하기 위해 React.memo를 사용했습니다.

코드 분할

React.lazy와 Suspense를 사용하여 라우트 수준에서 코드 분할을 구현하여 초기 로딩 속도를 개선했습니다.

배포 자동화

GitHub Actions를 사용하여 main 브랜치에 푸시할 때 자동으로 GitHub Pages에 배포되도록 설정했습니다. 이로 인해 코드 변경 후 수동 배포 없이도 즉시 업데이트된 내용을 확인할 수 있습니다.

마무리

이 프로젝트를 통해 Vite의 빠른 개발 환경, React의 구성 요소 기반 구조, Tailwind CSS의 유틸리티 우선 스타일링의 장점을 경험할 수 있었습니다. 특히 사용자 경험을 고려한 드롭다운 네비게이션 설계를 통해 복잡한 메뉴 구조를 깔끔하게 정리할 수 있었고, 이 과정에서 겪은 문제 해결 경험은 향후 유사한 문제에 대처하는 데 큰 도움이 될 것입니다.

이러한 기술 스택은 현대적인 웹 애플리케이션 개발에 있어 매우 효율적이고 생산적인 조합으로 입증되었습니다. 처음부터 설정한 개발 환경은 빠른 빌드 시간과 훌륭한 개발자 경험을 제공하며, 사용자에게도 빠르고 반응적인 인터페이스를 제공할 수 있어 만족스러운 결과를 얻을 수 있었습니다.

 

부록

 

GitHub - jeonck/react-from-scratch

Contribute to jeonck/react-from-scratch development by creating an account on GitHub.

github.com

 

 

Vite + React + Tailwind App

 

jeonck.github.io

반응형