대부분의 애플리케이션 개발에서 데이터 관리는 필수적인 부분입니다. 애플리케이션이 점점 복잡해짐에 따라 처리해야 할 데이터의 양도 늘어나기 마련입니다. 만약 애플리케이션이 대량의 데이터를 효과적으로 관리하지 못한다면 성능 저하를 피할 수 없습니다.
페이지네이션과 무한 스크롤은 애플리케이션 성능을 최적화하는 데 매우 유용한 두 가지 기술입니다. 이러한 기술들을 통해 데이터 렌더링 과정을 보다 효율적으로 처리하고 사용자 경험을 전반적으로 향상시킬 수 있습니다.
TanStack Query를 활용한 페이지네이션 및 무한 스크롤 구현
TanStack Query, 즉 React Query의 변형은 JavaScript 애플리케이션을 위한 강력한 상태 관리 라이브러리입니다. 이 라이브러리는 캐싱과 같은 데이터 관련 작업을 포함한 다양한 기능을 제공하며, 애플리케이션 상태를 관리하기 위한 효율적인 솔루션을 제공합니다.
페이지네이션은 대규모 데이터 세트를 더 작은 페이지로 분할하여, 사용자가 탐색 버튼을 통해 관리 가능한 단위로 콘텐츠를 탐색할 수 있도록 합니다. 반면, 무한 스크롤은 더욱 동적인 탐색 환경을 제공합니다. 사용자가 스크롤하면 새로운 데이터가 자동으로 로드되고 표시되므로 명시적인 탐색 작업이 필요하지 않습니다.
페이지네이션과 무한 스크롤은 모두 대량의 데이터를 효율적으로 관리하고 표시하는 것을 목표로 합니다. 어떤 방법을 선택할지는 애플리케이션의 데이터 요구 사항에 따라 달라집니다.
이 프로젝트의 코드는 GitHub 저장소에서 확인할 수 있습니다.
Next.js 프로젝트 설정
시작하려면 Next.js 프로젝트를 생성해야 합니다. App 디렉터리를 사용하는 최신 버전의 Next.js 13을 설치하세요.
npx create-next-app@latest next-project --app
다음으로, npm과 같은 노드 패키지 관리자를 사용하여 프로젝트에 TanStack 패키지를 설치합니다.
npm i @tanstack/react-query
Next.js 애플리케이션에 TanStack Query 통합
Next.js 프로젝트에 TanStack Query를 통합하려면 애플리케이션의 루트 레이아웃(layout.js 파일)에서 TanStack Query의 새 인스턴스를 생성하고 초기화해야 합니다. 이를 위해 TanStack Query에서 `QueryClient`와 `QueryClientProvider`를 가져온 후, 다음과 같이 `QueryClientProvider`를 사용하여 하위 요소를 감쌉니다.
"use client"
import React from 'react'
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';const metadata = {
title: 'Create Next App',
description: 'Generated by create next app',
};export default function RootLayout({ children }) {
const queryClient = new QueryClient();return (
<html lang="en">
<body>
<QueryClientProvider client={queryClient}>
{children}
</QueryClientProvider>
</body>
</html>
);
}export { metadata };
이렇게 설정하면 TanStack Query가 애플리케이션의 상태에 대한 전체 접근 권한을 갖게 됩니다.
`useQuery` 훅은 데이터 가져오기 및 관리를 단순화합니다. 페이지 번호와 같은 페이지네이션 매개변수를 제공하면 데이터의 특정 하위 집합을 쉽게 검색할 수 있습니다.
또한 이 훅은 캐시 옵션 설정, 로딩 상태 처리 등 데이터 가져오기 기능을 사용자 정의할 수 있는 다양한 옵션과 구성을 제공합니다. 이러한 기능을 통해 원활한 페이지네이션 환경을 쉽게 만들 수 있습니다.
이제 Next.js 앱에서 페이지네이션을 구현하기 위해 `src/app` 디렉터리에 `Pagination/page.js` 파일을 만듭니다. 해당 파일 내에서 다음 항목을 가져옵니다.
"use client"
import React, { useState } from 'react';
import { useQuery} from '@tanstack/react-query';
import './page.styles.css';
다음으로, React 함수 컴포넌트를 정의합니다. 이 컴포넌트 내부에서는 외부 API에서 데이터를 가져오는 함수를 정의해야 합니다. 여기서는 JSON Placeholder API를 사용하여 게시물 목록을 가져옵니다.
export default function Pagination() {
const [page, setPage] = useState(1);const fetchPosts = async () => {
try {
const response = await fetch(`https://jsonplaceholder.typicode.com/posts?
_page=${page}&_limit=10`);if (!response.ok) {
throw new Error('Failed to fetch posts');
}const data = await response.json();
return data;
} catch (error) {
console.error(error);
throw error;
}
};
}
이제 `useQuery` 훅을 정의하고 다음 매개변수를 객체 형태로 지정합니다.
const { isLoading, isError, error, data } = useQuery({
keepPreviousData: true,
queryKey: ['posts', page],
queryFn: fetchPosts,
});
`keepPreviousData` 값은 `true`로 설정되어 있습니다. 이는 새로운 데이터를 가져오는 동안 앱이 이전 데이터를 유지하도록 합니다. `queryKey` 매개변수는 쿼리 키(이 경우에는 데이터를 가져올 엔드포인트 및 현재 페이지)를 포함하는 배열입니다. 마지막으로, `queryFn` 매개변수인 `fetchPosts`는 데이터를 가져오는 함수 호출을 트리거합니다.
앞서 언급했듯이, 훅은 배열과 객체를 구조 분해하고, 이를 통해 데이터를 가져오는 과정에서 사용자 경험(적절한 UI 렌더링)을 향상시키는 방법과 유사하게 다양한 상태를 제공합니다. 이러한 상태에는 `isLoading`, `isError` 등이 포함됩니다.
이러한 상태를 사용하여 진행 중인 프로세스의 현재 상태에 따라 다양한 메시지 화면을 렌더링하는 다음 코드를 추가합니다.
if (isLoading) {
return (<h2>Loading...</h2>);
}if (isError) {
return (<h2 className="error-message">{error.message}</h2>);
}
마지막으로, 브라우저 페이지에 렌더링할 JSX 요소에 대한 코드를 추가합니다. 이 코드는 다음 두 가지 기능을 제공합니다.
- 앱이 API에서 게시물을 가져오면 해당 게시물은 `useQuery` 훅에서 제공하는 `data` 변수에 저장됩니다. 이 변수는 애플리케이션의 상태를 관리하는 데 도움이 됩니다. 그런 다음, 이 변수에 저장된 게시물 목록을 매핑하고 브라우저에서 렌더링할 수 있습니다.
- 사용자가 페이지네이션된 추가 데이터를 쿼리하고 표시할 수 있도록 이전 및 다음 버튼과 같은 두 개의 탐색 버튼을 추가합니다.
return (
<div>
<h2 className="header">Next.js Pagination</h2>
{data && (
<div className="card">
<ul className="post-list">
{data.map((post) => (
<li key={post.id} className="post-item">{post.title}</li>
))}
</ul>
</div>
)}
<div className="btn-container">
<button
onClick={() => setPage(prevState => Math.max(prevState - 1, 0))}
disabled={page === 1}
className="prev-button"
>Prev Page</button><button
onClick={() => setPage(prevState => prevState + 1)}
className="next-button"
>Next Page</button>
</div>
</div>
);
마지막으로 개발 서버를 시작합니다.
npm run dev
그런 다음 브라우저에서 `http://localhost:3000/Pagination`으로 이동합니다.
앱 디렉터리에 `Pagination` 폴더를 포함했으므로 Next.js는 이를 경로로 처리하여 해당 URL에서 페이지에 액세스할 수 있도록 합니다.
무한 스크롤은 매끄러운 탐색 경험을 제공합니다. 좋은 예시는 사용자가 아래로 스크롤할 때 자동으로 새로운 동영상을 가져와서 보여주는 YouTube입니다.
`useInfiniteQuery` 훅을 사용하면 서버에서 페이지 단위로 데이터를 가져오고, 사용자가 아래로 스크롤할 때 자동으로 다음 데이터 페이지를 가져와서 렌더링함으로써 무한 스크롤을 구현할 수 있습니다.
무한 스크롤을 구현하려면 `src/app` 디렉터리에 `InfiniteScroll/page.js` 파일을 추가합니다. 그런 다음 다음 항목을 가져옵니다.
"use client"
import React, { useRef, useEffect, useState } from 'react';
import { useInfiniteQuery } from '@tanstack/react-query';
import './page.styles.css';
다음으로, React 함수 컴포넌트를 만듭니다. 페이지네이션 구현과 유사하게 이 컴포넌트 내부에서 게시물 데이터를 가져오는 함수를 만듭니다.
export default function InfiniteScroll() {
const listRef = useRef(null);
const [isLoadingMore, setIsLoadingMore] = useState(false);const fetchPosts = async ({ pageParam = 1 }) => {
try {
const response = await fetch(`https://jsonplaceholder.typicode.com/posts?
_page=${pageParam}&_limit=5`);if (!response.ok) {
throw new Error('Failed to fetch posts');
}const data = await response.json();
await new Promise((resolve) => setTimeout(resolve, 2000));
return data;
} catch (error) {
console.error(error);
throw error;
}
};
}
페이지네이션 구현과는 달리, 이 코드는 데이터를 가져올 때 2초의 지연을 추가하여 사용자가 스크롤하여 새로운 데이터 세트의 다시 가져오기를 트리거하는 동안 현재 데이터를 탐색할 수 있도록 합니다.
이제 `useInfiniteQuery` 훅을 정의합니다. 컴포넌트가 처음 마운트될 때, 이 훅은 서버에서 첫 번째 데이터 페이지를 가져옵니다. 사용자가 아래로 스크롤하면 훅이 자동으로 다음 데이터 페이지를 가져와서 컴포넌트에 렌더링합니다.
const { data, fetchNextPage, hasNextPage, isFetching } = useInfiniteQuery({
queryKey: ['posts'],
queryFn: fetchPosts,
getNextPageParam: (lastPage, allPages) => {
if (lastPage.length < 5) {
return undefined;
}
return allPages.length + 1;
},
});const posts = data ? data.pages.flatMap((page) => page) : [];
`posts` 변수는 여러 페이지의 모든 게시물을 단일 배열로 결합하여 데이터 변수의 평탄화된 버전을 만듭니다. 이렇게 하면 개별 게시물을 쉽게 매핑하고 렌더링할 수 있습니다.
사용자의 스크롤을 추적하고 사용자가 목록 하단에 가까울 때 더 많은 데이터를 로드하기 위해 Intersection Observer API를 활용하여 요소가 뷰포트와 교차하는 시점을 감지하는 함수를 정의할 수 있습니다.
const handleIntersection = (entries) => {
if (entries[0].isIntersecting && hasNextPage && !isFetching && !isLoadingMore) {
setIsLoadingMore(true);
fetchNextPage();
}
};useEffect(() => {
const observer = new IntersectionObserver(handleIntersection, { threshold: 0.1 });if (listRef.current) {
observer.observe(listRef.current);
}return () => {
if (listRef.current) {
observer.unobserve(listRef.current);
}
};
}, [listRef, handleIntersection]);useEffect(() => {
if (!isFetching) {
setIsLoadingMore(false);
}
}, [isFetching]);
마지막으로 브라우저에서 렌더링되는 게시물에 대한 JSX 요소를 추가합니다.
return (
<div>
<h2 className="header">Infinite Scroll</h2>
<ul ref={listRef} className="post-list">
{posts.map((post) => (
<li key={post.id} className="post-item">
{post.title}
</li>
))}
</ul>
<div className="loading-indicator">
{isFetching ? 'Fetching...' : isLoadingMore ? 'Loading more...' : null}
</div>
</div>
);
모든 변경이 완료된 후 `http://localhost:3000/InfiniteScroll`을 방문하여 실제 적용된 결과를 확인해 보세요.
TanStack Query: 단순한 데이터 가져오기 이상의 기능
페이지네이션 및 무한 스크롤은 TanStack Query의 기능을 강조하는 좋은 예시입니다. 간단히 말해, 이 라이브러리는 다재다능한 데이터 관리 도구입니다.
광범위한 기능 세트를 통해 효율적인 상태 처리를 포함하여 앱의 데이터 관리 프로세스를 간소화할 수 있습니다. 또한 다른 데이터 관련 작업과 함께 웹 애플리케이션의 전반적인 성능과 사용자 경험을 향상시킬 수 있습니다.