페이지네이션

매우 고급져보이는 이 단어의 뜻은 정말 생각지도 못하게 심플하고, 여러분은 이미 이걸 몇 번 봤다.

본인 티스토리 블로그 하단부

이런거 다들 한번씩 봤잖음. 블로그건 검색이건… 이게 페이지네이션이다. 저게 코딩관련 글을 11페이지로 옮겼는데(다 옮겼음) 분량이 어마무시하다 그죠…? 아무튼, 페이지네이션은 이런 식으로 한 페이지에 정해진 양의 콘텐츠를 보여준다. 반대되는 개념인 무한 스크롤은 끝이 보일때까지 아래로 아래로 내리는 것.

무한 스크롤은 보통 모바일 앱에서 많이 사용하는 방식인데, 인별이나 미디움도 무한 스크롤이다. 아래로 스크롤하면서 콘텐츠를 계속 볼 수 있어서 굳이 페이지 이동하고 로딩하는 걸 기다리지 않아도 된다. 하지만 콘텐츠가 많아지면 로딩하는 데 시간이 걸리기도 하고(…), 읽다 보면 스크롤바는 줄어드는데 이게 대체 어디까지 있는건지 모르기도 하고, 페이지의 밑으로 내려가면 계속 콘텐츠가 로딩이 되는 특성상 사이트의 푸터가 안보이기 때문에 사이드바로 푸터를 빼야 한다.

네이버 푸터

페이지네이션은 페이지를 클릭하고 로딩해야 하는 수고로움+추가 작업의 수고로움이 있지만 한 페이지에 표시할 수 있는 최대 콘텐츠의 수가 정해져있다. 그래서 내가 아까 봤던 글이 몇페이지 어디 있더라, 만 알면 다시 가서 볼 수 있다. 그리고 아 이쪽 콘텐츠는 재미없다 그러면 그냥 다음 페이지로 가면 된다. 이렇게 둘 다 일장일단이 있기 때문에 적절한 사용이 중요하다고.


페이지네이션 구현하기

Reference

https://nohack.tistory.com/125

https://velog.io/@eunoia/JS%EB%A1%9C-Pagination-%EA%B5%AC%ED%98%84%ED%95%98%EA%B8%B0

초간단! 페이지네이션

해당 코드는 참고문헌 1번에 있으니 별도로 올리지는 않는다. 근데 충격받음… 아니 이게 간단한거라고?

게시판이긴 한데, 테이블이 아니라 li태그 안에 display: flex를 줘서 구현했다. 저거… 아 저 내용물은 동적으로 생성한거라 저래요… DB에서 갖고온 게 아님… 아무튼 그렇다.

페이지네이션을 할 때 필요한 건

  1. 한 페이지에 콘텐츠 몇 개를 보여줄 것인가?
  2. 페이지를 넘기는 버튼은 한 페이지에 몇 개를 보여줄 것인가? (목록 밑에 숫자 써있는거)

이거다. 이 두 개를 정하고 나면 전체 콘텐츠 개수에 따라 필요한 페이지 수를 계산하고 버튼을 만들면 된다.

그리드 페이지네이션

위에 있는 컨텐츠는 게시판 리스트같이 생긴건데, 이번에 구현할 건 컨텐츠가 그리드 형태이다. 그니까 저렇게 길고 쭉 뻗은 리스트가 아니고 네모땡땡한 거다. 인스타 피드같은 거.

<html>

<head>
    <meta charset="UTF-8" />
    <title>Pagenation</title>
    <link rel="stylesheet" href="style.css" />
    <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" rel="stylesheet"
        integrity="sha384-1BmE4kWBq78iYhFldvKuhfTAU6auU8tT94WrHftjDbrCEXSU1oBoqyl2QvZ6jIW3" crossorigin="anonymous">
    <script src="https://kit.fontawesome.com/dc58858c96.js" crossorigin="anonymous"></script>
</head>

