DogKaeBi

[Next.js 블로그] 리스트 & 페이지네이션 통합편

List, Pagination을 작성 완료했다. 아직 정리가 필요해 보이는 곳은 많지만 우선 기능적으로 작동하고, 작동 이유도 알 것 같다.

[Next.js 블로그] 리스트 & 페이지네이션 통합편

참고 내용

해당 내용

[카드 리스트 기본틀]
[카드 리스트 grid]
[한자 카드]
[블로그 카드]
[블로그 카드 이미지]
[페이지별 다른 카드 리스트]
[카드 수량 제한/컨트롤러] [페이지네이션]

컴포넌트 : [Next.js 컴포넌트]
searchParams : [Sub Nav]


사용하는 페이지

3곳에서 사용된다 :
한자 카드 리스트 (/cantonese)
단어 카드 리스트 (/cantonese/word)
일기 카드 리스트 (/blog)

한자/단어 카드는 12장/페이지,
일기 카드는 6장/페이지.
(변경 가능)

한자/단어는 pathname 으로 구분되고
일기는 pathname과 params의 tag으로 구분된다.


Card List 내용

grid로 정렬한다.
한자 한줄 4개,
단어/일기 한줄 3개.

한자/단어는 CnCard 컴포넌트를
일기는 BlogCard 컴포넌트를 사용한다.


Cn Card 내용

한자, 발음, 뜻이 출력.
링크 id, sort id 필요.


Blog Card 내용

제목, 설명문, 시간, 썸네일 출력.
링크 slug 필요.


Pagination 내용

앞뒤 버튼 및 숫자 버튼.
현재 페이지가 첫 페이지, 앞 버튼 없음.
현재 페이지가 끝 페이지, 뒤 버튼 없음.
첫/끝 번호 항시 출력.
페이지 수량 7이하 모든 숫자 표시.
페이지 수량 7이상 앞뒤 번호 출력, 기타 생략.

현재pathName, tag유지, 페이지 번호 변경.


controller 내용

카테고리, 페이지 번호, 카드 수량을 받음.
해당 카드 데이터 전달.
총 카드 수량 전달.

(
아직 데이터 베이스가 없음으로
전체 데이터를 매번 다 읽음에서 효율 낮음
차후 데이터별 총 수량의 변수가 따로 있는 것이 효율적으로 예상
)



컨트롤러 getCardData.js

// .\app\controller\getCardData.js

export function getCardData(category, pageNum, maxCard) {
  const data = getCategoryData(category);
  const allLength = data.length;
  const cardLength = allLength >= pageNum * maxCard ? maxCard : allLength % maxCard;
  const startNum = (pageNum - 1) * maxCard;
  const endNum = startNum + cardLength;

  return {
    data: data.slice(startNum, endNum),
    length: allLength,
  };
}

function getCategoryData(category) {
  if (category == "tc") return getTcData();
  if (category == "word") return getWordData();
  if (category == null) {
    return getBlogData();
  } else {
    return getBlogData().filter((post) => post.category == category);
  }
}
참고: DB를 결정하지 않아서 getTcData getWordData getBlogData은 임시 array 데이터를 return 하게 만들었다. 즉 함수 형태이지만 실제로는 그냥 배열 변수이다.
  • getCategoryData으로 카테고리의 모든 데이터를 호출.
  • 이번 페이지 카드 수량 계산.
  • 배열의 시작번호와 끝번호를 계산.
  • 배열을 slice해 전달.
  • 카테고리 전체 길이 전달.


컴포넌트 CardList.js

// .\app\components\CardList.js
import CnCard from './CnCard.js'
import BlogCard from './BlogCard.js'

const cardList = ({type, data}) => {
  const wrapClass = {
    tc: "grid gap-4 grid-cols-4"
    word: "grid gap-4 grid-cols-3"
    blog: "grid gap-4 grid-cols-3"
  }

  return (
    <div className={wrapClass[type]}>
      {type == "blog"
        ? data.map((post) => <BlogCard key={post.slug} data={post}>)
        : data.map((dict) => <CnCard key={dict.id} data={dict}>)
      }
    </div>
  )
}

export default cardList;
  • type과 data를 받음
  • type으로 css를 결정
  • type으로 자녀 컴포넌트 결정(CnCard vs BlogCard)
  • data를 자녀에게 전달


컴포넌트 카드

// .\app\components\CnCard.js
import Link from "next/link";

const CnCard = ({ data }) => {
  const cardClass = "text-center shadow hover:scale-105";
  const jyutClass = "overflow-hidden whitespace-nowrap text-ellipsis";

  return (
    <Link className={cardClass} href={`/cantonese/${data.id}`}>
      <p className={jyutClass}>{data.jyutJam}</p>
      <h1>{data.tc}</h1>
      <p>{data.title}</p>
    </Link>
  );
};
// .\app\components\BlogCard.js
import Link from "next/link";
import Image from "next/image";

const BlogCard = ({ data }) => {
  const cardClass = "overflow-hidden shadow hover:scale-105";
  const imgClass = "object-cover h-40";
  const lineClamp = "text-elipsis line-clamp-2";

  return (
    <Link className={cardClass} href={`/blog/${data.slug}`}>
      <Image src="./logo.jpg" width={300} height={200} alt={data.slug} className={ImgClass} />
      <h1 className={lineClamp}>{data.title}</h1>
      <p className={lineClamp}>{data.desc}</p>
      <p>{data.date}</p>
    </Link>
  );
};

Link의 href의 page는 임시 링크 확인용 빈 페이지를 만들었다.

