본문 바로가기
React

[React] Tailwind CSS 라이브러리 사용해 페이지네이션 구현하기

by teamnova 2025. 7. 24.
728x90

 

안녕하세요 오늘은 Tailwind CSS 라이브러리를 사용해 페이지네이션을 구현해보도록 하겠습니다. 

Tailwind CSS 라이브러리  "유틸리티-퍼스트(utility-first)" CSS 프레임워크" 입니다. 

여기서 "라이브러리"라는 표현도 쓰이지만, 정확히는 CSS 프레임워크라고 부르는 것이 더 적절합니다.

 

 

- 기존 CSS 프레임워크 (예: Bootstrap)와의 차이점

 

  • Bootstrap: 미리 정의된 컴포넌트(버튼, 카드, 내비게이션 바 등)를 제공하고, 해당 컴포넌트에 btn, card, navbar와 같은 클래스를 부여하여 사용합니다. 디자인이 정해져 있어서 빠르게 만들 수 있지만, 커스터마이징하려면 복잡한 CSS 오버라이딩이 필요할 때가 많습니다.
  • Tailwind CSS: btn이나 card 같은 컴포넌트 단위의 클래스를 제공하지 않습니다. 대신, flex, pt-4 (패딩 탑 16px), text-center, bg-blue-500 (파란색 배경)처럼 아주 작은 단위의 **"유틸리티 클래스"**들을 제공합니다. 이 유틸리티 클래스들은 각각 특정 CSS 속성(예: display: flex;, padding-top: 1rem;, text-align: center;, background-color: #3b82f6;)을 담당합니다.

 

 

- 주요 특징 및 장점

 

  • 극강의 커스터마이징 유연성: 미리 정의된 디자인에 얽매이지 않고, 원하는 대로 모든 요소를 조합하여 유니크한 디자인을 만들 수 있습니다. 디자인 시스템을 구축하기에도 용이합니다.
  • 빠른 개발 속도: HTML 파일에서 CSS를 왔다 갔다 할 필요 없이, 마크업 내에서 바로 스타일을 적용할 수 있어 개발 속도가 매우 빠릅니다. "CSS를 작성하기 위해 HTML 파일을 떠날 필요가 없다"는 것이 핵심 철학입니다.
  • 반응형 디자인: md:, lg:와 같은 접두사를 사용하여 쉽게 반응형 디자인을 적용할 수 있습니다. 예를 들어 md:flex는 중간 화면 크기 이상에서만 display: flex;를 적용합니다.
  • 작은 최종 CSS 파일 크기: 개발 과정에서는 많은 유틸리티 클래스가 존재하지만, 빌드 시에는 사용되지 않는 CSS를 자동으로 제거(Purge)해 주기 때문에 최종 결과물의 CSS 파일 크기가 매우 작아집니다. 이는 웹 성능에 긍정적인 영향을 줍니다.
  • 컴포넌트 기반 프레임워크와의 시너지: React, Vue, Angular 등 컴포넌트 기반의 프레임워크와 함께 사용할 때 그 진가가 발휘됩니다. 컴포넌트 안에서 해당 컴포넌트의 스타일을 모두 관리할 수 있어 코드가 깔끔해집니다.

 

 

 

- 페이지 네이션 

  • 현재 표시되는 게시물 정보 (Showing X-Y of Z posts)
<div className="text-sm text-gray-600">
  Showing {startIndex + 1}-{Math.min(endIndex, blogPosts.length)} of {blogPosts.length} posts
</div>

 

  • '이전' 및 '다음' 버튼
    • 페이지를 앞뒤로 이동할 수 있는 "Previous (이전)" 와 "Next (다음)" 버튼을 구현한 것입니다.
    • onClick: 버튼이 클릭될 때 실행될 함수를 정의합니다.
    • Previous 버튼: setCurrentPage(Math.max(1, currentPage - 1))
    • Next 버튼: setCurrentPage(Math.min(totalPages, currentPage + 1))
<button
  onClick={() => setCurrentPage(Math.max(1, currentPage - 1))}
  disabled={currentPage === 1}
  className="px-4 py-2 text-sm font-medium text-gray-500 bg-white border border-gray-300 rounded-md hover:bg-gray-50 disabled:opacity-50 disabled:cursor-not-allowed"
>
  Previous
</button>

<button
  onClick={() => setCurrentPage(Math.min(totalPages, currentPage + 1))}
  disabled={currentPage === totalPages}
  className="px-4 py-2 text-sm font-medium text-gray-500 bg-white border border-gray-300 rounded-md hover:bg-gray-50 disabled:opacity-50 disabled:cursor-not-allowed"
>
  Next
</button>

 

  • 페이지 번호 목록
    • 각 페이지 번호 버튼들을 동적으로 생성하고 렌더링하는 핵심적인 로직입니다. 
    • totalPages 값(예: 3)을 이용하여 길이가 totalPages인 배열을 만듭니다. Array.from의 두 번째 인자는 각 요소에 대해 실행될 맵핑 함수입니다.
    • .map(page => (...)): 생성된 페이지 번호 배열을 순회하면서 각 페이지 번호에 해당하는 <button> 요소를 생성합니다.
<div className="flex items-center space-x-1">
  {Array.from({ length: totalPages }, (_, i) => i + 1).map(page => (
    <button
      key={page}
      onClick={() => setCurrentPage(page)}
      className={`px-3 py-2 text-sm font-medium rounded-md ${
        currentPage === page
          ? 'bg-blue-600 text-white'
          : 'text-gray-500 bg-white border border-gray-300 hover:bg-gray-50'
      }`}
    >
      {page}
    </button>
  ))}
</div>

 

 

 

전체 코드 입니다. 

1. BlogSlider.jsx 

import React, { useState } from 'react'

function BlogSlider() {
  const [currentPage, setCurrentPage] = useState(1)
  const cardsPerPage = 5
  
  const blogPosts = [
    {
      id: 1,
      title: "Getting Started with React Three Fiber",
      excerpt: "Learn how to create stunning 3D web experiences with React Three Fiber and Three.js. This comprehensive guide covers everything from basic setup to advanced animations.",
      image: "https://images.unsplash.com/photo-1555066931-4365d14bab8c?w=400",
      date: "2024-01-15",
      author: "John Doe",
      category: "Web Development",
      readTime: "5 min read"
    },
    {
      id: 2,
      title: "Building Responsive Web Applications",
      excerpt: "Discover the best practices for creating responsive web applications that work seamlessly across all devices and screen sizes.",
      image: "https://images.unsplash.com/photo-1460925895917-afdab827c52f?w=400",
      date: "2024-01-12",
      author: "Jane Smith",
      category: "Frontend",
      readTime: "8 min read"
    },
    {
      id: 3,
      title: "Modern JavaScript ES6+ Features",
      excerpt: "Explore the latest JavaScript features including arrow functions, destructuring, async/await, and more to write cleaner, more efficient code.",
      image: "https://images.unsplash.com/photo-1555066931-4365d14bab8c?w=400",
      date: "2024-01-10",
      author: "Mike Johnson",
      category: "JavaScript",
      readTime: "12 min read"
    },
    {
      id: 4,
      title: "Optimizing React Performance",
      excerpt: "Learn advanced techniques to optimize your React applications for better performance, including memoization, code splitting, and lazy loading.",
      image: "https://images.unsplash.com/photo-1633356122544-f134324a6cee?w=400",
      date: "2024-01-08",
      author: "Sarah Wilson",
      category: "React",
      readTime: "10 min read"
    },
    {
      id: 5,
      title: "CSS Grid vs Flexbox: When to Use What",
      excerpt: "A comprehensive comparison of CSS Grid and Flexbox, with practical examples and guidelines for choosing the right layout method.",
      image: "https://images.unsplash.com/photo-1507003211169-0a1dd7228f2d?w=400",
      date: "2024-01-05",
      author: "Alex Brown",
      category: "CSS",
      readTime: "7 min read"
    },
    {
      id: 6,
      title: "Introduction to TypeScript",
      excerpt: "Get started with TypeScript and learn how to add static typing to your JavaScript projects for better development experience.",
      image: "https://images.unsplash.com/photo-1517077304055-6e89abbf09b0?w=400",
      date: "2024-01-03",
      author: "Chris Davis",
      category: "TypeScript",
      readTime: "15 min read"
    },
    {
      id: 7,
      title: "State Management with Redux Toolkit",
      excerpt: "Learn how to manage application state effectively using Redux Toolkit, the official, opinionated way to write Redux logic.",
      image: "https://images.unsplash.com/photo-1551288049-bebda4e38f71?w=400",
      date: "2024-01-01",
      author: "Lisa Chen",
      category: "Redux",
      readTime: "20 min read"
    },
    {
      id: 8,
      title: "Building REST APIs with Node.js",
      excerpt: "Create robust REST APIs using Node.js, Express, and MongoDB. Learn authentication, validation, and error handling.",
      image: "https://images.unsplash.com/photo-1555066931-4365d14bab8c?w=400",
      date: "2023-12-28",
      author: "Tom Wilson",
      category: "Backend",
      readTime: "18 min read"
    },
    {
      id: 9,
      title: "Testing React Components with Jest",
      excerpt: "Master the art of testing React components using Jest and React Testing Library for reliable, maintainable code.",
      image: "https://images.unsplash.com/photo-1551288049-bebda4e38f71?w=400",
      date: "2023-12-25",
      author: "Emma Taylor",
      category: "Testing",
      readTime: "14 min read"
    },
    {
      id: 10,
      title: "Deploying React Apps to Production",
      excerpt: "Learn the best practices for deploying React applications to production, including optimization, CI/CD, and monitoring.",
      image: "https://images.unsplash.com/photo-1460925895917-afdab827c52f?w=400",
      date: "2023-12-22",
      author: "David Lee",
      category: "Deployment",
      readTime: "11 min read"
    }
  ]

  const totalPages = Math.ceil(blogPosts.length / cardsPerPage)
  const startIndex = (currentPage - 1) * cardsPerPage
  const endIndex = startIndex + cardsPerPage
  const currentPosts = blogPosts.slice(startIndex, endIndex)

  const formatDate = (dateString) => {
    const date = new Date(dateString)
    return date.toLocaleDateString('en-US', {
      year: 'numeric',
      month: 'short',
      day: 'numeric'
    })
  }

  return (
    <section className="py-20 bg-white">
      <div className="max-w-7xl mx-auto px-6">
        {/* 헤더 */}
        <div className="text-center mb-12">
          <h2 className="text-4xl font-bold text-gray-900 mb-4">
            Latest Blog Posts
          </h2>
          <p className="text-xl text-gray-600 max-w-3xl mx-auto">
            Explore my latest thoughts on web development, programming tips, and industry insights
          </p>
        </div>

        {/* 카드 그리드 */}
        <div className="grid md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-5 gap-6 mb-8">
          {currentPosts.map(post => (
            <div
              key={post.id}
              className="bg-white rounded-xl shadow-lg overflow-hidden hover:shadow-xl transition-shadow duration-300 border border-gray-100"
            >
              {/* 이미지 */}
              <div className="relative h-48 overflow-hidden">
                <img
                  src={post.image}
                  alt={post.title}
                  className="w-full h-full object-cover hover:scale-110 transition-transform duration-300"
                />
                <div className="absolute top-4 left-4">
                  <span className="px-2 py-1 bg-blue-600 text-white text-xs rounded-full font-medium">
                    {post.category}
                  </span>
                </div>
              </div>

              {/* 내용 */}
              <div className="p-6">
                <div className="flex items-center text-sm text-gray-500 mb-3">
                  <span>{formatDate(post.date)}</span>
                  <span className="mx-2">•</span>
                  <span>{post.readTime}</span>
                </div>
                
                <h3 className="text-lg font-bold text-gray-900 mb-3 line-clamp-2">
                  {post.title}
                </h3>
                
                <p className="text-gray-600 text-sm mb-4 line-clamp-3">
                  {post.excerpt}
                </p>
                
                <div className="flex items-center justify-between">
                  <span className="text-sm text-gray-500">
                    By {post.author}
                  </span>
                  <button className="text-blue-600 hover:text-blue-700 text-sm font-medium">
                    Read More →
                  </button>
                </div>
              </div>
            </div>
          ))}
        </div>

        {/* 페이지네이션 */}
        <div className="flex items-center justify-between">
          <div className="text-sm text-gray-600">
            Showing {startIndex + 1}-{Math.min(endIndex, blogPosts.length)} of {blogPosts.length} posts
          </div>
          
          <div className="flex items-center space-x-2">
            <button
              onClick={() => setCurrentPage(Math.max(1, currentPage - 1))}
              disabled={currentPage === 1}
              className="px-4 py-2 text-sm font-medium text-gray-500 bg-white border border-gray-300 rounded-md hover:bg-gray-50 disabled:opacity-50 disabled:cursor-not-allowed"
            >
              Previous
            </button>
            
            <div className="flex items-center space-x-1">
              {Array.from({ length: totalPages }, (_, i) => i + 1).map(page => (
                <button
                  key={page}
                  onClick={() => setCurrentPage(page)}
                  className={`px-3 py-2 text-sm font-medium rounded-md ${
                    currentPage === page
                      ? 'bg-blue-600 text-white'
                      : 'text-gray-500 bg-white border border-gray-300 hover:bg-gray-50'
                  }`}
                >
                  {page}
                </button>
              ))}
            </div>
            
            <button
              onClick={() => setCurrentPage(Math.min(totalPages, currentPage + 1))}
              disabled={currentPage === totalPages}
              className="px-4 py-2 text-sm font-medium text-gray-500 bg-white border border-gray-300 rounded-md hover:bg-gray-50 disabled:opacity-50 disabled:cursor-not-allowed"
            >
              Next
            </button>
          </div>
        </div>
      </div>
    </section>
  )
}

export default BlogSlider

 

 

 

2. App.jsx 

import { motion, useScroll } from "motion/react";
import BlogSlider from "./components/BlogSlider";

function App() {
  const { scrollYProgress } = useScroll();

  return (
        {" "}
          <BlogSlider />
      </div>
    </>
  );
}

export default App;

 

 

시연 사진입니다.