
![]()
오즈의 제작소 꿀팁 페이지는 무한 스크롤로 구현되어 있었다.
백엔드 API 변경 작업이 있던 중, 페이지네이션에서 page 시작이 0으로 바뀜에 따라서 기존 코드에서 동작하지 않던 것이다. 따라서 코드 수정이 필요했다.
위와 같은 이유로 Hook을 만들어서 공통적으로 사용하고자 하였다.
오즈의 제작소는 data fetch 라이브러리로 swr을 사용중에 있었다.
swr은 무한 스크롤을 위한 useSWRInfinite를 제공한다.
useSWRInfinite로 data를 받아오면 응답 값 전체 객체가 배열의 요소로 담기게 된다.
따라서 기존 API 응답 값을 그대로 사용하면 아래와 같이 담겨서 요소를 순회할 수 없게 된다.

useSWRInfinite에서 받아오는 data 값을 contents로 변경하면 아래와 같이 담겨서 순회가 가능하다.

useSWR에서 key 역할을 하는 getKey를 생성해서 useSWRInfinite에 전달해야 한다.
내가 만드는 Hook에서는 key 값을 인자로 받아온다. 따라서 뒤에 page만 변경해주었다.
const getKey = (pageIndex, previousPageData) => {
if (previousPageData && !previousPageData.length) return null;
return [`${key}&page=${pageIndex}`, pageIndex];
};
자세한 사용법은 공식 문서에 있으니 참고하면 좋겠다!
스크롤이 어느 정도 내려간 시점에 꿀팁 데이터를 불러오기 위해서는 scroll event를 사용하는 방법이 있다.
scroll event를 사용하는 경우, 스크롤이 내려갈 때마다 이벤트가 발생한다. 이는 성능 하락을 야기한다. 이를 예방하기 위해서는 debounce나 throttle을 사용할 수 있다.
debounce -> 이벤트에서 가장 마지막 또는 제일 처음 이벤트만 실행되도록 한다.
throttle -> 이벤트가 발생할 때마다 실행되도록 하는 것이 아니라, 일정 시간이 지나면 한번에 처리한다.
그러나 Intersection Observer API를 사용하면 debounce, throttle 없이 무한 스크롤을 구현할 수 있다.
const onIntersect = (entries) => {
entries.forEach((entry) => {
if (entry.isIntersecting) {
setSize((prev) => prev + 1);
}
});
};
useEffect(() => {
let observer;
if (target) {
observer = new IntersectionObserver(onIntersect, {
threshold: 0.5,
rootMargin: '50px',
});
observer.observe(target);
}
return () => observer && observer.disconnect();
}, [target]);
IntersectionObserver가 탐지하고 싶은 target이 등록된 경우에만 관찰을 시작한다.
해당 target이 화면에 일정 부분 노출된다면 useSWRInfinite의 setSize를 이전보다 1만큼 증가시켜 다음 data를 받아온다.
그렇다면 target을 어떻게 만드는지 궁금할 것이다. target은 관찰하고 싶은 대상의 ref에 setTarget을 넘겨주면 해당 Element를 가져온다.
const [target, setTarget] = React.useState(null);
.
.
.
<div ref={setTarget} />
Intersectoin Observer API가 아니라 react-intersection-observer 라이브러리를 활용하는 방법도 있다. useSWRInfinite를 사용하는 부분은 동일하나, 관찰하고 싶은 대상을 탐지하는 부분에서 차이가 난다.
import { useInView } from 'react-intersection-observer';
.
.
.
const [ref, inView] = useInView();
React.useEffect(() => {
if (!inView) return;
setSize((prev) => prev + 1);
}, [inView]);
Intersection Observer API를 사용한 것보다 코드 양이 줄었다.
Intersection Observer에 대해서 모른다면 간편하게 사용할 수 있을 것 같다.
만약 무한 스크롤을 꿀팁 페이지뿐 아니라 상품, 업체 페이지에서도 사용한다고 하면 꿀팁 페이지에서 작성된 무한 스크롤 코드를 복붙해야 한다.
그러나 위 코드를 Hook으로 뺀다면 data를 불러오고 target을 탐지하는 로직이 있기 때문에 사용하는 부분에서는 Hook의 요구 사항에 맞게 사용하기만 하면 된다.
Intersection Observer 버전과 react-intersection-observer 버전이 있지만, 공통적으로 key, api를 인자로 받고 data, error, setSize, ref or setTarget(ref를 설정할 수 있는 것)을 반환한다.
로직을 통합해서 Hook으로 뺀 결과물이다.
import { useEffect } from 'react';
import { useSWRInfinite } from 'swr';
const useInfiniteScroll = ({ key, api }) => {
const getKey = (pageIndex, previousPageData) => {
if (previousPageData && !previousPageData.length) return null;
return [`${key}&page=${pageIndex}`, pageIndex];
};
const { data, error, setSize } = useSWRInfinite(getKey, api);
const [target, setTarget] = useState(null);
const onIntersect = (entries) => {
entries.forEach((entry) => {
if (entry.isIntersecting) {
setSize((prev) => prev + 1);
}
});
};
useEffect(() => {
let observer;
if (target) {
observer = new IntersectionObserver(onIntersect, {
threshold: 0.5,
rootMargin: '50px',
});
observer.observe(target);
}
return () => observer && observer.disconnect();
}, [target]);
return {
data,
error,
setSize,
setTarget,
};
};
export default useInfiniteScroll;
import { useEffect } from 'react';
import { useSWRInfinite } from 'swr';
import { useInView } from 'react-intersection-observer';
const useInfiniteScroll = ({ key, api }) => {
const [ref, inView] = useInView();
const getKey = (pageIndex, previousPageData) => {
if (previousPageData && !previousPageData.length) return null;
return [`${key}&page=${pageIndex}`, pageIndex];
};
const { data, error, setSize } = useSWRInfinite(getKey, api);
useEffect(() => {
if (!inView) return;
setSize((prev) => prev + 1);
}, [inView]);
return {
data,
error,
setSize,
ref,
};
};
export default useInfiniteScroll;
key와 api만 인자로 전달하면 어디서든 사용 가능한 무한 스크롤 Hook을 만들어봤다.
만약 swr을 사용하지 않는다면 사용하는 data fetch 라이브러리에서 제공하는 스펙에 맞게 data 호출 부분만 수정하면 될 것이다.