본문 바로가기

개발

Gatsby 블로그: Pagination 추가하기

Gatsby에서의 Pagination

Gatsby에서는 빌드 타임에 페이지 번호별로 페이지를 만들어두고, <Link> 컴포넌트를 사용하면 매우 빠르게 화면이 전환되는 페이지네이션을 구현할 수 있다. <Link> 컴포넌트의 특징은 뒤에서 조금 더 자세히 설명할 예정.

Material UI Pagination 컴포넌트

편의를 위해 Material UI를 사용했다. Material UI는 Pagination이라는 컴포넌트를 제공하기 때문에 상당히 편리하게 기능을 구현할 수 있다.

Gatsby에서 Material UI를 사용하기 위해서는 gatsby-plugin-material-ui 플러그인을 설치해야 한다. 적용하는 방법이 조금 번거로운데, 새로운 플러그인(gatsby-plugin-top-layout)을 정의하고 그 플러그인이 gatsby-plugin-material-ui를 사용하도록 gatsby-config.js에 추가해줘야 한다.

 

module.exports = {
  // ...
  plugins: [
    `gatsby-plugin-top-layout`,
    {
      resolve: `gatsby-plugin-material-ui`,
    },
  ],
}

 

gatsby-plugin-top-layout의 코드 자체는 간단한데, TopLayout.js 컴포넌트에서 필요한 스타일들을 전부 import하고 gatsby-browser.js, gatsby-ssr.js에서 컴포넌트를 불러와서 root element를 감싸는 식이다(wrapRootElement). 정확한 코드는 Material UI의 예시(Github Repo)를 참고하면 될 것 같다.

 

//////////////////
//// TopLayout.js
import "@fontsource/roboto";
import "../../src/normalize.css";
import "../../src/style.css";

import React from "react";
import CssBaseline from "@material-ui/core/CssBaseline";
import { ThemeProvider } from "@material-ui/core/styles";
import theme from "../../src/theme";

export default function TopLayout(props) {
  return (
    <ThemeProvider theme={theme}>
      <CssBaseline />
      {props.children}
    </ThemeProvider>
  );
}

//////////////////
// gatsby-browser.js, gatsby-ssr.js
import React from "react";
import TopLayout from "./TopLayout";

export const wrapRootElement = ({ element }) => {
  return <TopLayout>{element}</TopLayout>;
};

구현하기

Pagination을 구현하는 방법은 공식 문서를 기준으로 설명해보려고 한다. 기본적인 아이디어는 매우 간단하다. gatsby-node.js에서 모든 포스트를 불러오고, 페이지당 표시할 포스트 개수를 정해 총 페이지 수를 계산한다. 그리고 이 값들을 기준으로 페이지 번호마다 페이지를 생성한다.

페이지 생성

exports.createPages = async ({ graphql, actions, reporter }) => {
  const { createPage } = actions
  const result = await graphql(
    `
      {
        allMarkdownRemark(
          sort: { fields: [frontmatter___date], order: DESC }
          limit: 1000
        ) {
          nodes {
            fields {
              slug
            }
          }
        }
      }
    `
  )
  // ...

  // Create blog-list pages
  const posts = result.data.allMarkdownRemark.nodes
  const postsPerPage = 6
  const numPages = Math.ceil(posts.length / postsPerPage)
  Array.from({ length: numPages }).forEach((_, i) => {
    createPage({
      path: i === 0 ? `/page/` : `/page/${i + 1}/`,
      component: path.resolve("./src/templates/blog-list-template.js"),
      context: {
        limit: postsPerPage,
        skip: i * postsPerPage,
        numPages,
        currentPage: i + 1,
      },
    })
  })
}

 

posts는 불러온 모든 포스트, postsPerPagenumPages는 각각 페이지당 포스트 갯수와 총 페이지 수를 나타냄을 알 수 있다. createPage 함수로 페이지를 생성할 때 context로 넘겨주는 limitskipblog-list-template.js 템플릿의 page query에서 사용될 값으로, 전체 포스트에서 포스트 몇 개를 건너뛰고 (skip), 몇 개를 불러올지 (limit) 나타낸다. numPagescurrentPage는 page component에서 pagination 기능을 구현할 때 사용할 것이다.

페이지 템플릿 작성

다음과 같이 skip, limit이 포함된 쿼리를 이용하여 일부 포스트를 가져와서 페이지 컴포넌트에서 사용한다.

 

// blog-list-template.js
import { graphql } from "gatsby"
// 컴포넌트 생략

export const blogListQuery = graphql`
  query blogListQuery($skip: Int!, $limit: Int!) {
    allMarkdownRemark(
      sort: { fields: [frontmatter___date], order: DESC }
      limit: $limit
      skip: $skip
    ) {
      nodes {
        fields {
          slug
        }
        frontmatter {
          title
        }
      }
    }
  }
`

 

문서의 설명에는 나와있지 않지만, 페이지 컴포넌트에서 numPagescurrentPage 값을 이용해 다음과 같이 Pagination을 구현할 수 있다.

 

// blog-list-template.js
import React from "react"
import Layout from "../components/layout"

// 추가
import { Pagination, PaginationItem } from "@material-ui/lab"
import { Link } from "gatsby"

const BlogList = ({ data, pageContext }) => {
  // 페이지 쿼리를 통해 불러온 포스트
  const posts = data.allMarkdownRemark.nodes

  // 현재 페이지, 총 페이지수 (gatsby-node.js의 createPage함수의 context로 넘겨받음)
  const { currentPage, numPages } = pageContext

  const renderedPosts = posts.map(({ node }) => (
    <Link to={node.fields.slug} key={node.fields.slug}>
      <div>{node.frontmatter.title}</div>
    </Link>
  ))

  return (
    <Layout>
      {renderedPosts}
      <Pagination
        count={numPages}
        page={currentPage}
        renderItem={(item) => (
          <PaginationItem
            component={Link}
            to={item.page === 1 ? `/page/` : `/page/${item.page}/`}
            {...item}
          />
        )}
      />
    </Layout>
  )
}

export default BlogList

// 페이지 쿼리 생략

Gatsby Link

위에서 PaginationItem(페이지 숫자 버튼)을 일반적인 <a> 태그가 아닌 Gatsby의 <Link> 컴포넌트로 구현했다.

 

<Link>의 중요한 특징은 컴포넌트가 사용자의 viewport 내에 들어오기만 해도 해당 링크가 가리키는 리소스에 대한 request가 시작된다는 것이다. 또 사용자가 <Link>에 마우스 커서를 올리게 되면, request의 우선순위를 더 높인다. 그래서 사용자가 링크를 클릭하게 될 시점에는 리소스를 이미 받아왔거나 받아오는 중이므로 매우 빠른 속도로 화면이 전환된다.

 

이러한 작동 방식은 당연히 Pagination 뿐만 아니라 사이트에 포함된 모든 Gatsby Link에 적용이 된다. 그래서 Gatsby에서는 속도를 위해 외부로 향하는 링크가 아닌 internal link들에 대해서는 모두 Gatsby Link를 사용하는 것을 권장하고 있다. (참고: Gatsby Link API)