카드는 디자인에 관련된 내용으로 특별점 없음.
부모 CardList가 grid으로 width 없음.

외부 이미지를 사용하고 싶으면
next.config.js에 이미지 링크를 추가한다.

const nextConfig = {
  images: {
    remotePatterns: [
      {
        protocol: "https",
        hostname: "placehold.co",
      },
    ],
  },
};

module.exports = nextConfig;


컴포넌트 Pagination

// .\app\components\Pagination.js

const Pagination = (props) => {
  const tag = props.tag;
  const pageQty = Number(props.maxPages);
  const page = Number(props.page);
  const pageNums = setPageNums(page, pageQty);

  function setPageNums(currentPage, lastPage){
    if (lastPage < 7) {
      let res = [];
      for (let i = 1, i <= lastPage ; i++) {
        res.push(i);
      }
      return res;
    } else {
      const prevPage = currentPage - 1;
      const nextPage = currentPage + 1;
      if (currentPage == 1) return [1, 2, -1, lastPage];
      if (currentPage == 2) return [1, 2, 3, -1, lastPage];
      if (currentPage == 3) return [1, 2, 3, 4, -1, lastPage];
      if (currentPage == lastPage) return [1, -1, prevPage, lastPage];
      if (currentPage == lastPage - 1) return [1, -1, prevPage, currentPage lastPage];
      if (currentPage == lastPage - 2) return [1, -1, prevPage, currentPage, nextPage, lastPage];
      return [1, -1, prevPage, currentPage, nextPage, -2, lastPage];
    }
  }

  function setParams(num){
    if (num < 1 || num > pageQty) return;
    if (tag == "" || tag == null) {
      return { page : num };
    } else {
      return { tag : tag, page: num };
    }
  }

  return (
    <div className="flex justify-center items-center">
      {page != 1 ? <Link href={{query: setParams(page - 1)}}>◀<Link/> : null}
      {pageNums.map((num) => {
        return num == -1 || num == -2 ? (
          <span key={("btn", num)}>...</span>
        ) : page == num ? (
          <span key={("btn", num)}>{num}</span>
        ) : (
          <Link key={("btn", num)} href={{ query: setParams(num) }}>
            {num}
          </Link>
        );
      })}
      {pageQty == 0 || pageQty == 1 || pageQty == page ? null
        : <Link href={{query: setParams(page + 1)}}>▶<Link/>}
    </div>
  )
}

부모에서 현재 페이지, 태그, 페이지수량을 받는다.

  • setPageNums에서 현재 페이지, 마지막 페이지로 계산. 배열을 생성
  • 배열을 map() 반복으로 링크를 생성
  • setParams에서 태그와 숫자로 query params를 만들어 반환
  • Link의 href에 setParams의 값을 전달


페이지에 적용하기

// .\app\cantonese\page.js

import Heros from "./components/Heros";
import SubNav from "./components/SubNav";
import CardList from "./components/CardList";
import Pagination from "./components/Pagination";
import { setCardList } from "./controller/setCardList";

export default function Cantonese(props) {
  const path = "tc";
  const page = props.searchParams.page ?? 1;
  const maxCardInPage = 12;
  const tcData = getCardData(path, page, maxCardInPage);
  const maxPages = Math.ceil(tcData.allLength / maxCardInPage);

  return (
    <>
      <Heros path={path} />
      <SubNav path={path} />
      <p>등록 한자 : {tcData.allLength}</p>
      <CardList path={path} data={tcData.list} />
      <Pagination page={page} maxPages={maxPages} />
    </>
  );
}
// .\app\blog\page.js

export default function Blog(props) {
  const path = "blog";
  const tag = props.searchParams.tag;
  const page = props.searchParams.page ?? 1;
  const maxCardInPage = 6;
  const blogData = getCardData(tag, page, maxCardInPage);
  const maxPages = Math.ceil(blogData.allLength / maxCardInPage);

  return (
    <>
      <Heros path={path} />
      <SubNav path={path} slug={tag ?? ""} />
      <CardList path={path} data={blogData.list} />
      <Pagination tag={tag} page={page} maxPages={maxPages} />
    </>
  );
}


Card List를 마치면서

이렇게...
카드(포스트) list와 Pagination까지 완료했다.

나도 코딩은 처음이어서
데이터를 어디서 만들고...
함수를 어디서 호출하는 것이 좋은지 아직 잘 모르겠다.

지금은 대부분
페이지에서 데이터를 호출한다.

getCardData도 페이지에서 사용했지만
"등록한자"를 CardList에 포함시키면
CardList에서 호출해도 된다.

Pagination의 setPageNums
controller 폴더로 분리할 수 있고,
부모에서 호출해서 값만을 전달할 수도 있다.

SubNav, CardList, Pagination은
언제나 같이 사용되서
하나의 부모에서 호출해도 된다...

개인적인 생각으로는
중복해서 같은 값/함수를 호출하는 것은 방지해야 한다.
만약 자녀에서 사용한 함수를 부모도 사용해야 하면...
부모에서 사용하고 자녀에게 값을 전달하는 것이 맞는 것 같다.
반대로
부모에서 값을 사용하지 않으면
자녀에서 계산하고 사용하는 것이 맞는 것 같다.

하지만 부모의 변수가 함수의 argument(전달인자)이면
부모의 변수를 자녀로 전달할지
부모에서 함수 호출로 결과값을 전달할지
햇갈리는 것 같다.


컴포넌트 사이의 의존성을 최소화해야 한다.

이라는 말을 들은 적이 있는 것 같다.
그에 비해 지금 내 코드는 전달인자가 넘쳐나는 느낌이다;;