← 목록으로

챕터 17: Tailwind로 만드는 인터랙티브 효과

서론

지난 챕터에서 CSS 애니메이션의 기초를 배웠다면, 이번에는 Tailwind CSS를 활용하여 더욱 쉽고 빠르게 인터랙티브한 효과를 만드는 방법을 알아보겠습니다. Tailwind CSS는 애니메이션과 트랜지션을 위한 풍부한 유틸리티 클래스를 제공하여, 복잡한 CSS를 작성하지 않고도 멋진 효과를 구현할 수 있게 해줍니다.

hover, focus, active 같은 상태 변경자와 animate 유틸리티를 조합하면, 단 몇 개의 클래스만으로도 전문적인 수준의 인터랙티브 UI를 만들 수 있습니다. 이번 챕터에서는 Tailwind CSS의 애니메이션 시스템을 완벽하게 마스터해보겠습니다.

본론

Tailwind의 기본 애니메이션 유틸리티

Tailwind CSS는 자주 사용되는 애니메이션을 미리 정의해두었습니다:

'use client'

import { useState } from 'react'

export default function TailwindAnimations() {
  const [showElements, setShowElements] = useState(true)
  
  return (
    <div className="p-8">
      <h2 className="text-2xl font-bold mb-6">Tailwind 기본 애니메이션</h2>
      
      {/* 기본 애니메이션 클래스 */}
      <div className="grid grid-cols-4 gap-6 mb-8">
        {/* Spin */}
        <div className="text-center">
          <div className="animate-spin h-16 w-16 border-4 border-blue-500 border-t-transparent rounded-full mx-auto"></div>
          <p className="mt-2 text-sm font-medium">animate-spin</p>
          <p className="text-xs text-gray-600">로딩 스피너</p>
        </div>
        
        {/* Ping */}
        <div className="text-center">
          <div className="relative h-16 w-16 mx-auto">
            <span className="animate-ping absolute inline-flex h-full w-full rounded-full bg-purple-400 opacity-75"></span>
            <span className="relative inline-flex rounded-full h-16 w-16 bg-purple-500"></span>
          </div>
          <p className="mt-2 text-sm font-medium">animate-ping</p>
          <p className="text-xs text-gray-600">알림 효과</p>
        </div>
        
        {/* Pulse */}
        <div className="text-center">
          <div className="animate-pulse h-16 w-16 bg-green-500 rounded-lg mx-auto"></div>
          <p className="mt-2 text-sm font-medium">animate-pulse</p>
          <p className="text-xs text-gray-600">로딩 스켈레톤</p>
        </div>
        
        {/* Bounce */}
        <div className="text-center">
          <div className="animate-bounce h-16 w-16 bg-red-500 rounded-full mx-auto flex items-center justify-center text-white">
            ↓
          </div>
          <p className="mt-2 text-sm font-medium">animate-bounce</p>
          <p className="text-xs text-gray-600">주목 효과</p>
        </div>
      </div>
      
      {/* 스켈레톤 로딩 예제 */}
      <div className="mb-8">
        <h3 className="font-bold mb-3">스켈레톤 로딩 UI</h3>
        <div className="border rounded-lg p-4 space-y-3">
          <div className="animate-pulse">
            <div className="h-4 bg-gray-300 rounded w-3/4 mb-2"></div>
            <div className="h-4 bg-gray-300 rounded w-1/2 mb-4"></div>
            <div className="h-32 bg-gray-300 rounded mb-2"></div>
            <div className="flex space-x-2">
              <div className="h-8 bg-gray-300 rounded w-20"></div>
              <div className="h-8 bg-gray-300 rounded w-20"></div>
            </div>
          </div>
        </div>
      </div>
      
      {/* 애니메이션 제어 */}
      <div>
        <h3 className="font-bold mb-3">애니메이션 제어</h3>
        <button
          onClick={() => setShowElements(!showElements)}
          className="bg-blue-500 text-white px-4 py-2 rounded mb-4"
        >
          {showElements ? '애니메이션 중지' : '애니메이션 시작'}
        </button>
        
        <div className="flex gap-4">
          <div className={`h-12 w-12 bg-orange-500 rounded ${showElements ? 'animate-spin' : ''}`}></div>
          <div className={`h-12 w-12 bg-pink-500 rounded ${showElements ? 'animate-pulse' : ''}`}></div>
          <div className={`h-12 w-12 bg-indigo-500 rounded ${showElements ? 'animate-bounce' : ''}`}></div>
        </div>
      </div>
    </div>
  )
}

