글 목록 페이지에는 DOM 사이즈를 줄이라는 조언이 있었다. DOM에 1620개나 되는 요소들이 있다고 한다. 자식이 110개나 있는 요소도 있고. 이런 식으로 DOM 크기가 너무 크고 child 노드도 많으면 메모리 사용량이 늘고 스타일 계신이 너무 길어지며 레이아웃 리플로우(문서 내 요소의 위치를 계산되는 프로세스)도 오래 걸리게 된다.
안 그래도 글 목록 페이지의 길어진 스크롤이 불편하던 참이었다. Vercel 템플릿에서 SSG 페이지네이션코드가 공개되어 있길래 이를 사용해 보았다.
먼저 동적 라우트를 사용해야 하기 때문에 동적 라우트 폴더를 새로 만들자. pages/posts/[category] 내에 동적 라우트가 있어야 하는데 여기에는 이미 [category]/[slug] 라는 동적 라우트가 있다. 따라서 pages/posts/[category]/page/[page]/index.tsx로 동적 라우트를 새로 만들었다.
이 템플릿은 페이지별로 상품의 목록을 보여주는 페이지네이션을 구현한 것이다. 이 템플릿의 핵심 로직을 나름대로 분석하면 다음과 같다.
이를 내 블로그의 현재 구조에 맞게 적절히 변경하여 구성해 보자.
PaginationPage컴포넌트
3. CategoryPagination 컴포넌트
기존에 쓰던 카테고리 페이지의 컨텐츠 부분을 따와서 CategoryPagination 컴포넌트를 만들었다. 템플릿의 PaginationPage 컴포넌트의 props에 현재 카테고리까지 props로 받아 오도록 했고 단순히 이를 보여주는 기능만 일단 구현했다.
그러면 src/pages/posts/[category]/index.tsx 페이지의 컴포넌트 구조는 CategoryPagenation를 사용하여 다음과 같이 바뀐다.
postList는 getStaticProps에서 잘 계산하여 props로 넘겨주어, 각 페이지별로 특정 개수의 글들만 목록에 보이도록 해야 한다.
다만 그전에 먼저 필요한 컴포넌트들을 모두 구현하자.
4. 페이지네이션 컴포넌트
페이지네이션 컴포넌트란 다음과 같이 현재 페이지 위치와 링크를 통한 페이지 이동을 하게 해주는 컴포넌트다.
이 컴포넌트를 만들기 위해 src/components/categoryPagination/pagination/index.tsx를 만들고 작성하자.
inc부터 length 길이의 배열을 만들어서 리턴하는 getPages 함수를 정의한다.
페이지네이션에 표시할 숫자와 문자열들의 배열을 리턴하는 getPaginationArray함수도 정의한다. currentPage의 값에 따라서 전체 페이지 중 적절한 페이지 번호들을 담은 배열을 리턴한다. 이때 중간에 dotts 변수를 이용해서 생략 표시도 넣는다.
이를 이용해서 페이지네이션을 보여주는 컴포넌트도 만든다. 페이지 번호의 경우 링크를 걸고, 현재 페이지와 같은 번호라면 강조 표시를 해준다.
그럼 간단하게 스타일링도 해볼까. src/components/categoryPagination/pagination/styles.module.css를 만들고 다음과 같이 작성한다.
그리고 className을 Pagination 컴포넌트 내에 부여한다.
그러면 이 페이지네이션을 어디에 배치해야 할까?
페이지네이션에 대응하는 다른 컨텐츠 전략인 무한 스크롤에 비해서 나는 페이지네이션을 택했다. 그리고 이것의 가장 큰 의미는 사용자에게 자신이 페이지를 제어하고 있다는 감각을 주는 것이라고 생각한다.
무한 스크롤에 비해서 마지막 페이지가 어딘지도 알 수 있고, 그 페이지에 비해 자신이 어느 지점에 있는 글을 보고 있는지도 짐작할 수 있기 때문이다.
이러한 감각을 극대화하기 위해서는 페이지네이션이 게시판의 맨 위에 배치되어 있는 게 가장 적절하다고 생각된다.
그래서 CategoryPagination 컴포넌트를 다음과 같이 수정하여 페이지네이션 컴포넌트가 카테고리 제목 바로 아래에 보이도록 했다. 사용자가 게시판에 들어오자마자 자신이 제어한다는 감각을 가질 수 있도록 하기 위해서이다.
5. 개별 페이지 만들기
이제 src/pages/posts/[category]/page/[page]/index.tsx를 작성하여 개별 페이지의 내용을 구현하자.
개별 페이지를 구현하기 위해선 뭐가 필요할까? 일단 해당 페이지의 글을 가져와야 한다. 이는 이전에 글을 가져오는 데에 쓰던 getSortedPosts함수를 쓸 수도 있다.
하지만 여기서는 우리가 지금 해야 하는 작업 즉 특정 카테고리의 특정 페이지의 글을 가져오는 로직을 새로운 함수로 만들자. src/utils/post.ts에 getCategoryPosts 함수를 만들었다.
category와 현재 페이지 그리고 페이지당 몇 개의 글이 들어가는지를 인수로 받는 함수이다. 그러면 함수 내에선 getSortedPosts의 결과에서 먼저 인수로 받은 category에 해당하는 글만 뽑아낸다. 그다음은 currentPage와 postsPerPage를 이용해서 현재 페이지에 해당하는 글 목록만 슬라이싱해서 배열로 가져온다.
이때 pagePosts뿐 아니라 totalPostNumber 즉 해당 카테고리에 속한 글의 개수도 리턴하는 것을 볼 수 있다. 이는 이후 페이지네이션 컴포넌트(페이지를 이동하는 데 쓰이는 컴포넌트)를 구현하는 데에 쓰일 것이다.
그럼 이제 개별 페이지에서는 이 함수를 사용해서 맞는 카테고리의 해당 페이지에 필요한 글을 가져온 후 보여주기만 하면 된다.
페이지당 몇 개의 글 Card를 보여줄지를 정하는 변수를 정의하자.
getStaticPaths를 작성해보자. 여기서는 각 카테고리별로 필요한 페이지들의 경로를 생성해서 paths로 리턴해 주면 된다. 다음과 같이 작성한다.
blogCategoryList에 있는 모든 카테고리들에 대해서 /posts/[카테고리]/page/[페이지번호]에 해당하는 URL 경로를 생성해주고 있다. 그런데 코드를 보면 페이지 번호를 1부터 5까지만 생성한다. 만약 글이 50개(정확히는 5*ITEMS_PER_PAGE개)를 넘어가면 다음 페이지는 어떻게 들어갈까?
이 ISR 로직은 다음과 같다. getStaticPaths에서 생성되지 않은 페이지가 처음으로 요청되면 일단 서버사이드 렌더링을 한 후 이를 캐싱하고 백그라운드에서 새로운 페이지를 생성한다. 그리고 해당 페이지에 대한 다음 요청부터는 정적 페이지로 제공한다. 따라서 사이트를 빌드한 후에도 정적 페이지가 새로 생성되도록 할 수 있게 된다.
5페이지를 넘어가면 아무래도 사용자가 해당 페이지를 요청할 확률이 적어지므로 적절한 조치라고 할 수 있겠다.
getStaticProps를 작성해보자. params로 받을 수 있는 정보는 category와 page인데 각 페이지에 몇 개의 글이 들어가는지는 이미 상수로 정의해 놓았으므로 이를 이용하면 페이지를 위한 정보를 모두 받아올 수 있다.
getCategoryPosts를 이용해서 페이지의 글을 모두 받아오고, map을 이용해서 PostMetaData의 image에 post._raw.thumbnail을 대응시킨다. 이렇게 만든 객체 배열을 리턴값으로 넘긴다.
이때 만약 페이지 정보에 해당하는 글이 없으면 404 페이지를 띄워줘야 하고 1페이지에 대한 요청은 /posts/[category]로 리다이렉트 시켜줘야 한다. 같은 내용에 대한 2가지 라우트를 막기 위함이다.
이렇게 하니까 배포시 에러가 뜬다. 다 비슷한 에러인데 그중 하나를 가져오면 다음과 같다.
Error: `redirect` can not be returned from getStaticProps during prerendering (/posts/cs/page/1)
페이지가 프리렌더링될 때 redirect를 리턴할 수 없다는 에러다. 빌드 시에 페이지를 구성하면서 getStaticProps에서 리턴한 값이 페이지 컴포넌트의 props로 들어가는데 이때 redirect를 리턴하면 페이지 구성에 문제가 생기는 것 같다.
우리의 목적은 사실 1페이지에 대한 요청을 /posts/[category]로 리다이렉트 시키는 것이다. 이를 위해서는 그냥 getStaticPaths에서 1페이지에 대한 경로를 생성하지 않으면 된다.
getStaticPaths를 다음과 같이 수정하자.
이제 빌드가 잘 되고 페이지 URL로 접근해 보면 페이지에도 잘 들어가진다. 이를 각 카테고리별 첫 페이지에도 적용한다. src/pages/posts/[category]/index.tsx를 편집.