Next.js에서 데이터 가져오기 마스터하기
Min-jun Kim
Dev Intern · Leapcell

소개
끊임없이 진화하는 프론트엔드 개발 환경에서 효율적인 데이터 가져오기는 성능이 뛰어나고 사용자 친화적인 애플리케이션을 구축하는 데 매우 중요합니다. 인기 있는 React 프레임워크인 Next.js는 데이터를 검색하고 렌더링하는 강력한 전략을 제공하며 이 분야에서 지속적으로 혁신해 왔습니다. React 서버 컴포넌트의 등장과 Next.js로의 통합으로 데이터 가져오기 패러다임이 크게 바뀌었습니다. 최신 고도로 최적화된 웹 애플리케이션을 구축하려는 개발자에게는 클라이언트 컴포넌트, 서버 컴포넌트 및 향상된 fetch
API 간의 차이점과 상호 작용을 이해하는 것이 중요합니다. 이 글에서는 이러한 필수 개념을 살펴보고 Next.js에서 데이터 가져오기를 마스터하고 탁월한 사용자 경험을 제공하는 데 도움이 되는 실용적인 지침과 코드 예제를 제공합니다.
핵심 개념 및 전략
구현 세부 정보에 대해 자세히 알아보기 전에 Next.js 데이터 가져오기 전략의 기반을 이루는 핵심 개념을 명확하게 이해해 보겠습니다.
클라이언트 컴포넌트 및 서버 컴포넌트
클라이언트 컴포넌트(CC): 사용자의 브라우저 내에서 클라이언트 측에서 완전히 실행되고 다시 렌더링되는 익숙한 React 컴포넌트입니다. 상호 작용 및 부작용을 위해 useState
및 useEffect
와 같은 React Hook을 활용할 수 있습니다. 데이터 가져오기의 경우 클라이언트 컴포넌트는 일반적으로 useEffect
를 axios
, swr
또는 react-query
와 같은 라이브러리 또는 네이티브 fetch
API와 함께 사용합니다.
서버 컴포넌트(SC): 획기적인 혁신인 서버 컴포넌트는 서버에서만 실행됩니다. 요청 시(또는 빌드 시) 한 번 렌더링되고 결과 HTML 및 필요한 번들을 클라이언트로 보냅니다. 서버에서 실행되기 때문에 서버 컴포넌트는 데이터베이스, 파일 시스템 또는 개인 API와 같은 서버 측 리소스에 민감한 정보를 클라이언트에 노출하지 않고 직접 액세스할 수 있습니다. 상태나 효과가 없으며 사용자 상호 작용을 직접 처리할 수 없습니다. 주요 사용 사례에는 데이터 가져오기, 서버 측 논리 액세스 및 클라이언트 측 상호 작용이 필요하지 않은 정적 또는 동적 콘텐츠 렌더링이 포함됩니다.
향상된 fetch
API
Next.js는 특히 서버 컴포넌트 내에서 사용할 때 강력한 기능을 갖춘 네이티브 fetch
API를 향상시킵니다. 이러한 향상 기능에는 자동 요청 메모이제이션, next/cache
를 통한 재검증 및 컴포넌트 정의에서 직접 async/await
지원이 포함됩니다. 이 통합은 기본 캐싱 및 재검증 논리를 사용하여 데이터를 가져오는 간소화된 방법을 제공합니다.
클라이언트 컴포넌트에서 데이터 가져오기
애플리케이션의 대화형 부분이나 사용자 작업에 따라 실시간 업데이트가 필요한 경우 클라이언트 컴포넌트를 사용합니다. 초기 렌더링 후 useEffect
를 사용하여 데이터를 가져오는 경우가 많습니다.
// components/ClientDataFetcher.jsx 'use client'; // This directive marks it as a Client Component import { useState, useEffect } from 'react'; export default function ClientDataFetcher() { const [data, setData] = useState(null); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); useEffect(() => { async function fetchData() { try { const response = await fetch('/api/public-data'); // Fetches from a public API endpoint if (!response.ok) { throw new Error('Failed to fetch data'); } const json = await response.json(); setData(json); } catch (err) { setError(err); } finally { setLoading(false); } } fetchData(); }, []); // Empty dependency array ensures it runs once on mount if (loading) return <p>Loading client data...</p>; if (error) return <p>Error: {error.message}</p>; return ( <div> <h2>Client-Fetched Data</h2> <pre>{JSON.stringify(data, null, 2)}</pre> </div> ); }
이 예에서 'use client'
지시문은 ClientDataFetcher
를 클라이언트 컴포넌트로 명확하게 식별합니다. 데이터는 컴포넌트가 마운트된 후 useEffect
를 사용하여 가져오므로 상호 작용이 가능하고 클라이언트 측 이벤트에 따라 데이터를 업데이트할 수 있습니다.
서버 컴포넌트에서 데이터 가져오기
서버 컴포넌트는 클라이언트로 보내지기 전에 서버에서 직접 데이터를 가져오는 데 탁월합니다. 이는 향상된 성능(데이터에 대한 클라이언트의 추가 네트워크 왕복 없음), 향상된 보안(API 키 노출 없음) 및 간소화된 코드와 같은 여러 가지 이점을 제공합니다.
// app/page.jsx (This is a Server Component by default in the App Router) // For RSCs, you can make the component async directly async function getServerData() { // Using an internal API route for example, or directly connect to a database const response = await fetch(`${process.env.API_BASE_URL}/api/products`, { cache: 'no-store', // Opt-out of caching for this request // next: { revalidate: 60 } // Revalidate this data every 60 seconds }); if (!response.ok) { throw new Error('Failed to fetch server data'); } return response.json(); } export default async function HomePage() { const products = await getServerData(); // Data fetched on the server return ( <div> <h1>Welcome to Next.js Store!</h1> <h2>Our Products (Server-Fetched)</h2> <ul> {products.map(product => ( <li key={product.id}> {product.name} - ${product.price} </li> ))} </ul> {/* Client components can be rendered within Server Components */} {/* For example, an "Add to Cart" button would be a Client Component */} {/* <AddToCartButton productId={product.id} /> */} </div> ); }
여기서 HomePage
는 서버 컴포넌트입니다(app
디렉터리에 위치하고 'use client'
지시어가 없다는 점에 의해 암시됨). getServerData
함수는 컴포넌트 내에서 직접 호출되어 렌더링 전에 결과를 기다립니다. cache: 'no-store'
또는 next: { revalidate: 60 }
(시간 기반 재검증용)와 같은 fetch
옵션은 Next.js 내에서 fetch
의 향상된 기능을 보여줍니다. cache: 'no-store'
는 데이터가 항상 각 요청에서 가져오는 것을 의미하며 revalidate
는 백그라운드 재검증을 활성화합니다.
클라이언트 및 서버 컴포넌트 결합
Next.js의 강점은 클라이언트 및 서버 컴포넌트를 원활하게 결합하는 기능에 있습니다. 서버 컴포넌트는 클라이언트 컴포넌트를 렌더링하고 데이터를 prop으로 전달할 수 있습니다. 이를 통해 필요한 곳에서 풍부한 상호 작용을 제공하면서 서버에서 정적 또는 자주 액세스되는 데이터를 가져올 수 있습니다.
// app/product/[id]/page.jsx (Server Component for dynamic routing) import AddToCartButton from '@/components/AddToCartButton'; // This is a Client Component async function getProductDetails(id) { const response = await fetch(`${process.env.API_BASE_URL}/api/products/${id}`); if (!response.ok) { throw new Error(`Failed to fetch product ${id}`); } return response.json(); } export default async function ProductDetailsPage({ params }) { const product = await getProductDetails(params.id); return ( <div> <h1>{product.name}</h1> <p>{product.description}</p> <p>Price: ${product.price}</p> {/* Passing server-fetched data as props to a Client Component */} <AddToCartButton productId={product.id} productName={product.name} /> </div> ); } // components/AddToCartButton.jsx 'use client'; import { useState } from 'react'; export default function AddToCartButton({ productId, productName }) { const [isLoading, setIsLoading] = useState(false); const [message, setMessage] = useState(''); const handleAddToCart = async () => { setIsLoading(true); setMessage(''); try { // Simulate API call to add to cart await new Promise(resolve => setTimeout(resolve, 1000)); // In a real app, you'd make a fetch POST request here setMessage(`${productName} added to cart!`); } catch (error) { setMessage('Failed to add to cart.'); } finally { setIsLoading(false); } }; return ( <button onClick={handleAddToCart} disabled={isLoading}> {isLoading ? 'Adding...' : 'Add to Cart'} {message && <p>{message}</p>} </button> ); }
이 설정에서 ProductDetailsPage
(서버 컴포넌트)는 제품 데이터를 가져옵니다. 그런 다음 productId
및 productName
을 prop으로 전달하여 AddToCartButton
(클라이언트 컴포넌트)을 렌더링합니다. 버튼 자체는 클라이언트 측 상태 및 상호 작용을 처리하여 클릭 시 API 호출을 합니다. 이 패턴은 두 컴포넌트 유형의 강점을 활용합니다.
결론
Next.js는 개발자에게 매우 최적화되고 복원력 있는 웹 애플리케이션을 구축할 수 있는 도구를 제공하는 강력하고 유연한 데이터 가져오기 전략 모음을 제공합니다. 클라이언트 컴포넌트와 서버 컴포넌트의 핵심 차이점을 이해하고 향상된 fetch
API를 활용하면 데이터 가져오기 위치와 방법을 정보에 입각한 결정을 내릴 수 있습니다. 이 접근 방식은 성능 향상, 보안 강화 및 사용자 경험 향상으로 이어져 Next.js를 최신 웹 개발에 탁월한 선택으로 만듭니다.