대용량 데이터를 클라이언트에서 필터링, 정렬, 페이지네이션하는 것은 네트워크 대역폭 낭비, 메모리 사용량 증가, 그리고 심각한 UI 성능 저하를 야기합니다. 대신 서버 측 API를 적극 활용하여 필요한 데이터만 가져오고, 점진적 로딩 전략을 도입해야 합니다.
데이터 페칭 시, API가 제공하는 필터링, 정렬, 페이지네이션 기능을 사용하지 않고 모든 데이터를 클라이언트로 가져온 후 JavaScript로 처리하는 것은 여러 문제를 야기합니다.
대부분의 현대적인 API는 서버 측에서 데이터 필터링, 정렬, 페이지네이션 기능을 제공합니다. 이를 적극적으로 활용하여 효율적인 데이터 처리를 구현해야 합니다.
React Query, SWR 등의 라이브러리 활용)// 가정: 백엔드에서 모든 상품 데이터를 한 번에 응답한다고 가정
// 실제 애플리케이션에서는 수천, 수만 개의 상품 데이터가 될 수 있습니다.
const allProductsFromServer = [
{ id: 1, name: '사과', category: '과일' },
{ id: 2, name: '바나나', category: '과일' },
// ... (수천 개의 데이터)
{ id: 9999, name: '감자', category: '채소' },
];
function ProductListBefore() {
const [searchTerm, setSearchTerm] = React.useState('');
const [currentPage, setCurrentPage] = React.useState(1);
const itemsPerPage = 10;
// ⚠️ 안티패턴: 모든 데이터를 클라이언트로 가져와서 필터링
const filteredProducts = React.useMemo(() => {
return allProductsFromServer.filter(product =>
product.name.toLowerCase().includes(searchTerm.toLowerCase())
);
}, [searchTerm]);
// ⚠️ 안티패턴: 필터링된 모든 데이터에서 페이지네이션
const indexOfLastItem = currentPage * itemsPerPage;
const indexOfFirstItem = indexOfLastItem - itemsPerPage;
const currentProducts = filteredProducts.slice(indexOfFirstItem, indexOfLastItem);
const totalPages = Math.ceil(filteredProducts.length / itemsPerPage);
return (
<div>
<h2>전체 상품 ({allProductsFromServer.length}개)</h2>
<input
type="text"
placeholder="상품 검색..."
value={searchTerm}
onChange={(e) => {
setSearchTerm(e.target.value);
setCurrentPage(1); // 검색어 변경 시 첫 페이지로 리셋
}}
/>
<p>현재 페이지: {currentPage} / {totalPages}</p>
<ul>
{currentProducts.length > 0 ? (
currentProducts.map(product => (
<li key={product.id}>{product.name} ({product.category})</li>
))
) : (
<li>검색 결과가 없습니다.</li>
)}
</ul>
<div>
{Array.from({ length: totalPages }, (_, i) => (
<button
key={i}
onClick={() => setCurrentPage(i + 1)}
disabled={currentPage === i + 1}
style={{ fontWeight: currentPage === i + 1 ? 'bold' : 'normal', margin: '0 5px' }}
>
{i + 1}
</button>
))}
</div>
</div>
);
}// 서버는 검색어, 페이지, 페이지당 항목 수를 받아 해당하는 데이터와 전체 개수를 반환한다고 가정
// 예: /api/products?search=사과&page=1&limit=10 -> { products: [...], totalCount: 25 }
function ProductListAfter() {
const [searchTerm, setSearchTerm] = React.useState('');
const [currentPage, setCurrentPage] = React.useState(1);
const [products, setProducts] = React.useState([]);
const [loading, setLoading] = React.useState(false);
const [totalPages, setTotalPages] = React.useState(0);
const itemsPerPage = 10; // 서버와 협의된 페이지당 항목 수
// 💡 검색어 입력 시 불필요한 API 호출을 줄이기 위한 Debounce 훅 사용
const debouncedSearchTerm = useDebounce(searchTerm, 500);
React.useEffect(() => {
const fetchProducts = async () => {
setLoading(true);
try {
// ✅ 올바른 패턴: 서버 측 API에 검색어, 페이지, 페이지당 항목 수 전달
const response = await fetch(
`/api/products?search=${debouncedSearchTerm}&page=${currentPage}&limit=${itemsPerPage}`
);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const data = await response.json();
setProducts(data.products); // 서버에서 필터링/페이지네이션된 데이터만 받음
setTotalPages(Math.ceil(data.totalCount / itemsPerPage)); // 서버에서 전체 개수 받음
} catch (error) {
console.error("상품을 불러오는 데 실패했습니다.", error);
setProducts([]); // 에러 발생 시 데이터 초기화
setTotalPages(0);
} finally {
setLoading(false);
}
};
fetchProducts();
}, [debouncedSearchTerm, currentPage]); // 검색어 또는 페이지 변경 시에만 API 호출
// useDebounce 훅 예시 (실제 구현 시 별도 파일 또는 유틸리티 라이브러리 사용 권장)
function useDebounce(value, delay) {
const [debouncedValue, setDebouncedValue] = React.useState(value);
React.useEffect(() => {
const handler = setTimeout(() => {
setDebouncedValue(value);
}, delay);
return () => {
clearTimeout(handler);
};
}, [value, delay]);
return debouncedValue;
}
return (
<div>
<h2>상품 목록</h2>
<input
type="text"
placeholder="상품 검색..."
value={searchTerm}
onChange={(e) => {
setSearchTerm(e.target.value);
setCurrentPage(1); // 검색어 변경 시 첫 페이지로 리셋
}}
/>
{loading ? (
<div>상품 목록 로딩 중...</div>
) : (
<>
<p>현재 페이지: {currentPage} / {totalPages}</p>
<ul>
{products.length > 0 ? (
products.map(product => (
<li key={product.id}>{product.name} ({product.category})</li>
))
) : (
<li>검색 결과가 없습니다.</li>
)}
</ul>
<div>
{Array.from({ length: totalPages }, (_, i) => (
<button
key={i}
onClick={() => setCurrentPage(i + 1)}
disabled={currentPage === i + 1}
style={{ fontWeight: currentPage === i + 1 ? 'bold' : 'normal', margin: '0 5px' }}
>
{i + 1}
</button>
))}
</div>
</>
)}
</div>
);
}