Lsiron

pagination 구현하고 분석해보기. 본문

백엔드/Node.js

pagination 구현하고 분석해보기.

Lsiron 2024. 6. 12. 06:21

페이지네이션을 구현해보자.

 

현재 '이전' 버튼과 'n페이지 이동' 버튼 그리고 '다음' 버튼을 구현해보고자 한다.

 

JS template engine은 ejs 를 사용하였고, URL parameter 방법을 이용하여 get 요청을 하였다.

 

서버 측의 app.js 와 클라이언트 측의 list.ejs 가 있다.

 

먼저 완성본부터 확인해보자.

 

 

<app.js>

app.get('/list/:page', async (req, res) => {
  let page = parseInt(req.params.page) || 1;
  const postsPerPage = 5;
  const totalPosts = await db.collection('post').countDocuments();
  const totalPages = Math.ceil(totalPosts / postsPerPage);

  if (page < 1) page = 1;
  if (page > totalPages) page = totalPages;

  let result = await db.collection('post').find()
    .skip((page - 1) * postsPerPage)
    .limit(postsPerPage)
    .toArray();

  res.render('list.ejs', {
    posts: result,
    currentPage: page,
    totalPages: totalPages,
    hasPrev: page > 1,
    hasNext: page < totalPages
  });
});

 

<list.ejs>

    <div class="pagination">
      <% if (hasPrev) { %>
        <a href="/list/<%= currentPage - 1 %>">이전</a>
      <% } else { %>
        <span>이전</span>
      <% } %>

      <% for (let i = 1; i <= totalPages; i++) { %>
        <% if (i === currentPage) { %>
          <span><%= i %></span>
        <% } else { %>
          <a href="/list/<%= i %>"><%= i %></a>
        <% } %>
      <% } %>

      <% if (hasNext) { %>
        <a href="/list/<%= currentPage + 1 %>">다음</a>
      <% } else { %>
        <span>다음</span>
      <% } %>
    </div>

 

 

먼저 app.js 부터 분석을 해보자.

 

<app.js>

app.get('/list/:page', async (req, res) => {
  let page = parseInt(req.params.page) || 1;
  const postsPerPage = 5;
  const totalPosts = await db.collection('post').countDocuments();
  const totalPages = Math.ceil(totalPosts / postsPerPage);

  if (page < 1) page = 1;
  if (page > totalPages) page = totalPages;

  let result = await db.collection('post').find()
    .skip((page - 1) * postsPerPage)
    .limit(postsPerPage)
    .toArray();

  res.render('list.ejs', {
    posts: result,
    currentPage: page,
    totalPages: totalPages,
    hasPrev: page > 1,
    hasNext: page < totalPages
  });
});

 

1.라우트 설정