Hover, Focus, Active 효과

Tailwind의 상태 변경자를 활용한 인터랙티브 효과:

'use client'

import { useState } from 'react'

export default function InteractiveStates() {
  const [activeCard, setActiveCard] = useState(null)
  
  return (
    <div className="p-8">
      <h2 className="text-2xl font-bold mb-6">인터랙티브 상태 효과</h2>
      
      {/* Hover 효과 모음 */}
      <div className="mb-8">
        <h3 className="font-bold mb-3">Hover 효과</h3>
        <div className="grid grid-cols-3 gap-4">
          {/* 크기 변화 */}
          <div className="bg-blue-500 text-white p-6 rounded-lg text-center hover:scale-110 transition-transform duration-300 cursor-pointer">
            <p className="font-bold">Scale</p>
            <p className="text-sm">hover:scale-110</p>
          </div>
          
          {/* 회전 */}
          <div className="bg-green-500 text-white p-6 rounded-lg text-center hover:rotate-6 transition-transform duration-300 cursor-pointer">
            <p className="font-bold">Rotate</p>
            <p className="text-sm">hover:rotate-6</p>
          </div>
          
          {/* 그림자 */}
          <div className="bg-purple-500 text-white p-6 rounded-lg text-center hover:shadow-2xl transition-shadow duration-300 cursor-pointer">
            <p className="font-bold">Shadow</p>
            <p className="text-sm">hover:shadow-2xl</p>
          </div>
          
          {/* 투명도 */}
          <div className="bg-red-500 text-white p-6 rounded-lg text-center hover:opacity-75 transition-opacity duration-300 cursor-pointer">
            <p className="font-bold">Opacity</p>
            <p className="text-sm">hover:opacity-75</p>
          </div>
          
          {/* 이동 */}
          <div className="bg-yellow-500 text-white p-6 rounded-lg text-center hover:-translate-y-2 transition-transform duration-300 cursor-pointer">
            <p className="font-bold">Translate</p>
            <p className="text-sm">hover:-translate-y-2</p>
          </div>
          
          {/* 색상 변화 */}
          <div className="bg-gray-500 hover:bg-gradient-to-r hover:from-pink-500 hover:to-violet-500 text-white p-6 rounded-lg text-center transition-all duration-300 cursor-pointer">
            <p className="font-bold">Gradient</p>
            <p className="text-sm">hover:bg-gradient</p>
          </div>
        </div>
      </div>
      
      {/* Focus 효과 */}
      <div className="mb-8">
        <h3 className="font-bold mb-3">Focus 효과</h3>
        <div className="space-y-3">
          <input
            type="text"
            placeholder="Ring 효과"
            className="w-full px-4 py-2 border rounded-lg focus:ring-4 focus:ring-blue-300 focus:border-blue-500 focus:outline-none transition-all"
          />
          
          <input
            type="text"
            placeholder="Scale + Ring"
            className="w-full px-4 py-2 border rounded-lg focus:scale-105 focus:ring-2 focus:ring-purple-300 focus:outline-none transition-all"
          />
          
          <textarea
            placeholder="Shadow 효과"
            className="w-full px-4 py-2 border rounded-lg focus:shadow-lg focus:border-green-500 focus:outline-none transition-all"
            rows="3"
          />
        </div>
      </div>
      
      {/* Active 효과 */}
      <div className="mb-8">
        <h3 className="font-bold mb-3">Active (클릭) 효과</h3>
        <div className="flex gap-4">
          <button className="bg-blue-500 text-white px-6 py-3 rounded-lg active:scale-95 transition-transform">
            active:scale-95
          </button>
          
          <button className="bg-green-500 text-white px-6 py-3 rounded-lg active:bg-green-700 transition-colors">
            active:bg-green-700
          </button>
          
          <button className="bg-purple-500 text-white px-6 py-3 rounded-lg active:ring-4 active:ring-purple-300 transition-all">
            active:ring-4
          </button>
        </div>
      </div>
      
      {/* Group Hover */}
      <div>
        <h3 className="font-bold mb-3">Group Hover 효과</h3>
        <div className="grid grid-cols-3 gap-4">
          {[1, 2, 3].map((num) => (
            <div
              key={num}
              className="group bg-white border-2 border-gray-200 rounded-lg p-4 hover:border-blue-500 hover:shadow-xl transition-all duration-300 cursor-pointer"
              onMouseEnter={() => setActiveCard(num)}
              onMouseLeave={() => setActiveCard(null)}
            >
              <div className="h-32 bg-gray-200 rounded mb-3 group-hover:bg-gradient-to-r group-hover:from-blue-400 group-hover:to-purple-500 transition-all duration-300"></div>
              <h4 className="font-bold text-gray-700 group-hover:text-blue-600 transition-colors">
                카드 제목 {num}
              </h4>
              <p className="text-sm text-gray-500 group-hover:text-gray-700 transition-colors mt-1">
                마우스를 올리면 전체가 변합니다
              </p>
              <button className="mt-3 text-blue-500 opacity-0 group-hover:opacity-100 transition-opacity duration-300">
                더 보기 →
              </button>
            </div>
          ))}
        </div>
      </div>
    </div>
  )
}

