📜

[Gatsby] Infinite Scroll - 무한 스크롤 구현


어떻게 구현할까?

무한 스크롤을 구현하는 방법에는 크게 두가지가 있겠다.

  • Scroll Event
  • IntersectionObserver

Scroll Event는 말 그대로 DOM의 스크롤 이벤트로 '현재 스크롤 한 길이'와 '전체 페이지 길이'를 비교해서 추가적인 요소를 불러오는 방법이다.

IntersectionObserver는 타겟 요소를 정해서 그 요소가 화면에 보이게 되면 추가적인 요소를 불러오는 방법이다.

스크롤 이벤트를 사용해서 구현하면 훨씬 쉽게 구현할 수 있지만, 전체적인 스크롤에 반응하기 때문에 IntersectionObserver보다 성능적으로 비효율적이다. 특히 스크롤 이벤트에 쓰이는 documentElement.scrollTopdocumentElement.offsetHeightreflow를 일으켜서 성능상 좋지 않다.

reflow”는 모든 엘리먼트의 위치와 길이 등을 다시 계산하는 것으로 문서의 일부 혹은 전체를 다시 렌더링한다.

그래서 생소하더라도 IntersectionObserver를 사용해서 구현했다. API에 대한 자세한 내용은 여기를 참조하자.

IntersectionObserver로 구현

일단 흐름을 간단하게 생각해보자.

  1. target에 observer를 달아준다
  2. observer에서 intersect를 감지한다
  3. 추가적인 요소를 불러온다
  4. 2~3 계속 반복

흐름대로라면 제일 먼저 해야할 것은 타겟 요소에 observer를 달아주는 것이다. Observer를 컴포넌트에서 바로 달아주는 것도 가능하지만, custom hook으로 만들어주는게 더 이쁠 것 같다. 구현할 useInfiniteScroll.js hook은 콜백 함수option을 인자로 받아서 IntersectionObserver를 초기화 해준다.

1import { useState, useEffect, useCallback } from "react";
2
3const defaultOptions = {
4 root: null,
5 rootMargin: '1px',
6 threshold: '0.1',
7}
8
9export default function useInfiniteScroll(onIntersect, option = defaultOptions) {
10 const [ref, setRef] = useState(null);
11 // intersecting이 있을 때 콜백 함수 실행
12 const checkIntersect = useCallback(([entry], observer) => {
13 if (entry.isIntersecting) {
14 onIntersect(entry, observer);
15 }
16 }, []);
17 // ref나 option이 바뀔 경우 observer를 새로 등록한다.
18 useEffect(() => {
19 let observer;
20 if (ref) {
21 observer = new IntersectionObserver(checkIntersect, option);
22 observer.observe(ref);
23 }
24 return () => observer && observer.disconnect();
25 }, [ref, option, checkIntersect]);
26 // setRef를 넘겨주어서 ref를 변경시킬 수 있도록 한다.
27 return [ref, setRef];
28};

코드를 같이 읽어보자.

일단 타겟 요소를 저장하기 위해 ref state를 둔다. 이 Hook을 사용하는 컴포넌트에 setRef를 넘겨주어서 타겟 요소를 지정할 수 있게 한다. checkIntersect 함수는 타겟 요소의 가시성에 변화가 생기면 실행될 콜백 함수다. intersection이 있다면 인자로 넘어온 콜백 함수를 실행해준다. useEffect() 내부는 옵저버를 초기화하고 clean-up 해준다. setRef로 타겟 요소가 바뀌게 되면 옵저버를 새로 등록하게 된다.

컴포넌트에서 사용하기

훅이 완성됐으니 이제 사용해보자. 지금 블로그에 적용한 방법이다.

1const Posts = ({ posts }) => {
2 const [count, setCount] = useState(10);
3 const [ref, setRef] = useInfiniteScroll((entry, observer) => {
4 loadMorePosts();
5 });
6
7 function loadMorePosts() {
8 setCount(v => {
9 if (v + 1 <= posts.length) return v + 1;
10 else return v;
11 });
12 }
13
14 return (
15 <div>
16 {posts.slice(0, count).map(node => {
17 return <PostPreview key={node.slug} node={node} />;
18 })}
19 <div ref={setRef}/>
20 </div>
21 )
22}

count로 렌더링 될 요소의 개수를 지정한다. 그리고 콜백 함수로 count를 증가시켜 요소가 추가적으로 렌더링 될 수 있게 했다.

추가적으로 throttle 혹은 requestAnimationFrame으로 최적화를 해주면 이벤트의 빈도수를 조절할 수 있어 성능상 더 좋다고 한다. 하지만 아직 자바스크립트에 대한 이해가 부족하기 때문에 점차적으로 업데이트를 할 예정이다.

참조: