221103 TIL 바닐라 자바스크립트로 인피니티 스크롤 구현하기

오늘 한 일

  • 바닐라 자바스크립트로 인피니티 스크롤 실습

🔎 인피니티 스크롤 적용 화면

Nov-03-2022 22-34-46

  • 게시글 데이터는 페이지네이션이 가능한 더미 데이터를 제공 해주는 JSONPlaceholder에서 가져오도록 한다.

데이터를 5개씩 보여주기

가져온 데이터를 DOM에 보여주는 함수를 작성한다.

async function showPosts() {
    const posts = await getPosts();

    posts.forEach(post => {
       const postEl = document.createElement('div');
       postEl.classList.add('post');
       postEl.innerHTML = `
        <div class="number">${post.id}</div>
        <div class="post-info">
            <h2 class="post-title">${post.title}</h2>
            <p class="post-body">${post.body}</p>
        </div>
       `
       postsContainer.appendChild(postEl);
    });
}
  • classList.add('className') : classList는 css class의 현재 값을 반환하는 읽기 전용 프로퍼티이다.
    • add(), remove() 메서드로 class를 추가하거나 삭제할 수 있다.

데이터 가져오기 & 로딩 바

데이터를 불러오는 동안 로딩 바를 불러오는 함수를 생성한다.

// fetch 되는동안 로딩  
function showLoading() {
    loading.classList.add('show');

    setTimeout(() => {
        loading.classList.remove('show');

        setTimeout(() => {
            page = page + 1;
            showPosts();
        }, 300);
    }, 1000);
}

크롤이 바닥에 닿았을 때 데이터 호출

window.addEventListener('scroll', () => {
    const { scrollTop, scrollHeight, clientHeight } = document.documentElement;

    if(scrollHeight - scrollTop === clientHeight) {
        showLoading();
    }
});
  • scrollHeight : overflow로 화면에 보이지 않는 콘텐츠를 포함하여 요소의 높이를 측정한다.
  • scrollTop : y축으로 스크롤한 거리를 측정한다.
  • clientHeight : 요소 내부의 높이이다.

그림으로 표현하면 다음과 같다.

Untitled

간단하게 인피니트 스크롤을 구현할 수 있다.

전체 소스코드 보기

/index.html

<!DOCTYPE html>
<html lang="ko">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Home</title>
    <link rel="stylesheet" href="style.css"/>
</head>
<body>
    <h1>Home</h1>

    <div class="filter-container">
        <input type="text" id="filter" class="filter" placeholder="검색">
    </div>

    <div id="posts-container"></div>

    <div class="loader">
        <div class="circle"></div>
        <div class="circle"></div>
        <div class="circle"></div>
    </div>

    <script src="script.js"></script>
</body>
</html>

/script.js

const postsContainer = document.getElementById('posts-container');
const loading = document.querySelector('.loader');
const filter = document.getElementById('filter');

let limit = 5;
let page = 1;

// API 호출
async function getPosts() {
    const res = await fetch(
        `https://jsonplaceholder.typicode.com/posts?_limit=${limit}&_page=${page}`
    );
    
    const data = await res.json();

    return data;
}

// 가져온 Data를 DOM에 보여주기
async function showPosts() {
    const posts = await getPosts();

    posts.forEach(post => {
       const postEl = document.createElement('div');
       // classList는 css class의 현재 값을 반환하거나, 메서드를 사용하여 추가, 삭제 등의 작업을 할 수 있다. 
       postEl.classList.add('post');
       postEl.innerHTML = `
        <div class="number">${post.id}</div>
        <div class="post-info">
            <h2 class="post-title">${post.title}</h2>
            <p class="post-body">${post.body}</p>
        </div>
       `
       postsContainer.appendChild(postEl);
    });
}

// fetch 되는동안 로딩  
function showLoading() {
    loading.classList.add('show');

    setTimeout(() => {
        loading.classList.remove('show');

        setTimeout(() => {
            page = page + 1;
            showPosts();
        }, 300);
    }, 1000);
}

// 입력 값 검색
function filterPosts(e) {
    const term = e.target.value.toUpperCase();
    const posts = document.querySelectorAll('.post');
  
    posts.forEach(post => {
      const title = post.querySelector('.post-title').innerText.toUpperCase();
      const body = post.querySelector('.post-body').innerText.toUpperCase();
  
      if (title.indexOf(term) > -1 || body.indexOf(term) > -1) {
        post.style.display = 'flex';
      } else {
        post.style.display = 'none';
      }
    });
}
  
showPosts();

window.addEventListener('scroll', () => {
    const { scrollTop, scrollHeight, clientHeight } = document.documentElement;

    if(scrollHeight - scrollTop === clientHeight) {
        showLoading();
    }
});

filter.addEventListener('input', filterPosts);

/style.css

@import url('https://fonts.googleapis.com/css?family=Roboto&display=swap');

* {
  box-sizing: border-box;
}

body {
  background-color: #296ca8;
  color: #fff;
  font-family: 'Roboto', sans-serif;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  min-height: 100vh;
  margin: 0;
  padding-bottom: 100px;
}

h1 {
  margin-bottom: 0;
  text-align: center;
}

.filter-container {
  margin-top: 20px;
  width: 80vw;
  max-width: 800px;
}

.filter {
  width: 100%;
  padding: 12px;
  font-size: 16px;
}

.post {
  position: relative;
  background-color: #4992d3;
  box-shadow: 0 2px 4px rgba(0, 0, 0, 0.3);
  border-radius: 3px;
  padding: 20px;
  margin: 40px 0;
  display: flex;
  width: 80vw;
  max-width: 800px;
}

.post .post-title {
  margin: 0;
}

.post .post-body {
  margin: 15px 0 0;
  line-height: 1.3;
}

.post .post-info {
  margin-left: 20px;
}

.post .number {
  position: absolute;
  top: -15px;
  left: -15px;
  font-size: 15px;
  width: 40px;
  height: 40px;
  border-radius: 50%;
  background: #fff;
  color: #296ca8;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 7px 10px;
}

.loader {
  opacity: 0;
  display: flex;
  position: fixed;
  bottom: 50px;
  transition: opacity 0.3s ease-in;
}

.loader.show {
  opacity: 1;
}

.circle {
  background-color: #fff;
  width: 10px;
  height: 10px;
  border-radius: 50%;
  margin: 5px;
  animation: bounce 0.5s ease-in infinite;
}

.circle:nth-of-type(2) {
  animation-delay: 0.1s;
}

.circle:nth-of-type(3) {
  animation-delay: 0.2s;
}

@keyframes bounce {
  0%,
  100% {
    transform: translateY(0);
  }

  50% {
    transform: translateY(-10px);
  }
}


출처