Next.js에서 부분 사전 렌더링으로 성능 잠금 해제
Takashi Yamamoto
Infrastructure Engineer · Leapcell

Next.js에서 부분 사전 렌더링으로 웹 성능 향상
빠르게 진화하는 웹 개발 환경에서 사용자 경험과 성능은 무엇보다 중요합니다. 우리는 훌륭한 검색 엔진 최적화(SEO)를 보장하면서도 번개처럼 빠르고 매우 상호작용적인 애플리케이션을 제공하기 위해 끊임없이 노력합니다. 특히 동적 콘텐츠로 이 균형을 달성하는 것은 역사적으로 상당한 어려움을 안겨주었습니다. 전통적인 접근 방식은 개발자가 서버 측 렌더링(SSR)의 SEO 이점과 클라이언트 측 렌더링(CSR)의 동적 상호작용성 사이에서 선택하도록 강요하는 경우가 많았습니다. 바로 여기서 부분 사전 렌더링(PPR)이 Next.js 프레임워크 내에서 게임 체인저로 등장하여 이 격차를 해소하는 정교한 솔루션을 제공하고 웹 성능과 개발자 효율성의 새로운 시대를 약속합니다.
핵심 개념 이해하기
PPR의 실질적인 측면을 자세히 살펴보기 전에, 그 작동 방식과 이점의 기초가 되는 몇 가지 기본 개념을 파악하는 것이 중요합니다.
- 서버 측 렌더링 (SSR): 서버가 각 요청 시 페이지의 전체 HTML을 생성하는 렌더링 전략입니다. 즉각적인 콘텐츠, 좋은 SEO 및 접근성을 제공하지만 반복적인 서버 측 계산으로 인해 매우 동적인 페이지의 경우 느릴 수 있습니다.
- 클라이언트 측 렌더링 (CSR): 브라우저는 최소한의 HTML 셸을 수신하고 JavaScript가 데이터를 가져와 클라이언트에서 직접 콘텐츠를 렌더링합니다. 이는 훌륭한 상호작용성을 제공하지만 초기 로드 시간이 느리거나(빈 화면) 크롤러가 JavaScript 실행에 어려움을 겪는 경우 SEO가 좋지 않을 수 있습니다.
- 정적 사이트 생성 (SSG): 페이지가 빌드 시간에 정적 HTML 파일로 미리 렌더링됩니다. 이는 극한의 속도와 확장성을 제공하지만 자주 변경되지 않는 콘텐츠에만 적합합니다.
- React 서버 컴포넌트 (RSC): React에서 도입된 새로운 패러다임으로, 개발자가 서버에서 컴포넌트를 렌더링하고 클라이언트로 스트리밍할 수 있습니다. 이를 통해 렌더링 위치를 세밀하게 제어하여 성능을 개선하고 클라이언트 측 번들 크기를 줄일 수 있습니다.
부분 사전 렌더링(PPR)은 이러한 전략의 가장 좋은 측면을 결합합니다. 핵심적으로 PPR은 React 서버 컴포넌트와 Next.js의 렌더링 엔진 위에 구축된 최적화입니다. 이를 통해 페이지의 특정 부분이 빌드 시간(또는 첫 요청 시 캐싱)에 정적으로 사전 렌더링되고, 더 동적인 다른 부분은 필요에 따라 서버에서 스트리밍됩니다. 이는 사용자가 거의 즉시 빠르고 완성도 높은 페이지를 받고, 동적 요소가 점진적으로 경험을 향상시킨다는 것을 의미합니다.
부분 사전 렌더링 작동 방식 및 실제 적용
PPR의 우아함은 '폴백(fallback)'에 있습니다. 페이지의 동적 부분으로 무엇을 렌더링할지 정의할 때 '폴백' 로딩 상태를 제공할 수 있습니다. Next.js는 거의 즉시 사전 렌더링된 정적 콘텐츠를 제공하고 동적 섹션에 대한 폴백을 표시합니다. 동적 데이터가 서버(React 서버 컴포넌트를 통해)에서 사용 가능해지면 해당 섹션은 하이드레이션되고 최신 콘텐츠를 표시합니다.
일반적인 시나리오인 전자 상거래 제품 페이지를 예로 들어 보겠습니다.
다음을 포함하는 제품 페이지를 상상해 보세요.
- 정적 요소: 제품 이미지, 제목, 설명, 가격(이러한 요소는 종종 요청마다 변경되지 않습니다).
- 동적 요소: 실시간 재고 현황, 개인화된 추천, 사용자 리뷰(이러한 요소는 자주 변경됩니다).
PPR 없이 일반적으로 다음 중에서 선택해야 합니다.
- SSR: 모든 요청마다 정적 부분도 포함하여 전체 페이지를 다시 빌드하므로 초기 렌더링 속도가 느려질 수 있습니다.
- SSG + 동적 부분에 대한 CSR: 정적 부분은 생성하지만 동적 콘텐츠가 있어야 할 위치에 빈 공간이 나타나고 클라이언트 측 JavaScript가 해당 콘텐츠를 가져와 렌더링한 후에만 채워집니다.
PPR을 사용하면 두 가지의 장점을 모두 얻을 수 있습니다. 다음은 개념을 시연하는 간단한 코드 예입니다.
ProductPage
컴포넌트가 있다고 가정해 봅시다.
// app/product/[slug]/page.js import { Suspense } from 'react'; import ProductDetails from '@/components/ProductDetails'; import RecommendedProducts from '@/components/RecommendedProducts'; import UserReviews from '@/components/UserReviews'; import ProductSkeleton from '@/components/ProductSkeleton'; // 동적 콘텐츠를 위한 로딩 스켈레톤 export default function ProductPage({ params }) { const { slug } = params; return ( <div className="container mx-auto p-4"> <ProductDetails slug={slug} /> {/* 정적 제품 데이터를 가져오는 서버 컴포넌트일 수 있습니다. */} <h2 className="text-2xl font-bold mt-8 mb-4">You might also like</h2> <Suspense fallback={<ProductSkeleton count={3} />}> <RecommendedProducts slug={slug} /> {/* 폴백이 있는 동적 서버 컴포넌트 */} </Suspense> <h2 className="text-2xl font-bold mt-8 mb-4">Customer Reviews</h2> <Suspense fallback={<p>Loading reviews...</p>}> <UserReviews slug={slug} /> {/* 폴백이 있는 동적 서버 컴포넌트 */} </Suspense> </div> ); } // components/ProductDetails.js (서버 컴포넌트) async function ProductDetails({ slug }) { // 데이터베이스 또는 API에서 정적과 같은 제품 데이터 가져오기 const product = await fetch(`https://api.example.com/products/${slug}`).then(res => res.json()); return ( <div> <img src={product.imageUrl} alt={product.name} className="w-full h-64 object-cover" /> <h1 className="text-3xl font-bold mt-4">{product.name}</h1> <p className="text-xl text-gray-700">${product.price.toFixed(2)}</p> <p className="mt-2">{product.description}</p> </div> ); } // components/RecommendedProducts.js (서버 컴포넌트 - 동적) async function RecommendedProducts({ slug }) { // 동적 추천을 위한 느린 API 호출 시뮬레이션 await new Promise(resolve => setTimeout(resolve, 1500)); const recommendations = await fetch(`https://api.example.com/recommendations?product=${slug}`).then(res => res.json()); return ( <div className="grid grid-cols-3 gap-4"> {recommendations.map(reco => ( <div key={reco.id} className="border p-2"> <p>{reco.name}</p> <p>${reco.price.toFixed(2)}</p> </div> ))} </div> ); } // components/UserReviews.js (서버 컴포넌트 - 동적) async function UserReviews({ slug }) { // 사용자 리뷰를 위한 또 다른 느린 API 호출 시뮬레이션 await new Promise(resolve => setTimeout(resolve, 2000)); const reviews = await fetch(`https://api.example.com/reviews?product=${slug}`).then(res => res.json()); return ( <div className="mt-4"> {reviews.length === 0 ? <p>No reviews yet.</p> : ( reviews.map(review => ( <div key={review.id} className="border-b py-2"> <p className="font-bold">{review.user}</p> <p>{review.comment}</p> </div> )) )} </div> ); }
이 예제에서는:
ProductDetails
는 핵심 제품 데이터를 가져오는 서버 컴포넌트입니다. PPR 덕분에 페이지의 이 부분은 서버에서 렌더링되고 즉시 클라이언트로 스트리밍될 수 있습니다. 데이터가 안정적이라면 Next.js는 이 섹션의 전체 HTML을 캐싱할 수도 있습니다.RecommendedProducts
와UserReviews
도 서버 컴포넌트이지만Suspense
경계로 래핑되어 있습니다. 이는 Next.js에 이러한 컴포넌트가 로드하는 데 시간이 더 걸릴 수 있음을 알려줍니다.- 사용자가 이 페이지를 요청하면 Next.js는
ProductDetails
(아마도 캐시에서)와RecommendedProducts
및UserReviews
에 대한fallback
UI를 신속하게 렌더링합니다. - 백그라운드에서 서버는
RecommendedProducts
및UserReviews
에 대한 데이터 가져오기를 계속 진행합니다. 준비가 되면 이러한 컴포넌트는 서버에서 렌더링되고 해당 폴백을 원활하게 대체하는 HTML로 클라이언트로 스트리밍됩니다.
부분 사전 렌더링의 장점
PPR을 채택함으로써 얻을 수 있는 이점은 광범위합니다.
- 즉각적인 초기 로드: 사용자는 의미 있는 콘텐츠를 훨씬 더 빠르게 볼 수 있으며, 정적 셸과 비동적 부분은 거의 즉시 제공됩니다. 이는 인지된 성능을 크게 향상시킵니다.
- 우수한 사용자 경험: 동적 콘텐츠는 초기 렌더링을 차단하지 않고 점진적으로 로드되어 SSG의 초기 속도와 유사하게 더 부드럽고 반응성이 뛰어난 느낌을 제공합니다.
- 향상된 SEO: 검색 엔진 크롤러는 사전 렌더링된 정적 콘텐츠를 포함한 완전한 HTML 페이지를 수신하여 순수 CSR과 달리 뛰어난 인덱싱 및 검색 가능성을 보장합니다.
- 정적 부분에 대한 서버 부하 감소: 인기 있는 페이지의 경우 정적 셸과 안정적인 부분을 효율적으로 캐싱할 수 있어 후속 요청에 대한 서버의 계산 부담을 줄입니다.
- 최적화된 번들 크기: 더 많은 컴포넌트를 서버에서 렌더링함으로써 클라이언트로 배송되어야 하는 JavaScript가 줄어들어 번들 크기가 작아지고 스크립트 실행 속도가 빨라집니다.
- 간소화된 데이터 가져오기: 서버 컴포넌트와의 통합을 통해 서버에서 직접 데이터베이스 쿼리나 안전한 API 호출이 가능하여 클라이언트에 민감한 키를 노출하지 않고도 데이터 가져오기 로직을 단순화할 수 있습니다.
- 정적 및 동적 간의 유연성: PPR을 통해 개발자는 페이지의 어떤 부분이 정적이고 어떤 부분이 동적인지 정확하게 제어할 수 있으므로 성능과 최신성 간의 균형을 잡는 데 탁월한 유연성을 제공합니다.
결론
React 서버 컴포넌트와 Suspense
를 기반으로 하는 Next.js의 부분 사전 렌더링은 현대 웹 애플리케이션 개발의 복잡성을 해결하는 데 있어 기념비적인 도약을 나타냅니다. 정적 사이트의 속도, 서버 렌더링의 SEO 이점, 클라이언트 측 경험의 동적 상호작용성을 우아하게 결합합니다. 어떤 콘텐츠를 언제 제공할지 지능적으로 조정함으로써 PPR은 개발자가 뛰어난 웹 경험을 진정으로 제공하는 고성능, 사용자 중심, SEO 친화적인 애플리케이션을 구축할 수 있도록 지원합니다. 이는 핵심 웹 바이탈을 크게 개선하고 웹에서 효율적인 콘텐츠 전달을 위한 새로운 표준을 설정하는 강력한 패러다임입니다.