
IntersectionObserver는 두 영역의 교차를 관찰한다. new IntersectionObserver() 를 통해 관찰자를 초기화 하고 관찰할 대상을 지정한다.
/* 관찰자 초기화 */
const observer = new IntersectionObserver(callback, options);
/* 관찰 대상 등록 */
observer.observe(element);
관찰할 대상이 등록되거나 가시성에 변화가 감지되면 관찰자는 콜백을 실행한다. 콜백은 2개의 인수를 갖는다.
const callback = (entries, observer) => {
if (entries[0].isIntersecting) {
console.log('Intersecting@@@');
}
};
IntersectionObserverEntry 인스턴스의 배열이다. 다음 읽기 전용 속성들을 포함한다.
observer.unobserve(target);
/* callback */
const callback = (entries, observer) => {
if (entries[0].isIntersecting)
observer.unobserve(entries[0].target);
};
observer.observe(target);
observer.unobserve(target);
/* callback */
const callback = (entries, observer) => {
if (entries[0].isIntersecting)
observer.unobserve(entries[0].target);
};
observer.disconnect();
(Next.js로 구현되어 있음)
백은 page 번호를 받아 페이지 당 보내줄 데이터만큼의 데이터를 뽑아 보내준다.
1 page ⇒ 1 ~ 7 까지의 데이터
2 page ⇒ 8 ~ 14 까지의 데이터
프론트는 리스트의 마지막 요소 박스가 리트스 박스와 교차할 때 (스크롤이 현재 페이지에서 불러온 데이터의 마지막에 다다랐을 때) page를 증가 시켜 다음 페이지의 데이터를 불러오도록 한다.
page가 증가되면 해당 페이지의 데이터를 요청한다.
/* pages/api/users/index.js */
const USERS = Array.from({ length: 100 }, (_, idx) => ({
id: idx + 1,
name: `User${idx + 1}`,
});
const CNT = 7;
const LAST_PAGE = Math.ceil(USERS.length / CNT);
const users = (req, res) => {
const page = Number(req.query.page);
const sIdx = (page - 1) * CNT;
const eIdx = sIdx + CNT;
res.status(200).json({
data: USERS.slice(sIdx, eIdx),
totalPage: LAST_PAGE
});
};
export default users;
/* pages/infinite/index.js */
const Infinite = () => {
const [page, setPageInfo] = useState({ page: 1, totalPage: null });
const [users, setUsers] = useState([]);
const targetRef = useRef(null);
return (
<div>
<h2>Infinite Example</h2>
<ul id='container'>
{users.map((user, idx) => (
<li
key={user.id}
ref={users.length - 1 === idx ? targetRef : null}
>
{user.id}: {user.name}
</li>
))}
</ul>
<style jsx>{`
ul {
list-style: none;
padding-left: 0;
height: 420px;
border: 1px solid black;
overflow: scroll;
}
li {
border: 1px solid red;
height: 60px
}
`}</style>
);
};
export default Infinite;
/* 마지막 데이터에 다다랐을 때 page 1 증가 */
const intersectionHandler = (enteries, observer) => {
if (entries[0].isIntersecting) {
console.log('intersecting@@@');
observer.unobserve(entries[0].target);
if (pageInfo.page < pageInfo.totalPage) {
setPageInfo((prevPageInfo) => ({
...prevPageInfo,
page: prevPageInfo.page + 1),
}));
}
}
};
useEffect(() => {
const $container = document.getElementById('container');
const option = {
root: $container,
threshold: 1,
};
const observer = new IntersectionObserver(intersectionHandler, option);
if (targetRef.current)
observer.observe(targetRef.current)
}, [users]);
/* page가 증가하면 새로운 데이터 요청 */
useEffect(() => {
**const controller = new AbortController(); // 🚀
const { signal } = controller; // 🚀**
axios.get(
`${process.env.NEXT_PUBLIC_API_URL}api/users?page=${pageInfo.page}`,
**{ signal } // 🚀**
)
.then((res) => res.data)
.then((data) => {
console.log('data > ', data);
setUsers((prevUsers) => [...prevUsers, ...data.data]);
setPageInfo((prevPageInfo) => ({
...prevPageInfo,
totalPage: data.totalPage,
}));
})
.catch(console.error);
**return () => controller.abort(); // 🚀**
}, [pageInfo.page);
AbortController 인터페이스는 하나 이상의 웹 요청을 취소할 수 있게 해준다. 생성자로 새로운 AbortController 객체 인스턴스를 생성하고 요청을 취소하는데 사용되는 signal 객체 인터페이스를 얻는다. 그리고 이 signal을 요청을 보낼 때 옵션으로 넘겨준다. abort() 는 요청이 완료되기 전에 취소시킨다.
🚀 표시가 있는 부분은 아주아주아주 중요하다. React든 Next든 개발환경은 Strict Mode로 동작하는데 useEffect가 두 번씩 탄다. 그래서 자꾸 첫 페이지의 데이터가 배열에 두 번씩 들어가고 페이지가 두 번씩 증가하고…
왜 그러는가 살펴보면 일단 Strict Mode이기 때문에 useEffect가 두 번 타게된다. 그럼 데이터를 요청하는 비동기 함수가 백그라운드로 두 번 넘어간다. 이걸 막을 수 있는 방법이 AbortController를 사용하는 것이다.
useEffect와 cleanup 함수에서 콘솔로 찍고 확인해보면 다음과 같이 출력된다.

useEffect가 처음 탈 때 백그라운드로 넘어간 비동기 요청이 unmount 되면서 취소된다. (cleanup 함수에서 abort 시켰으므로) 그리고 두 번째로 useEffect를 탈 때는 unmount가 되지 않으니 요청이 정상적으로 보내졌다.
즉, AbortController와 cleanup 함수를 이용해 첫 번째로 타는 useEffect에서 보내는 요청을 취소 시킨것이다. useEffect에서 비동기 요청을 할 때는 꼭 이렇게 사용하자!
StrictMode 끄기 ㄴㄴ
import { useState, useEffect, useRef } from 'react';
import axios from 'axios';
import NavBar from '../../components/NavBar';
const Infinite = () => {
const [pageInfo, setPageInfo] = useState({ page: 1, totalPage: null });
const [users, setUsers] = useState([]);
const targetRef = useRef(null); // 현재 페이지의 마지막 데이터에 걸어줄 ref
/* 관찰 대상 등록 혹은 가시성 변화 감지 시 실행할 콜백 함수 */
const intersectionHandler = (entries, observer) => {
if (entries[0].isIntersecting) {
console.log('intersecting@@@');
observer.unobserve(entries[0].target); // 교차되면 관찰 종료
if (pageInfo.page < pageInfo.totalPage) // 마지막 페이지가 아니면 페이지 증가
setPageInfo((prevPageInfo) => ({
...prevPageInfo,
page: prevPageInfo.page + 1,
}));
}
};
/* 페이지 증가되면 다음 데이터 요청 */
useEffect(() => {
const controller = new AbortController();
const { signal } = controller;
axios
.get(
`${process.env.NEXT_PUBLIC_API_URL}api/users?page=${pageInfo.page}`,
{ signal }
)
.then((res) => res.data)
.then((data) => {
console.log('data > ', data);
setUsers((prevUsers) => [...prevUsers, ...data.data]);
setPageInfo((prevPageInfo) => ({
...prevPageInfo,
totalPage: data.totalPage,
}));
})
.catch(console.error);
return () => controller.abort(); // 잊지 말자
}, [pageInfo.page]);
/* 새로운 데이터를 받으면 마지막 요소 관찰 */
useEffect(() => {
const $container = document.getElementById('container');
const option = {
root: $container,
threshold: 1,
};
const observer = new IntersectionObserver(intersectionHandler, option);
if (targetRef.current) observer.observe(targetRef.current);
}, [users]);
return (
<div>
<NavBar />
<h2>Infinite Example</h2>
<ul id='container'>
{users.map((user, idx) => (
<li
key={user.id}
ref={users.length - 1 === idx ? targetRef : null}
>
{user.id}: {user.name}
</li>
))}
</ul>
<style jsx>{`
ul {
list-style: none;
padding-left: 0;
height: 420px;
border: 1px solid black;
overflow: scroll;
}
li {
border: 1px solid red;
height: 60px;
}
`}</style>
</div>
);
};
export default Infinite;