<body>
    <div class="wrapper">
        <div class="card" id="article-title">
            <div class="card-body">
                <h4 class="card-title">est velit</h4>
                <p class="card-text">Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor
                    incididunt ut labore et dolore magna aliqua. Quisque egestas diam in arcu cursus euismod. Eu feugiat
                    pretium nibh ipsum consequat nisl vel pretium. Tempus quam pellentesque nec nam. In fermentum
                    posuere urna nec tincidunt. Massa enim nec dui nunc mattis enim ut tellus elementum. Fringilla est
                    ullamcorper eget nulla facilisi etiam. At imperdiet dui accumsan sit amet nulla facilisi morbi. Vel
                    pretium lectus quam id leo. Ut faucibus pulvinar elementum integer enim neque. Gravida neque
                    convallis a cras semper auctor neque vitae. Lacus vestibulum sed arcu non odio euismod lacinia at
                    quis. Faucibus pulvinar elementum integer enim neque volutpat ac tincidunt. Porta nibh venenatis
                    cras sed felis.</p>
                <a href="#" class="btn btn-primary">Go somewhere</a>
            </div>
        </div>
        <div class="content-body">
            
        </div>
        <div class="buttons">
        </div>
    </div>
    <script src="script.js"></script>
    <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.bundle.min.js"
        integrity="sha384-ka7Sk0Gln4gmtz2MlQnikT1wXgYsOg+OMhuP+IlRH9sENBO0LRn5q+8nbTov4+1p"
        crossorigin="anonymous"></script>
</body>

</html>

물논 부트스트랩의 도움을 조금 받았지. (안에 있는 텍스트는 Lorem ipsum generator로 만들었음)

@font-face {
    font-family: 'DungGeunMo';
    src: url('https://cdn.jsdelivr.net/gh/projectnoonnu/noonfonts_six@1.2/DungGeunMo.woff') format('woff');
    font-weight: normal;
    font-style: normal;
}

* {
    margin: 0;
    padding: 0;
    font-family: 'DungGeunMo';
    font-size: 14pt;
    color: #234E70;
}

.wrapper {
    width: 1280px;
    margin: 0 auto;
}

#article-title {
    margin: 15px auto;
    background-color: #FBF8BE;
    border: 1px solid #234E70;
}

#article-title .btn-primary {
    background-color: #234E70;
    color: #FBF8BE;
    border: none;
}

.content-body {
    display: grid;
    grid-template-columns: repeat(3, 1fr);
    grid-template-rows: repeat(3.1fr);
    grid-gap:5px;
}

.col-sm-6 {
    width: 100% !important;
}

#contents {
    background-color: #234E70;
    color: #FBF8BE;
}

#contents .card-title,
#contents .card-text {
    color: #FBF8BE;
}

#contents .btn-primary {
    background-color: #FBF8BE;
    color: #234E70;
    border: none;
}

.buttons {
    text-align: center;
    width: 100%;
    margin:15px auto;
}

button {
    border: none;
    background-color: #fff;
    color: #234E70;
    width:50px;
}

.active {
    background-color: #234E70;
    color: #FBF8BE;
}

CSS의 경우 버튼 부분(페이지 아래에 있는 넘어가는 버튼)은 나중에 했다. 봐야 뭘 하지.

const contents = document.querySelector('.content-body')
const buttons = document.querySelector('.buttons')
const numOfContent = 100
const maxContent = 9
const maxButton = 5
const maxPage = Math.ceil(numOfContent / maxContent)
let page = 1

const makeContent = (id) => {
    const content = document.createElement('div')
    content.classList.add('col-sm-6')
    content.setAttribute('id', 'contents-card')
    content.innerHTML = `
                <div class="card" id="contents">
                    <div class="card-body">
                        <h5 class="card-title">No. ${id}</h5>
                        <p class="card-text">Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.</a>
                    </div>
                </div>`
    return content
}

const makeButton = (id) => {
    const button = document.createElement('button')
    button.classList.add('button')
    button.dataset.num = id
    button.innerText = id
    button.addEventListener("click", (e) => {
        Array.prototype.forEach.call(buttons.children, (button) => {
            if (button.dataset.num) button.classList.remove("active")
        })
        e.target.classList.add("active")
        renderContent(parseInt(e.target.dataset.num))
    })
    return button
}

const prevPage = () => {
    page -= maxButton
    render(page)
}

const nextPage = () => {
    page += maxButton
    render(page)
}

const prev = document.createElement('button')
prev.classList.add('button','prev')
prev.innerHTML = '<i class="fa-solid fa-angle-left"></i>'
prev.addEventListener('click', prevPage)

const next = document.createElement('button')
next.classList.add('button','next')
next.innerHTML = '<i class="fa-solid fa-angle-right"></i>'
next.addEventListener('click',nextPage)

const renderContent = (page) => {
    while(contents.hasChildNodes()) {
        contents.removeChild(contents.lastChild)
    }
    for (let id = (page - 1) * maxContent + 1;id <= page * maxContent && id <= numOfContent;id++){
        contents.appendChild(makeContent(id))
    }
}

const renderButton = (page) => {
    while(buttons.hasChildNodes()) {
        buttons.removeChild(buttons.lastChild)
    }
    for (let id = page;id < page + maxButton && id <= maxPage;id++) {
        buttons.appendChild(makeButton(id))
    }
    buttons.children[0].classList.add('active')

    buttons.prepend(prev)
    buttons.append(next)

    if (page - maxButton < 1) buttons.removeChild(prev)
    if (page + maxButton > maxPage) buttons.removeChild(next)
}

const render = (page) => {
    renderContent(page)
    renderButton(page)
}

render(page)

JS에서 동적으로 생성했다고 했는데, 그리드뷰도 마찬가지다. render~에서 만드는거다.

그리드쪽 높이를 고정하지 않으면 저렇게 버튼이 올라간다. 그래도 페이지네이션 구현했지롱! 참고로 CSS에서 display를 그리드로 줬기 때문에 위치를 잘 계산한 다음 네모땡땡한 것이 grid에서 어느정도를 차지하는가를 지정할 수 있다. 그게 무슨 말이냐면, 어떤건 크고 어떤건 작고 이런 게 가능하다. (근데 내용이 짧으면 자동 채우기가 안된다는 단점이…)

테이블 페이지네이션

콘텐츠가 예전 게시판스타일대로 table 태그에 담겨져 있다. 그래서 동적으로 추가할 때 table 전체가 아니라 그 안에 있는 tr태그를 추가해야 한다. (table 안에 tr 안에 td)

<html>

<head>
    <meta charset="UTF-8" />
    <title>Pagenation</title>
    <link rel="stylesheet" href="style.css" />
    <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" rel="stylesheet"
        integrity="sha384-1BmE4kWBq78iYhFldvKuhfTAU6auU8tT94WrHftjDbrCEXSU1oBoqyl2QvZ6jIW3" crossorigin="anonymous">
    <script src="https://kit.fontawesome.com/dc58858c96.js" crossorigin="anonymous"></script>
</head>

<body>
    <div class="wrapper">
        <h1>integer quis auctor elit</h1>
        <p>Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore
            magna aliqua. Tellus molestie nunc non blandit. Adipiscing enim eu turpis egestas pretium aenean pharetra.
            Sed lectus vestibulum mattis ullamcorper velit sed.</p>
        <table class="table table-primary table-striped table-hover" id="table-body">
            <thead>
                <tr>
                    <th>ID</th>
                    <th>Title1</th>
                    <th>Title2</th>
                </tr>
            </thead>
            <tbody class="table-body">
                
                <tr class="table-row">
                    <td>1</td>
                    <td>integer</td>
                    <td>Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut
                        labore et dolore magna aliqua.</td>
                </tr>
            </tbody>
        </table>
        <nav aria-label="Page navigation example">
            <ul class="pagination justify-content-center">
                
            </ul>
        </nav>
    </div>

    <script src="script.js"></script>
    <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.bundle.min.js"
        integrity="sha384-ka7Sk0Gln4gmtz2MlQnikT1wXgYsOg+OMhuP+IlRH9sENBO0LRn5q+8nbTov4+1p"
        crossorigin="anonymous"></script>
</body>

</html>

테이블과 페이지 번호 그거는 부트스트랩꺼 가져왔는데, 이거 생각보다 힘든게 CSS가 안먹혀서 죄다 important 때렸다… 아무튼, 페이지 번호는 ul 안에 li태그 안에 a까지 있는 구조이고, table은 원래 thead tbody가 없었는데 th 추가하려고 넣었다. 우리가 동적으로 추가할 tr은 tbody에 들어간다.

@font-face {
    font-family: 'CookieRunOTF-Bold';
    src: url('https://cdn.jsdelivr.net/gh/projectnoonnu/noonfonts_twelve@1.0/CookieRunOTF-Bold00.woff') format('woff');
    font-weight: normal;
    font-style: normal;
}

* {
    margin:0;
    padding:0;
    font-family:'CookieRunOTF-Bold';
}

.wrapper {
    width:1280px;
    margin:0 auto;
}

table {
    margin: 15px auto;
}

.page-link {
    border:none!important;
}

.page-item.active {
    border-bottom: 3px solid #0d6efd;
}

.page-item.active .page-link {
    background-color:transparent!important;
    color:#0d6efd!important;
}

위에도 말했듯… 걍 CSS 줬더니 안먹혀서 important 때렸음… 그거 말고는 딱히 특이한 건 없다.

const contents = document.querySelector('.table-body')
const buttons = document.querySelector('.pagination')
const numOfContent = 120
const maxContent = 10
const maxButton = 5
const maxPage = Math.ceil(numOfContent / maxContent)
let page = 1

const makeContent = (id) => {
    const content = document.createElement('tr')
    content.classList.add('.table-row')
    content.innerHTML = `
        <td>${id}</td>
        <td>Title_Text ${id}</td>
        <td>Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut
            labore et dolore magna aliqua.</td>`
    return content
}

const makeButton = (id) => {
    const button = document.createElement('li')
    const link = document.createElement('a')
    button.classList.add('page-item')
    link.classList.add('page-link')
    button.dataset.num = id
    link.dataset.num = id
    button.appendChild(link)
    link.innerText = id
    link.href = '#'
    button.addEventListener("click", (e) => {
        Array.prototype.forEach.call(buttons.children, (button) => {
            if (link.dataset.num) button.classList.remove("active")
        })
        button.classList.add("active")
        renderContent(parseInt(e.target.dataset.num))
    })
    return button
}

const prevPage = () => {
    page -= maxButton
    render(page)
}

const nextPage = () => {
    page += maxButton
    render(page)
}

const prev = document.createElement('li')
prev.classList.add('page-item','prev')
prev.innerHTML = '<a class="page-link" href="#">Previous</a>'
prev.addEventListener('click', prevPage)

const next = document.createElement('li')
next.classList.add('page-item','next')
next.innerHTML = '<a class="page-link" href="#">Next</a>'
next.addEventListener('click',nextPage)

const renderContent = (page) => {
    while(contents.hasChildNodes()) {
        contents.removeChild(contents.lastChild)
    }
    for (let id = (page - 1) * maxContent + 1;id <= page * maxContent && id <= numOfContent;id++){
        contents.appendChild(makeContent(id))
    }
}

const renderButton = (page) => {
    while(buttons.hasChildNodes()) {
        buttons.removeChild(buttons.lastChild)
    }
    for (let id = page;id < page + maxButton && id <= maxPage;id++) {
        buttons.appendChild(makeButton(id))
    }
    buttons.children[0].classList.add('active')

    buttons.prepend(prev)
    buttons.append(next)

    if (page - maxButton < 1) buttons.removeChild(prev)
    if (page + maxButton > maxPage) buttons.removeChild(next)
}

const render = (page) => {
    renderContent(page)
    renderButton(page)
}

render(page)

버튼이 원래는 disabled 클래스가 따로 있었는데, 그거 그대로 따라가려고 했더니 그만… 페이지가 음수가 떠버린것이고… (주륵)

기본적인 거 말고 CSS 건드린 건 딱히 없다.