[VanillaJS] infinity scroll로 data fetch하기

Chanho Kim·2021년 8월 31일
0

[vanillajs] 영화 앱

목록 보기
4/4

1. infinity scroll

데이터를 많이 부를 때 페이지 단위로 관리하는 pagination, 스크롤에 따라 데이터를 부르는 infinity scroll등 다양한 벙법을 사용합니다. 여기서는 IntersectionObserver를 활용해 스크롤에 따라 데이터를 부르는 infinity scroll을 구현 해보겠습니다.

2. IntersectionObserver

2-1. 정의

IntersectionObserver(교차 관찰자 API)는 타겟 엘레멘트와 타겟의 부모 혹은 상위 엘레멘트의 뷰포트가 교차되는 부분을 비동기적으로 관찰하는 API이다.

즉 내가 정한 target이 viewport(혹은 특정 element)안에 있는지 여부에 따라 callback함수를 실행해주는 api이다.

2-2. 사용법

option에서 root는 기본값이 viewport이며 특정 element로 설정 할 수 있다.

let options = {
  root: document.querySelector('#scrollArea'),
  rootMargin: '0px',
  threshold: 1.0
}

let observer = new IntersectionObserver(callback, options);

IntersectionObserver를 생성 후 target element를 observe하면 해당 element가 viewport에 있는지의 여부를 비동기로 확인 후 callback함수를 실행한다.

let target = document.querySelector('#listItem');
observer.observe(target);

위의 api를 활용해 영화 리스트의 맨 마지막 부분이 viewport안에 있으면 api를 호출해 데이터를 추가한다.

3. 구현

3-1. 기본 구조

├── index.html
└── src
    ├── App.js
    ├── main.js
    ├── api
    │   └── movieApi.js
    ├── components
    │   └── ScrollView.js	//추가
    ├── css
    │   └── style.css		//추가
    └── util
        └── InfinityScroll.js	//추가

3-2. style.css

간단한 스타일 작업을 해줍니다.

body {
  height: 100vh;
}

.container {
  display: flex;
  flex-wrap: wrap;
  justify-content: flex-start;
  flex: 1;
}

.block {
  width: 250px;
  height: 350px;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  margin: 3px 0;
  margin-left: calc((100% - (20% * 4)) / 4);
  border: 1px solid black;
  font-size: 10px;
}

.block > img {
  width: 200px;
  height: 280px;
}

3-3. index.html

만들어준 css파일을 연결해줍니다.

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1" />
    <title>JS STUDY</title>
    <link rel="stylesheet" href="./src/css/style.css" />
    <script type="module" src="./src/main.js"></script>
  </head>
  <body>
    <div class="app"></div>
  </body>
</html>

3-4. InfinityScroll.js

callback 함수의 첫번째 인자값은 현재 observe중인 target들입니다. 이 target의 isIntersecting값이 true인 경우 해당 target이 viewport에 있다고 생각하면 됩니다. 이를 활용해서 target이 target.isIntersecting === true 인 경우 즉, viewport에 있는 경우 데이터를 불러옵니다. 또한 callback함수의 두번째 인자값은 생성되는 IntersectionObserver를 뜻합니다. 데이터를 불러올 때 해당 target에 대한 관찰을 unobserve 해줍니다.

const InfinityScroll = (fetchData) => {
  return new IntersectionObserver((entires, observer) => {
    entires.forEach((entry) => {
      if (entry.isIntersecting) {
        fetchData();
        observer.unobserve(entry.target);
      }
    });
  }, {});
};

export default InfinityScroll;

3-5. movieApi.js

yts api는 page기능을 제공해줍니다. api호출 시 page값을 넘겨줘서 새로운 데이터를 받아 올 수 있도록 해줍니다.

...
const api = {
  getMovies: async (page) => {
    try {
      const movies = await request(
        `https://yts.mx/api/v2/list_movies.json?limit=15&page=${page}`
      );
      return {
        isError: false,
        data: movies?.data?.movies,
      };
    } catch (e) {
      return {
        isError: true,
        data: e,
      };
    }
  },
};

export { api };

3-6. ScrollView.js

이전에 만들어둔 Hello component는 삭제하고 새로운 ScrollView component를 만들어 줍니다. 기본 틀은 Hello component와 비슷하나, 위에서 만든 InfinityScroll.js를 활용해 Scroll에 따라 fetchData함수를 호출해줍니다.

먼저 constructor에서 만들어 둔 InfinityScroll.js의 함수를 통해 IntersectionObserver를 생성해줍니다.

...
constructor({ $target, data, fetchData }) {
  this.section = document.createElement("section");
  this.section.classList.add("container");
  this.data = [];
  fetchData();
  this.intersection = InfinityScroll(fetchData);
  $target.appendChild(this.section);
}
...

이후 setIntersection함수를 만들어 줍니다. 해당 함수는 불러온 데이터 중 가장 마지막 데이터를 가지고 있는 element를 위에서 만들어준 IntersectionObserver에 observe하는 역할을 합니다.

setIntersection() {
  const blocks = document.querySelectorAll(".block");
  this.intersection.observe(blocks[blocks.length - 1]);
}

ScrollView.js

import InfinityScroll from "../util/InfinityScroll.js";

export default class ScrollView {
  constructor({ $target, fetchData }) {
    this.section = document.createElement("section");
    this.section.classList.add("container");
    this.data = [];
    fetchData();
    this.intersection = InfinityScroll(fetchData);
    $target.appendChild(this.section);
  }

  setData(data) {
    this.data = data;
    this.render();
  }

  setIntersection() {
    const blocks = document.querySelectorAll(".block");
    this.intersection.observe(blocks[blocks.length - 1]);
  }

  render() {
    this.section.innerHTML = "";
    this.data.forEach((val, idx) => {
      const block = document.createElement("div");
      block.classList.add("block");

      const title = document.createElement("h1");
      title.innerText = val.title;
      
      const image = document.createElement("img");
      image.src = val.medium_cover_image;
      block.appendChild(title);
      block.appendChild(image);      
      this.section.appendChild(block);
    });
    this.setIntersection();
  }
}

3-7. app.js

기존에 만들어둔 Hello component대신 ScrollView component를 만들어줍니다. 이때 api에서 데이터를 불러올 때 page값을 넘겨줍니다.

import ScrollView from "./components/ScrollView.js";
import { api } from "./api/movieApi.js";

export default class App {
  constructor($target) {
    let page = 1;
    const scrollView = new ScrollView({
      $target,
      fetchData: async () => {
        const response = await api.getMovies(page);
        page += 1;
        if (!response.isError) {
          const newData = [...scrollView.data, ...response.data];
          scrollView.setData(newData);
        } else {
          //에러처리하기
        }
      },
    });
  }
}

3-8. 실행

아래 명령어로 실행 후 http://localhost:8080 에 가서 결과물을 확인해봅니다.

npx http-server ./


스크롤에 따라 데이터가 잘 불러와 지는지 확인해 봅니다.

참고 자료

https://developer.mozilla.org/ko/docs/Web/API/Intersection_Observer_API
https://github.com/woohyeonjo/ilovecat-javascript
https://yts.mx/api

profile
호기심쟁이 FE개발자 김찬호입니다!

0개의 댓글