본문 바로가기
프로젝트

[블로그 프로젝트] 3. 카테고리 분류 + 페이지네이션 구현

by Piva 2024. 2. 16.
  • 지난 글에서 설정한 front matter를 통해, 게시물에 다양한 정보를 넣을 수 있다.
  • 이것을 이용해서 각 게시물마다 카테고리를 부여하고, 카테고리 별 게시물 분류 및 간단한 페이지네이션 적용기에 대해 정리한다.

카테고리 분류

Front matter를 통해 카테고리 부여하기

  내 블로그 게시물의 front matter는 다음과 같이 작성하고 있다.

---
title: Random post
createdAt: '2023.10.31'
category: 'javascript'
draft: false
thumbnail: '/images/dummy_thumbnail.jpeg' 
---

// 이 외에도 상황에 따라 다른 데이터가 들어간다

 

  위 front matter를 통해 알 수 있듯, 게시물의 공개 여부/썸네일 지정/카테고리 지정까지 많은 것을 front matter를 통해 관리하는 것이 가능하다. 정해진 형식이 없으니 자신이 활용하고자 하는 바에 따라 다양한 항목을 지정할 수 있다.

 

 

카테고리 관리

  카테고리를 타입화 하여 사용할 수 있도록, 블로그 내에서 사용할 카테고리 리스트를 만든 후 이들을 자동으로 타입화 하는 방법을 사용하고 있다.

export const allCategories = {
  javascript: 'javascript',
  react: 'react',
  project: 'project',
  etc: 'etc',
} as const;

export type Category = (typeof allCategories)[keyof typeof allCategories];

 

  원래 타입에 Enum 을 사용했지만, 이런저런 이유로 Union 타입으로 변경했다. 이 이슈에 대해서는 나중에 별도로 다뤄보는 것으로.

 

 

카테고리 별 페이지 만들기

  지난 번 게시물 페이지와 동일하게 동적 라우팅을 사용했다. 각 카테고리 이름을 url로 사용하는 방식이다.

블로그 페이지 구조

 

 

  위에서 언급했듯, 전체 카테고리 리스트를 만들어 관리하고 있으므로, 이를 getStaticPaths에 바로 활용할 수 있다. 각 카테고리에 속한 게시물 만을 가져오는 것은 간단하게 filter 함수를 활용했다. 모든 게시물을 가져온 후, filter 함수를 통해 선택된 카테고리에 속하는 게시물만을 필터링하는 방식이다.

 

export const getStaticPaths: GetStaticPaths = async () => {
  // 모든 카테고리 리스트를 가져와 getStaticPaths의 paths로 넘긴다.
  const paths = Object.values(allCategories).map((el) => ({
    params: { category: el },
  }));

  return {
    paths,
    fallback: false,
  };
};

export const getStaticProps: GetStaticProps = async (context) => {
  const category = context.params?.category;
  // context를 통해, 동적 루트의 Segment([category]의 category)를 확인할 수 있다.
  
  // 모든 게시물에서 선택한 카테고리에 속하는 게시물만을 가져온다.
  const postsInCategory = getAllPosts().filter(
    (post) => post.category === category,
  );

  return {
    props: {
      posts: postsInCategory,
      category,
    },
  };
};

 

 

카테고리 UI

  각 카테고리 아이템은 간단히 모든 카테고리 리스트를 렌더링 하는 방식으로 구현했다.

데스크탑에서의 카테고리 페이지 이동

 

  다만 모바일의 경우, 카테고리가 늘어나면 늘어날 수록 단순한 리스트 형식으로 모든 카테고리를 전부 렌더링 하는 것이 보기에 좋지 않을 거란 생각이 들었다. 따라서 모바일에는 별도의 컴포넌트를 만들어, 데스크탑과는 달리 드롭다운 형식으로 카테고리를 선택할 수 있도록 만들었다.

 

import { Category, allCategories } from '@/types';
import { useRouter } from 'next/router';
import { ChangeEvent } from 'react';

