컨텐츠를 페이징 하는 기법 중 하나로,
아래로 스크롤 하다가 컨텐츠의 마지막 요소를 볼 즈음 다음 컨텐츠가 있으면 불러오는 방식이다.
Facebook, Twitter, Instagram 등 SNS에서 주로 사용된다.
구현 방식은 크게 두 가지가 있다.
Window의 scroll 이벤트를 통해 스크롤링이 일어날 때마다 화면 전체의 height와 스크롤 위치를 통해
스크롤이 컨텐츠 끝 즈음에 다다랐는지 체크해서 처리하는 방식
<!-- index.html -->
<html>
<head>
<title>무한 스크롤</title>
</head>
<body>
<main class="app"></main>
<script src="/src/main.js" type="module"></script>
</body>
</html>
// main.js
import App from "./App.js";
const $target = document.querySelector(".app");
new App({
$target,
});
// App.js
import PhotoList from "./PhotoList.js";
import { request } from "./api.js";
export default function App({ $target }) {
const $h1 = document.createElement("h1");
$h1.innerText = "Cat Photos";
$h1.style.textAlign = "center";
$target.appendChild($h1);
this.state = {
limit: 5,
nextStart: 0, // limit 갯수만큼 계속 더해짐
photos: [],
totalCount: 0,
isLoading: false,
};
const photoListComponent = new PhotoList({
$target,
initialState: {
isLoading: this.state.isLoading,
photos: this.state.photos,
totalCount: this.state.totalCount,
},
onScrollEnded: async () => {
await fetchPhotos();
},
});
this.setState = (nextState) => {
this.state = nextState;
photoListComponent.setState({
isLoading: this.state.isLoading,
photos: this.state.photos,
totalCount: this.state.totalCount,
});
};
const fetchPhotos = async () => {
this.setState({
...this.state,
isLoading: true,
});
const { limit, nextStart } = this.state;
const photos = await request(`/cat-photos?_limit=${limit}&_start=${nextStart}`);
this.setState({
...this.state,
nextStart: nextStart + limit,
photos: this.state.photos.concat(photos),
isLoading: false,
});
};
const initialize = async () => {
const totalCount = await request("/cat-photos/count");
this.setState({
...this.state,
totalCount,
});
await fetchPhotos();
};
initialize();
}
// PhotoList.js
export default function PhotoList({ $target, initialState, onScrollEnded }) {
let isInitailize = false;
const $photoList = document.createElement("div");
$target.appendChild($photoList);
this.state = initialState;
this.setState = (nextState) => {
this.state = nextState;
this.render();
};
this.render = () => {
if (!isInitailize) {
$photoList.innerHTML = `
<ul class="PhotoList__photos"></ul>
`;
isInitailize = true;
}
const { photos } = this.state;
const $photos = $photoList.querySelector(".photoList__photos");
photos.forEach((photo) => {
// photo의 id 기준으로 렌더링이 되어있는지 체크
if ($photos.querySelector(`[data-id="${photo.id}"]`) === null) {
// 없으면 li 생성하고 $photos에 appendChild
const $li = document.createElement("li");
$li.setAttribute("data-id", photo.id);
$li.style = "list-style:none";
$li.innerHTML = `<img width="100%" src="${photo.imagePath}" />`;
$photos.appendChild($li);
}
});
};
this.render();
window.addEventListener("scroll", () => {
const { isLoading, totalCount, photos } = this.state;
const isScrollEnded = window.innerHeight + window.scrollY + 100 >= document.body.offsetHeight;
// 스크롤이 맨 아래에 닿았을 때, 로딩 중이지 않을 때, 모든 데이터를 가져오기 전일 때
if (isScrollEnded && !isLoading && photos.length < totalCount) {
onScrollEnded();
}
});
}
현재 보고 있는 화면을 감시해서 노출이 되면 동작하는 방식
// PhotoList.js
export default function PhotoList({ $target, initialState, onScrollEnded }) {
let isInitailize = false;
const $photoList = document.createElement("div");
$target.appendChild($photoList);
this.state = initialState;
this.setState = (nextState) => {
this.state = nextState;
this.render();
};
const observer = new IntersectionObserver(
(entries) => {
entries.forEach((entry) => {
// 감지 되었고, 로딩 중이 아닐 때
if (entry.isIntersecting && !this.state.isLoading) {
console.log("화면 끝 감지", entry);
if (this.state.totalCount > this.state.photos.length) {
onScrollEnded();
}
}
});
},
{
threshold: 1, // 뷰포트에 완전히 감지가 된 경우에만 불러옴
}
);
let $lastLi = null;
this.render = () => {
if (!isInitailize) {
$photoList.innerHTML = `
<ul class="PhotoList__photos"></ul>
`;
isInitailize = true;
}
const { photos } = this.state;
const $photos = $photoList.querySelector(".photoList__photos");
photos.forEach((photo) => {
// photo의 id 기준으로 렌더링이 되어있는지 체크
if ($photos.querySelector(`[data-id="${photo.id}"]`) === null) {
// 없으면 li 생성하고 $photos에 appendChild
const $li = document.createElement("li");
$li.setAttribute("data-id", photo.id);
$li.style = "list-style:none; min-height: 500px;"; // 처음에 li가 딱 붙어있을 경우 예외 때문에 min-height을 지정
$li.innerHTML = `<img width="100%" src="${photo.imagePath}" />`;
$photos.appendChild($li);
}
});
const $nextLi = $photos.querySelector("li:last-child");
if ($nextLi !== null) {
if ($lastLi !== null) {
observer.unobserve($lastLi);
}
$lastLi = $nextLi;
observer.observe($lastLi);
}
};
this.render();
}