💻 Frontend
IntersectionObserver 로 무한스크롤 구현하기
category
💻 Frontend
IntersectionObserver APIcallbackentries: IntersectionObserverEntry[ ]observeroption예제1. HTML + JS예제 설명2. React3. 실제 프로젝트 적용 Next + TypeScript참고자료
JS에서 제공해주는 API인
IntersectionObserver
로 무한스크롤을 구현해보았습니다.IntersectionObserver API
IntersectionObserver API는 두가지 인자를 가집니다.
let observer = new IntersectionObserver(callback, options);
callback
첫번째 인자는
callback
으로, 관찰할 대상에 변화가 생기면 callback함수를 실행합니다. entries
observer
인자를 가집니다.let callback = (entries, observer) => { entries.forEach((entry) => { // Each entry describes an intersection change for one observed // target element: // entry.boundingClientRect // entry.intersectionRatio // entry.intersectionRect // entry.isIntersecting // entry.rootBounds // entry.target // entry.time }); };
entries: IntersectionObserverEntry[ ]
- boundingClientRect: 관찰 대상의 사각형 정보 (DOMRectReadOnly)
- intersectionRatio: 관찰 대상의 교차한 영역 정보 (DOMRectReadOnly)
- intersectionRect: 관찰 대상의 교차한 영역 백분율(
intersectionRect
영역에서boundingClientRect
영역까지 비율)(Number)
- isIntersecting: 관찰 대상의 교차 상태 (Boolean)
- rootBounds: 지정한 root 요소의 사각형 정보 (DOMRectReadOnly), 이는 옵션
rootMargin
에 의해 값이 변경되며, 만약 별도의 루트 요소(옵션root
)를 선언하지 않았을 경우null
을 반환합니다.
- target: 관찰 대상 요소 (Element)
- time: 변경이 발생한 시간 정보 (DOMHighResTimeStamp)
observer
콜백이 실행되는 해당 인스턴스를 참조합니다.
const io = new IntersectionObserver((entries, observer) => { console.log(observer) }, options) io.observe(element)
option
두번째 인자는
option
으로, callback
함수가 호출되는 상항을 제어할 수 있습니다.let options = { root: document.querySelector('#scrollArea'), rootMargin: '0px', threshold: 1.0 }
- root: 이는 대상 객체의 조상 요소여야하며, 기본값은 브라우저 뷰포트입니다.
- rootMargin: root가 가진 여백이며, 기본값은 0입니다. 값이 +이면 부모 크기가 커지는 것이고 -이면 부모 크기가 작아지는 것이라고 생각하면 됩니다.
- threshold: observer의 콜백이 실행될 대상 요소의 가시성 퍼센티지를 나타내는 단일 숫자 혹은 숫자 배열입니다. 만약 0.5로 설정해두었다면 요소가 0.5% 보여졌을 때 callback이 실행됩니다. 기본값은 0입니다. 이는 요소가 1픽셀이라도 보이자 마자 콜백이 실행됨을 의미합니다. 1은 요소가 다 보여지면callback을 실행한다는 것입니다.
예제
1. HTML + JS
<div class="card-container"> <div class="card">This is the first card</div> <div class="card">This is a card</div> <div class="card">This is a card</div> <div class="card">This is a card</div> <div class="card">This is a card</div> <div class="card">This is a card</div> <div class="card">This is a card</div> <div class="card">This is a card</div> ~~~ <div class="card">This is the last card</div> </div>
---
.card-container { display: flex; flex-direction: column; gap: 1rem; align-items: flex-start; } .card { background-color: #fff; border: 1px solid #000; border-radius: 0.25rem; padding: 0.5rem; transform: translateX(100px); opacity: 0; transition: 150ms; } .card.show { transform: translateX(0); opacity: 1; }
---
const cardContainer = document.querySelector(".card-container"); const cards = document.querySelectorAll(".card"); const observer = new IntersectionObserver( (entries) => { entries.forEach((entry) => { entry.target.classList.toggle("show", entry.isIntersecting); }); } ); cards.forEach((card) => { observer.observe(card); }); const lastCardObserver = new IntersectionObserver((entries) => { if (!entries[0].isIntersecting) return; loadNewCards(); lastCardObserver.observe(document.querySelector(".card:last-child")); }); lastCardObserver.observe(document.querySelector(".card:last-child")); function loadNewCards() { for (let i = 0; i < 10; i++) { const card = document.createElement("div"); card.textContent = "New Card"; card.classList.add("card"); observer.observe(card); cardContainer.append(card); } }
예제 설명
모든 카드들은 관찰 대상이 됩니다.
cards.forEach((card) => { observer.observe(card); });
카드들이 보일 때 마다 콜백함수가 실행됩니다.
const observer = new IntersectionObserver( (entries) => { entries.forEach((entry) => { entry.target.classList.toggle("show", entry.isIntersecting); }); } );
entry.isIntersecting
이 true이면 classList을 show로 바꿔주어 요소를 보여지게 합니다.
무한스크롤 되게하려면 스크롤을 마지막까지 했는지 감지해야합니다.
그래서 제일 마지막 카드를 관찰합니다.lastCardObserver.observe(document.querySelector(".card:last-child"));
마지막 카드가 보이면
loadNewCards
함수를 실행시켜 새로운 카드 10개를 추가합니다.const cardContainer = document.querySelector(".card-container"); const lastCardObserver = new IntersectionObserver((entries) => { if (!entries[0].isIntersecting) return; loadNewCards(); // 새로운 카드 추가 lastCardObserver.observe(document.querySelector(".card:last-child")); }); function loadNewCards() { for (let i = 0; i < 10; i++) { const card = document.createElement("div"); card.textContent = "New Card"; card.classList.add("card"); observer.observe(card); cardContainer.append(card); } }
이 과정이 반복되어 무한으로 스크롤할 수 있습니다!!
2. React
book들을 나열하고,
// book을 나열 시키는 코드 {books.map((book, i) => { if (books.length === i + 1) { return ( <div key={i} ref={lastBookElementRef}> {book} </div> ); } else { return <div key={i}>{book}</div>; } })}
그 중 마지막 book에 useRef로 DOM 접근을 합니다.
if (books.length === i + 1) { return ( <div key={i} ref={lastBookElementRef}> {book} </div> ); }
이 마지막 요소가 보인다면 새로운 요청을 보내야 하기 때문에 callback 함수를 생성해줍니다.
// 마지막 book Element const lastBookElementRef = useCallback( (node) => { // loading 시에는 실행 X if (loading) return; if (observer.current) observer.current.disconnect(); // observe가 호출되면 아래 콜백함수가 실행됨 observer.current = new IntersectionObserver((entries) => { // 마지막 요소가 보이고 book이 더 있다면 if (entries[0].isIntersecting && hasMore) { setPageNumber((prevPageNumber) => prevPageNumber + 1); } }); // node가 존재하면 observe 호출 if (node) observer.current.observe(node); }, [loading, hasMore] );
- 무한스크롤이 되어야 하는 영역에서 빈 div를 생성한다.
- 그 div에 ref를 지정해준다.
- ref에 IntersectionObserver를 지정해주고 그 요소가 보이면 일어나야 하는 로직을 작성해준다.
3. 실제 프로젝트 적용 Next + TypeScript
영화가 나열되는 부분 맨 아래 div를 생성하여 useRef로 DOM에 접근합니다.
const lastMovieElementRef = useRef<HTMLDivElement>(null); <> {movies?.map(movie => ( <Movie key={movie.id} movie={movie} /> ))} <div ref={lastMovieElementRef}></div> </>
useEffect로 요소에 변화가 있는지 감지합니다.
useEffect(() => { if (!lastMovieElementRef.current || movies?.length >= 100) return; const io = new IntersectionObserver((entries, observer) => { if (entries[0].isIntersecting) { getMovies(); } }); io.observe(lastMovieElementRef.current); return () => io.disconnect(); }, [getMovies, movies?.length]);
io.observe(lastMovieElementRef.current);
lastMovieElementRef가 보인다면 callback 함수를 실행시킵니다.
const io = new IntersectionObserver((entries, observer) => { if (entries[0].isIntersecting) { getMovies(); } });
영화를 불러와 movie 데이터를 업데이트 해주고 그 다음 요청을 하기 위해
pageNumber
를 증가시킵니다.const pageNumber = useRef<number>(1); const getMovies = useCallback(async () => { try { const { data }: MovieDataType = await axios.get( `${process.env.BASE_URL}/${id}/similar?api_key=${process.env.API_KEY}&page=${pageNumber.current}`, ); setMovies((prevMovies: MovieType[]) => [...prevMovies, ...data.results]); pageNumber.current += 1; } catch (e) { console.log(e); } }, []);
아래와 같이 if문에 조건을 추가해 movies 개수가 100개 넘으면 요청을 그만하는 식으로도 변경할 수 있습니다.
if (!lastUpcomingMovieElementRef.current || movies.length >= 100) return;