오랜만에 무한스크롤링을 구현해보려고 합니다.
실무에서 무한스크롤링로 이미지를 계속해서 로딩하다보니, 성능적인 이슈가 발생했는데요.
비슷하게 기능을 구현하여 성능 이슈를 해결해보고 싶었습니다..!
그전에... 무한스크롤링을 구현해보기로 했습니다.
이미지를 가져오는 API가 필요했는데요.
Unsplash API를 사용해보기로 했습니다.
Unsplash API 사용하기

Unspash는 개발자를 위한 문서를 제공해주고 있는데요.
Pagination도 구현되어 있어서 페이징으로 Unspash 이미지를 가져올 수 있습니다.

API 요청 횟수는 제한되어 있습니다.
demo 모드에서는 한 시간에 50회 요청으로 제한됩니다.
더 많은 요청을 원한다면, 유료로 플랜을 바꾸거나 Unsplash에 문의가 필요할 듯합니다.

API 응답 헤더를 보면 X-Ratelimit-Remaining 헤더가 보이는데요.
이 헤더값을 통해 잔여 API 요청 횟수를 알 수 있습니다.
Tanstack-Query의 useInfiniteQuery 로직
const { data, fetchNextPage, hasNextPage } = useInfiniteQuery<
UnsplashPhoto[]
>({
queryKey: ["unsplash-photos"],
initialPageParam: 1,
queryFn: ({ pageParam = 1 }) =>
fetch(`https://api.unsplash.com/photos?page=${pageParam}&per_page=5`, {
headers: {
Authorization: `Client-ID ${process.env.NEXT_PUBLIC_UNSPLASH_ACCESS_KEY}`,
},
}).then((res) => res.json()),
getNextPageParam: (lastPage, allPages) => {
if (lastPage.length === 0) return undefined;
return allPages.length + 1;
},
staleTime: 1000 * 60 * 24,
refetchOnWindowFocus: false,
});
useInfiniteQuery를 이용해서 위와 같이 무한스크롤링 로직을 구현해볼 수 있습니다.
queryFn에 fetch를 이용해서 API 호출 함수를 구현할 수 있습니다.
Unsplash API는 한 시간에 API 요청횟수 제한이 있어서, staleTime을 한 시간으로 잡았습니다.
Unspash API는 Access 키를 넣어야 동작하기 때문에
계정을 생성하신 후에, Access 키를 확인하시고 API 요청 헤더에 Authorization에 올바른 값을 넣어주시면 됩니다.

다음 페이지를 언제 호출하면 좋을까요?
스크롤 이벤트를 이용해서 스크롤이 하단에 위치했을 때를 계산해서 구현하는 방법도 있을 거에요.
그것보다 더 쉬운 방법은 Intersection Observer API를 사용하면 됩니다.
관찰하는 대상이 화면에 노출되었을 때, 특정로직을 수행할 수 있습니다.
위의 그림을 직접 그려봤는데요.
저는 이미지들을 담을 컨테이너의 맨 밑에 요소를 하나 추가해서
이 요소가 노출될 때 다음 페이지의 데이터를 가져오도록 처리했습니다.
/**
* 페이지 컴포넌트 내부
*/
// 이미지 컨테이너 맨밑의 요소를 저장 (관찰대상)
const scrollEndRef = useRef<HTMLDivElement>(null);
...
// Intersection Observer로 다음 페이지를 호출하는 로직 구현
useEffect(() => {
if (!scrollEndRef.current) return;
if (!hasNextPage) return;
const observer = new IntersectionObserver(
(entries) => {
if (entries[0].isIntersecting && hasNextPage) {
fetchNextPage();
}
},
{
root: scrollEndRef.current.parentElement,
rootMargin: "0px 0px 100px 0px",
}
);
observer.observe(scrollEndRef.current);
return () => {
observer.disconnect();
};
}, [scrollEndRef.current, hasNextPage]);
여기서 끝이 아니다. 이미지 지연 대응하기
이미지를 가져올 API를 붙였고, 다음 페이지의 이미지도 잘 들고 옵니다.
그런데, 여기서 끝이 아닙니다.
이미지가 늦게 렌더링될 수도 있습니다.
여러가지 원인들이 있겠지만 아래와 같은 이유들이 있을 것입니다.
- 느린 네트워크 환경
- 성능이 낮은 기기
- 이미지 파일크기가 큰 경우
개발자 도구를 이용해서, CPU 성능을 x4 감속했고, 네트워크 속도도 빠른 4G로 낮춰봤습니다.
위의 동영상을 보시면, 브라우저는 이미지가 완전히 다운로드되기 전까지는...
이미지를 렌더링하지 않기에 유저는 빈 영역을 볼 수 없다는 사실을 확인하실 수 있습니다.
이는 유저의 사용자 경험을 떨어뜨리고, 유저를 웹 사이트에서 이탈시킬 수 있습니다.
개선방법은 대체이미지를 미리 넣어주는 것입니다.
(다른 명칭으로는 스켈레톤 이미지 또는 Placeholder 이미지)
export default function LazyPhoto({ photo }: { photo: UnsplashPhoto }) {
const [isLoaded, setIsLoaded] = useState(false);
return (
<div className="py-0.5 aspect-square relative">
// 이미지 로딩 전
{!isLoaded && (
<div className="absolute w-full h-full bg-gray-300 rounded-md blur-xs" />
)}
<img
loading="lazy"
src={photo?.urls.full}
alt={photo?.description || "Unsplash Photo"}
className="absolute object-cover rounded-md w-full h-full"
onLoad={() => {
setIsLoaded(true);
}}
/>
</div>
);
}
리액트를 사용한다면, useState를 이용해서 쉽게 구현할 수 있습니다.
이미지 컨테이너를 하나 만들어서, 공간을 잡아둡니다.
이미지 로딩되기 전에 대체이미지를 이용해서 이미지 컨테이너 영역에 표시합니다.
이미지 로딩이 마치면, useState를 이용해서 DOM에서 대체이미지를 제거합니다.
대체이미지를 넣어주니 좀 더 낫습니다.
유저에게 이미지가 로딩되고 있다는 것을 직관적으로 알려주기 때문입니다.
여기까지 무한스크롤링을 이용한 이미지 구현을 정리해봤습니다.
'React' 카테고리의 다른 글
| 카카오맵 API: 특정 마커들이 다 보이게 지도 범위(Bounds) 조절하기 (0) | 2025.12.21 |
|---|---|
| [React] react-hook-form의 useWatch를 이용해 실시간으로 폼 데이터 확인하기 (0) | 2024.12.25 |
| [React] 컴포넌트를 어떻게 잘 구현할 수 있을까? (0) | 2023.07.29 |
| [React] 리액트 컴포넌트 최적화해보기 (with. useCallback, React.memo) (0) | 2023.06.22 |
| [React] React-Query 기본 사용법 정리 (useQuery, useMutation) (0) | 2023.01.20 |