Transition 유틸리티 활용

Tailwind의 transition 클래스로 부드러운 효과 만들기:

'use client'

import { useState } from 'react'

export default function TransitionUtilities() {
  const [isOpen, setIsOpen] = useState(false)
  const [activeTab, setActiveTab] = useState(1)
  
  return (
    <div className="p-8">
      <h2 className="text-2xl font-bold mb-6">Transition 유틸리티</h2>
      
      {/* Duration 옵션 */}
      <div className="mb-8">
        <h3 className="font-bold mb-3">Duration 속도 비교</h3>
        <div className="space-y-2">
          <div className="bg-blue-500 text-white p-3 rounded hover:bg-blue-700 transition-colors duration-75">
            duration-75 (75ms)
          </div>
          <div className="bg-blue-500 text-white p-3 rounded hover:bg-blue-700 transition-colors duration-300">
            duration-300 (300ms)
          </div>
          <div className="bg-blue-500 text-white p-3 rounded hover:bg-blue-700 transition-colors duration-500">
            duration-500 (500ms)
          </div>
          <div className="bg-blue-500 text-white p-3 rounded hover:bg-blue-700 transition-colors duration-1000">
            duration-1000 (1000ms)
          </div>
        </div>
      </div>
      
      {/* Timing Function */}
      <div className="mb-8">
        <h3 className="font-bold mb-3">Timing Functions</h3>
        <div className="space-y-2">
          <div className="bg-green-500 text-white p-3 rounded hover:translate-x-24 transition-transform duration-1000 ease-linear">
            ease-linear
          </div>
          <div className="bg-green-500 text-white p-3 rounded hover:translate-x-24 transition-transform duration-1000 ease-in">
            ease-in
          </div>
          <div className="bg-green-500 text-white p-3 rounded hover:translate-x-24 transition-transform duration-1000 ease-out">
            ease-out
          </div>
          <div className="bg-green-500 text-white p-3 rounded hover:translate-x-24 transition-transform duration-1000 ease-in-out">
            ease-in-out
          </div>
        </div>
      </div>
      
      {/* Delay 효과 */}
      <div className="mb-8">
        <h3 className="font-bold mb-3">Delay 효과</h3>
        <div className="group flex gap-2">
          {[0, 75, 150, 300, 500, 700].map((delay, index) => (
            <div
              key={index}
              className={`w-12 h-12 bg-purple-500 rounded group-hover:scale-125 group-hover:rotate-45 transition-all duration-500 delay-${delay}`}
            />
          ))}
        </div>
        <p className="text-sm text-gray-600 mt-2">마우스를 올려보세요 (순차적 애니메이션)</p>
      </div>
      
      {/* 실용적인 예제: 아코디언 */}
      <div className="mb-8">
        <h3 className="font-bold mb-3">아코디언 메뉴</h3>
        <div className="border rounded-lg overflow-hidden">
          <button
            onClick={() => setIsOpen(!isOpen)}
            className="w-full px-4 py-3 bg-gray-50 hover:bg-gray-100 flex justify-between items-center transition-colors"
          >
            <span className="font-medium">클릭하여 열기/닫기</span>
            <span className={`transform transition-transform duration-300 ${isOpen ? 'rotate-180' : ''}`}>
              ▼
            </span>
          </button>
          
          <div className={`transition-all duration-500 ${isOpen ? 'max-h-40 opacity-100' : 'max-h-0 opacity-0'} overflow-hidden`}>
            <div className="p-4 bg-white">
              <p className="text-gray-600">
                이것은 아코디언 콘텐츠입니다. transition-all과 max-height를 활용하여
                부드러운 열림/닫힘 효과를 구현했습니다.
              </p>
            </div>
          </div>
        </div>
      </div>
      
      {/* 탭 전환 효과 */}
      <div>
        <h3 className="font-bold mb-3">탭 전환 효과</h3>
        <div className="border rounded-lg overflow-hidden">
          <div className="flex bg-gray-100">
            {[1, 2, 3].map((tab) => (
              <button
                key={tab}
                onClick={() => setActiveTab(tab)}
                className={`flex-1 px-4 py-3 transition-all duration-300 ${
                  activeTab === tab
                    ? 'bg-white text-blue-600 shadow-sm transform scale-105'
                    : 'hover:bg-gray-200'
                }`}
              >
                탭 {tab}
              </button>
            ))}
          </div>
          
          <div className="p-6">
            <div className="transition-all duration-300 transform">
              {activeTab === 1 && (
                <div className="animate-fade-in">
                  <h4 className="font-bold mb-2">첫 번째 탭</h4>
                  <p className="text-gray-600">첫 번째 탭의 내용입니다.</p>
                </div>
              )}
              {activeTab === 2 && (
                <div className="animate-fade-in">
                  <h4 className="font-bold mb-2">두 번째 탭</h4>
                  <p className="text-gray-600">두 번째 탭의 내용입니다.</p>
                </div>
              )}
              {activeTab === 3 && (
                <div className="animate-fade-in">
                  <h4 className="font-bold mb-2">세 번째 탭</h4>
                  <p className="text-gray-600">세 번째 탭의 내용입니다.</p>
                </div>
              )}
            </div>
          </div>
        </div>
      </div>
    </div>
  )
}

커스텀 애니메이션 만들기

Tailwind 설정을 확장하여 커스텀 애니메이션 추가:

'use client'

import { useState } from 'react'

export default function CustomAnimations() {
  const [notifications, setNotifications] = useState([])
  
  const addNotification = (type) => {
    const id = Date.now()
    const newNotification = { id, type, message: `${type} 알림입니다!` }
    setNotifications(prev => [...prev, newNotification])
    
    setTimeout(() => {
      setNotifications(prev => prev.filter(n => n.id !== id))
    }, 3000)
  }
  
  return (
    <div className="p-8">
      <h2 className="text-2xl font-bold mb-6">커스텀 애니메이션</h2>
      
      <style jsx>{`
        @keyframes slideInRight {
          from {
            transform: translateX(100%);
            opacity: 0;
          }
          to {
            transform: translateX(0);
            opacity: 1;
          }
        }
        
        @keyframes slideOutRight {
          from {
            transform: translateX(0);
            opacity: 1;
          }
          to {
            transform: translateX(100%);
            opacity: 0;
          }
        }
        
        @keyframes wiggle {
          0%, 100% { transform: rotate(-3deg); }
          50% { transform: rotate(3deg); }
        }
        
        @keyframes fadeInUp {
          from {
            opacity: 0;
            transform: translateY(20px);
          }
          to {
            opacity: 1;
            transform: translateY(0);
          }
        }
        
        .animate-slideInRight {
          animation: slideInRight 0.5s ease-out;
        }
        
        .animate-slideOutRight {
          animation: slideOutRight 0.5s ease-out forwards;
        }
        
        .animate-wiggle {
          animation: wiggle 0.3s ease-in-out infinite;
        }
        
        .animate-fadeInUp {
          animation: fadeInUp 0.6s ease-out;
        }
      `}</style>
      
      {/* 알림 토스트 */}
      <div className="mb-8">
        <h3 className="font-bold mb-3">알림 토스트</h3>
        <div className="flex gap-2 mb-4">
          <button
            onClick={() => addNotification('success')}
            className="bg-green-500 text-white px-4 py-2 rounded"
          >
            성공 알림
          </button>
          <button
            onClick={() => addNotification('error')}
            className="bg-red-500 text-white px-4 py-2 rounded"
          >
            에러 알림
          </button>
          <button
            onClick={() => addNotification('info')}
            className="bg-blue-500 text-white px-4 py-2 rounded"
          >
            정보 알림
          </button>
        </div>
        
        {/* 알림 컨테이너 */}
        <div className="fixed top-4 right-4 space-y-2 z-50">
          {notifications.map((notification) => (
            <div
              key={notification.id}
              className={`animate-slideInRight px-6 py-3 rounded-lg shadow-lg text-white ${
                notification.type === 'success' ? 'bg-green-500' :
                notification.type === 'error' ? 'bg-red-500' :
                'bg-blue-500'
              }`}
            >
              {notification.message}
            </div>
          ))}
        </div>
      </div>
      
      {/* Wiggle 효과 */}
      <div className="mb-8">
        <h3 className="font-bold mb-3">Wiggle 애니메이션</h3>
        <div className="flex gap-4">
          <button className="bg-yellow-500 text-white px-6 py-3 rounded-lg hover:animate-wiggle">
            마우스 올리면 흔들림
          </button>
          
          <div className="bg-red-500 text-white px-6 py-3 rounded-lg animate-wiggle">
            계속 흔들림
          </div>
        </div>
      </div>
      
      {/* Fade In Up */}
      <div className="mb-8">
        <h3 className="font-bold mb-3">Fade In Up 효과</h3>
        <div className="grid grid-cols-3 gap-4">
          {[1, 2, 3].map((item, index) => (
            <div
              key={item}
              className="animate-fadeInUp bg-gradient-to-r from-purple-400 to-pink-400 text-white p-6 rounded-lg"
              style={{ animationDelay: `${index * 0.2}s` }}
            >
              <h4 className="font-bold mb-2">카드 {item}</h4>
              <p className="text-sm">순차적으로 나타납니다</p>
            </div>
          ))}
        </div>
      </div>
    </div>
  )
}