const MobileCategories = ({ defaultCategory }: { defaultCategory: Category }) => {
  const router = useRouter();
  const selectedCategory = defaultCategory as Category;

  let defaultValue = 'all';

  if (selectedCategory != null) {
    defaultValue = defaultCategory as Category;
  }

  const onChangeSelection = (e: ChangeEvent<HTMLSelectElement>) => {
    const { value } = e.target;
    if (value === 'all') {
      router.push('/');
    } else {
      router.push(`/categories/${value}`);
    }
  };

  return (
    <select
      onChange={onChangeSelection}
      defaultValue={defaultValue}
    >
      <option value="all" >전체</option>
      {Object.keys(allCategories).map((el) => (
        <option key={el} value={el}>
          {el}
        </option>
      ))}
    </select>
  );
};

 

  모바일 카테고리 컴포넌트의 코드를 가져왔다. 드롭다운을 구현하기 위해 select / option 태그를 사용하였다.

  • 전체 카테고리 리스트를 가져와 option 태그로 렌더한다.
  • select 태그를 통해 option 태그로 이루어진 메뉴를 컨트롤하는 것이 가능하다.
    • select 태그에 defaultValue를 설정할 수 있다. 아무 카테고리가 선택되어있지 않으면 전체(all), 선택된 상태라면 해당 카테고리를 defaultValue로 설정하게끔 했다. 선택된 카테고리는 prop으로 [category]페이지에서 전달한다.
    • 특정 카테고리를 선택하면 onChange와 useRouter를 통해 해당 카테고리 페이지로 이동하게 한다. 

 

모바일 카테고리 컴포넌트

 

  이미지와 같이 정상적으로 카테고리 페이지가 나오는 것을 확인할 수 있다.

 

 

 

페이지네이션 구현

  페이지네이션은 여러 아이템이나 컨텐츠를 한꺼번에 띄우는 대신, 페이지를 나누어 띄우는 것을 의미한다. 블로그에서는 띄우는 다수의 컨텐츠는 게시물이므로, 전체 게시물 페이지 및 카테고리별 게시물 페이지에 페이지네이션을 적용하게 된다.

 

  페이지네이션의 구현에는 query를 사용했다. query란 url을 통해 전달되는 데이터이다.

? 뒷 부분이 query. 여기서는 2의 값을 갖는 page를 전달한다.

 

  위처럼 Page라는 값을 url을 통해 전달함으로써, 현재 몇 번째 페이지를 보여주어야 하는지를 알 수 있다.

 

  페이지네이션에는 아래와 같은 과정을 거쳤다.

  • useRouter 훅을 사용해 현재 선택된 카테고리와 page 값을 가져온다.
  • 선택된 카테고리가 있다면 해당 카테고리에 속하는 게시물들을, 없다면 전체 게시물들을 가져와 페이지를 나눈다.
    • 각 페이지마다 보여줄 게시물의 최대 개수를 정한다. 내 블로그의 경우에는 5개이다.
    • 현재 페이지 값에 따라 slice 혹은 filter 등의 함수를 통해 보여줄 게시물들을 선택한다.

  아래엔 페이지네이션에 사용된 코드 일부를 가져왔다(전체 코드는 깃허브에 있다).

import { useRouter } from 'next/router';
import { useEffect, useState } from 'react';

const MAX_CONTENTS_DISPLAY = 5;

const router = useRouter();
const [currentPage, setCurrentPage] = useState<number>(1);
const [visiblePosts, setVisiblePosts] = useState<Post[]>([]);

useEffect(() => {
  // useRouter 훅을 통해 query로 전달된 page를 가져온다.
  const page = router.query?.page ?? '1';
  setCurrentPage(Number.parseInt(page as string, 10));
}, [router.query?.page]);

useEffect(() => {
  // 현재 페이지에 띄워질 게시물들을 뽑는다.
  const startIdx = (currentPage - 1) * 5;
  const endIdx = startIdx + MAX_CONTENTS_DISPLAY;

  const selectedPosts = originalPosts.slice(startIdx, endIdx);
  setVisiblePosts(selectedPosts);
}, [currentPage, originalPosts]);

 

 

  비슷하게, 현재 카테고리 내의 전체 게시물의 개수를 알고 있다면 다음 페이지로 이동할 수 있는지, 없는지의 여부 또한 알 수 있다. 이를 이용해서 다음/이전 페이지로 이동할 수 있는 Chevron 버튼 또한 구현했다.

 

완성된 페이지네이션의 모습