app.get('/list/:page', async (req, res) => {

 

GET 요청을 처리하며, URL 경로에서 페이지 번호를 매개변수로 받는다.

예를 들어 /list/1은 첫 번째 페이지를, /list/2는 두 번째 페이지를 의미한다. ( 저 뒤에 page 는 굳이 page가 아니여도 이름을 맘대로 지어도 된다.)

 

2. 페이지 번호 파싱

let page = parseInt(req.params.page) || 1;

 

req.params 로 하면, 유저가 URL 파라미터 자리에 입력한 글자가 담겨있다.

( 그러면 req.params.page 는 유저가 page에 무슨 숫자를 입력했는지를 나타낸다는 뜻)

 

req.params.page를 정수로 파싱하여 page 변수에 저장한다.

만약 req.params.page가 유효하지 않으면 기본값으로 1을 사용한다.

( 유저가 숫자 말고 쓸데없는 값을 집어넣으면 무조건 1로 봄 )

 

3. 페이지당 게시물 수 및 총 게시물 수 계산

const postsPerPage = 5;
const totalPosts = await db.collection('post').countDocuments();

 

한 페이지에 표시할 게시물 수를 postsPerPage로 설정한다.

데이터베이스의 post 컬렉션에서 전체 게시물 수를 totalPosts에 저장한다.

 

4. 총 페이지 수 계산

const totalPages = Math.ceil(totalPosts / postsPerPage);

 

총 페이지 수를 계산한다. 총 게시물 수를 페이지당 게시물 수로 나누고 올림하여 계산한다. ( 그냥 공식임 )

 

5. 페이지 번호 유효성 검사 및 조정

if (page < 1) page = 1;
if (page > totalPages) page = totalPages;

 

페이지 번호가 1보다 작으면 1로, 총 페이지 수보다 크면 총 페이지 수로 조정한다.

 

6. 게시물 조회

let result = await db.collection('post').find()
  .skip((page - 1) * postsPerPage)
  .limit(postsPerPage)
  .toArray();

 

database의 'post' 컬렉션에서 필요한 페이지의 게시물을 find() 메서드를 사용하여 조회한다.

( cf. find({}) 로 사용하면 {} 내부의 조건에 맞는 것만 가져온다는 뜻.

findOne() 은 mongodb 데이터 하나만 가져온다는 뜻. 보통 데이터를 가져올때 아래와 같이 많이 사용한다.)

app.get('/list', async (req, res) => {        // 서버를 요청하는 코드
   let result = await db.collection('post').find().toArray() // 외우기, mongoDB 모든 데이터를 가져오는 방법
   res.render('list.ejs',{posts : result})  // 유저에게 ejs 파일 보내는 법(render), 응답은 1번밖에 안됨
 })

 

skip() 메서드를 사용하여 이전 페이지의 게시물 수만큼 건너뛰고,

( ex: skip(10) 뜻은 데이터를 맨 위에서 10개를 skip한다는 뜻)

 

limit() 메서드를 사용하여 현재 페이지의 게시물만 가져온다.

( ex: limit(10) 뜻은 데이터를 맨 위에서 10개만 가져온다는 뜻)

 

그 결과를 toArray()를 통해 배열로 변환한다.

 

7. 뷰 렌더링

res.render('list.ejs', {
  posts: result,
  currentPage: page,
  totalPages: totalPages,
  hasPrev: page > 1,
  hasNext: page < totalPages
});

 

list.ejs 템플릿을 렌더링하며, 필요한 데이터를 전달한다.

  • posts: 현재 페이지의 게시물 목록
  • currentPage: 현재 페이지 번호
  • totalPages: 총 페이지 수
  • hasPrev: 이전 페이지가 있는지 여부 (currentPage가 1보다 큰 경우 true)
  • hasNext: 다음 페이지가 있는지 여부 (currentPage가 총 페이지 수보다 작은 경우 true)

즉, 변수명과 그 값들을 담아서 list.ejs에 넘겨주는 것임.

 

다음은 list.ejs를 분석 해 보자.

 

<list.ejs>

    <div class="pagination">
      <% if (hasPrev) { %>
        <a href="/list/<%= currentPage - 1 %>">이전</a>
      <% } else { %>
        <span>이전</span>
      <% } %>

      <% for (let i = 1; i <= totalPages; i++) { %>
        <% if (i === currentPage) { %>
          <span><%= i %></span>
        <% } else { %>
          <a href="/list/<%= i %>"><%= i %></a>
        <% } %>
      <% } %>

      <% if (hasNext) { %>
        <a href="/list/<%= currentPage + 1 %>">다음</a>
      <% } else { %>
        <span>다음</span>
      <% } %>
    </div>

 

!참고!

<% %> 와 같은 이상한게 들어 가 있는데 이게 무엇인지 부터 알아보자.

ejs에서 html 안에 자바스크립트 문법을 사용할 땐 이 <% %> 로 자바스크립트로 표현한 부분만을 감싸주어야 한다.

 

주의할 점: 정말 자바스크립트를 사용한 부분만 감싸줘야한다. 

 <% for (let i = 1; i <= totalPages; i++) { 
        if (i === currentPage) {
          <span> i </span>
         } else { 
          <a href="/list/i ">= i </a>
         } 
       } %>

 

=> 위와 같이 표현하면 안 된다. 엉망진창으로 출력이 될 것이다.

 <% for (let i = 1; i <= totalPages; i++) { %>
        <% if (i === currentPage) { %>
          <span><%= i %></span>
        <% } else { %>
          <a href="/list/<%= i %>"><%= i %></a>
        <% } %>
      <% } %>

 

=> 위와 같이 딱 자바스크립트를 쓰는 부분만 감싸 줘야한다.

 

종류는 아래와 같이 총 세가지가 있다. 

<% 감싸줘 %>, <%- 감싸줘 %>, <%= 감싸줘 %>

 

먼저 <% 감싸줘 %> 는 for 반복문이라던지 if 조건문이라던지 생 자바스크립트 문법 쓸 때 사용한다.

 

다음으로 <%- 감싸줘 %> 는 include와 같이 특수한 문법을 쓸 때 사용한다.

<%- include('nav.ejs') %> <!-- nav.ejs html을 불러와서 넣어준다. -->

 

마지막으로 <%= 감싸줘 %><%- 감싸줘 %> 와 기능이 거의 유사하다. 

 

허나 %- 는 안에 있는 데이터가 html 형식일 경우 실제 html처럼 인식하고, 반대로 %= 는 문자로 인식한다. 즉, 

 <%- <a>안녕</a> %> 은 페이지에 안녕 으로 나오고 <%= <a>안녕</a> %> 은 페이지에 <a>안녕</a> 이대로 나온다.

 

즉, 위 코드 예시에 있는 include 문법을 쓰면 nav.ejs에서 페이지로 표현되는 것이 그대로 페이지에 나온다.

 

1. 페이지네이션 컨테이너

<div class="pagination">

 

모든 페이지네이션 요소를 포함하는 div 요소이다. CSS 클래스를 사용하여 스타일을 지정할 수 있다.

 

2. 이전 페이지 링크

<% if (hasPrev) { %>
  <a href="/list/<%= currentPage - 1 %>">이전</a>
<% } else { %>
  <span>이전</span>
<% } %>

 

hasPrev가 true일 때 (currentPage가 1보다 큰 경우): 이전 페이지로 이동하는 링크를 렌더링한다.

<a href="/list/<%= currentPage - 1 %>">이전</a>

 

currentPage - 1은 이전 페이지 번호이다.

 

hasPrev가 false일 때: 이전이라는 텍스트만 렌더링하여 현재 페이지가 첫 페이지임을 표시한다.

<span>이전</span>

 

 

3. 페이지 번호 링크

<% for (let i = 1; i <= totalPages; i++) { %>
  <% if (i === currentPage) { %>
    <span><%= i %></span>
  <% } else { %>
    <a href="/list/<%= i %>"><%= i %></a>
  <% } %>
<% } %>

 

 

1부터 totalPages까지 반복하면서 각 페이지 번호를 렌더링한다.

 

현재 페이지 번호인 경우: 링크 대신 현재 페이지 번호를 텍스트로 표시한다.

 

<span><%= i %></span>

 

현재 페이지 번호가 아닌 경우: 해당 페이지로 이동하는 링크를 렌더링 한다.

<a href="/list/<%= i %>"><%= i %></a>

 

 

4. 다음 페이지 링크

<% if (hasNext) { %>
  <a href="/list/<%= currentPage + 1 %>">다음</a>
<% } else { %>
  <span>다음</span>
<% } %>

 

hasNext가 true일 때 (currentPage가 totalPages보다 작은 경우): 다음 페이지로 이동하는 링크를 렌더링한다.

<a href="/list/<%= currentPage + 1 %>">다음</a>

 

currentPage + 1은 다음 페이지 번호이다.

 

hasNext가 false일 때: 다음이라는 텍스트만 렌더링하여 현재 페이지가 마지막 페이지임을 표시한다.

<span>다음</span>

 

 

위 사진 처럼 구현이 되었다.

 

1페이지 일 경우, 이전 버튼이 활성화가 되지 않으며, 마지막 페이지 일 경우 다음 버튼이 활성화 되지 않는다.

 

또한 각 페이지를 옮겨 다닐 수 있으며, URL상엔 페이지 번호가 입력된다.