실습: 인터랙티브 대시보드

모든 효과를 종합한 인터랙티브 대시보드:

'use client'

import { useState } from 'react'

export default function InteractiveDashboard() {
  const [selectedMetric, setSelectedMetric] = useState('sales')
  const [sidebarOpen, setSidebarOpen] = useState(true)
  const [darkMode, setDarkMode] = useState(false)
  
  const metrics = [
    { id: 'sales', label: '매출', value: '₩2.4M', change: '+12%', color: 'blue' },
    { id: 'users', label: '사용자', value: '1,482', change: '+5%', color: 'green' },
    { id: 'orders', label: '주문', value: '342', change: '+8%', color: 'purple' },
    { id: 'revenue', label: '수익', value: '₩1.8M', change: '-3%', color: 'red' }
  ]
  
  return (
    <div className={`min-h-screen transition-colors duration-500 ${darkMode ? 'bg-gray-900' : 'bg-gray-50'}`}>
      {/* 헤더 */}
      <header className={`${darkMode ? 'bg-gray-800' : 'bg-white'} shadow-sm transition-colors duration-500`}>
        <div className="flex items-center justify-between p-4">
          <div className="flex items-center gap-4">
            <button
              onClick={() => setSidebarOpen(!sidebarOpen)}
              className={`p-2 rounded-lg hover:bg-gray-100 transition-colors ${darkMode ? 'hover:bg-gray-700 text-white' : ''}`}
            >
              <svg className="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
                <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 6h16M4 12h16M4 18h16" />
              </svg>
            </button>
            <h1 className={`text-xl font-bold ${darkMode ? 'text-white' : 'text-gray-800'}`}>
              대시보드
            </h1>
          </div>
          
          <button
            onClick={() => setDarkMode(!darkMode)}
            className={`p-2 rounded-lg transition-all duration-300 ${
              darkMode 
                ? 'bg-yellow-500 text-gray-900 hover:bg-yellow-400' 
                : 'bg-gray-800 text-white hover:bg-gray-700'
            }`}
          >
            {darkMode ? '☀️' : '🌙'}
          </button>
        </div>
      </header>
      
      <div className="flex">
        {/* 사이드바 */}
        <aside className={`${darkMode ? 'bg-gray-800' : 'bg-white'} h-screen transition-all duration-300 ${
          sidebarOpen ? 'w-64' : 'w-0'
        } overflow-hidden`}>
          <nav className="p-4">
            {['대시보드', '분석', '고객', '설정'].map((item, index) => (
              <button
                key={item}
                className={`w-full text-left px-4 py-3 rounded-lg mb-2 transition-all duration-300 hover:translate-x-2 ${
                  darkMode 
                    ? 'text-gray-300 hover:bg-gray-700 hover:text-white' 
                    : 'text-gray-700 hover:bg-blue-50 hover:text-blue-600'
                }`}
                style={{ animationDelay: `${index * 0.1}s` }}
              >
                {item}
              </button>
            ))}
          </nav>
        </aside>
        
        {/* 메인 콘텐츠 */}
        <main className="flex-1 p-6">
          {/* 메트릭 카드 */}
          <div className="grid grid-cols-4 gap-4 mb-8">
            {metrics.map((metric, index) => (
              <div
                key={metric.id}
                onClick={() => setSelectedMetric(metric.id)}
                className={`${darkMode ? 'bg-gray-800' : 'bg-white'} p-6 rounded-xl cursor-pointer transition-all duration-300 hover:scale-105 hover:shadow-xl ${
                  selectedMetric === metric.id ? 'ring-4 ring-blue-400' : ''
                }`}
                style={{ animationDelay: `${index * 0.1}s` }}
              >
                <div className="flex justify-between items-start mb-4">
                  <p className={`text-sm ${darkMode ? 'text-gray-400' : 'text-gray-600'}`}>
                    {metric.label}
                  </p>
                  <span className={`text-xs px-2 py-1 rounded ${
                    metric.change.startsWith('+') 
                      ? 'bg-green-100 text-green-600' 
                      : 'bg-red-100 text-red-600'
                  }`}>
                    {metric.change}
                  </span>
                </div>
                <p className={`text-2xl font-bold ${darkMode ? 'text-white' : 'text-gray-800'}`}>
                  {metric.value}
                </p>
                
                {/* 미니 차트 애니메이션 */}
                <div className="mt-4 flex items-end gap-1 h-8">
                  {[40, 60, 45, 70, 65, 80, 75].map((height, i) => (
                    <div
                      key={i}
                      className={`flex-1 bg-${metric.color}-500 rounded-t transition-all duration-500 hover:opacity-80`}
                      style={{ 
                        height: `${height}%`,
                        animationDelay: `${i * 0.05}s`
                      }}
                    />
                  ))}
                </div>
              </div>
            ))}
          </div>
          
          {/* 액티비티 피드 */}
          <div className={`${darkMode ? 'bg-gray-800' : 'bg-white'} rounded-xl p-6`}>
            <h2 className={`text-lg font-bold mb-4 ${darkMode ? 'text-white' : 'text-gray-800'}`}>
              최근 활동
            </h2>
            <div className="space-y-3">
              {['새로운 주문 접수', '고객 문의 응답', '재고 업데이트', '보고서 생성'].map((activity, index) => (
                <div
                  key={index}
                  className={`flex items-center gap-3 p-3 rounded-lg transition-all duration-300 hover:translate-x-2 ${
                    darkMode 
                      ? 'hover:bg-gray-700' 
                      : 'hover:bg-gray-50'
                  }`}
                  style={{ animationDelay: `${index * 0.1}s` }}
                >
                  <div className={`w-2 h-2 rounded-full animate-pulse ${
                    index === 0 ? 'bg-green-500' : 'bg-gray-400'
                  }`} />
                  <p className={darkMode ? 'text-gray-300' : 'text-gray-700'}>
                    {activity}
                  </p>
                  <span className={`ml-auto text-xs ${darkMode ? 'text-gray-500' : 'text-gray-400'}`}>
                    {index === 0 ? '방금 전' : `${index * 5}분 전`}
                  </span>
                </div>
              ))}
            </div>
          </div>
        </main>
      </div>
    </div>
  )
}

결론

Tailwind CSS의 애니메이션 유틸리티를 활용하면 복잡한 CSS를 작성하지 않고도 전문적인 수준의 인터랙티브 효과를 구현할 수 있습니다. 이번 챕터에서 우리는 기본 애니메이션 클래스, hover/focus/active 상태 효과, transition 유틸리티, 그리고 커스텀 애니메이션까지 다양한 기법을 배웠습니다.

Tailwind로 인터랙티브 효과를 만들 때의 핵심:

  1. 유틸리티 우선: 미리 정의된 클래스를 조합하여 빠르게 구현
  2. 일관성: 프로젝트 전체에서 동일한 duration과 easing 사용
  3. 반응성: hover, focus 등 사용자 상호작용에 즉각 반응
  4. 성능: transform과 opacity 위주로 애니메이션 구성
  5. 접근성: prefers-reduced-motion 고려

다음 챕터에서는 라우팅의 개념을 이해하고, Next.js의 강력한 파일 기반 라우팅 시스템을 배워보겠습니다!