Next.js 서버 사이드 렌더링(SSR): 개발자가 알아야 할 모든 것
단순한 클라이언트 렌더링으로는 SEO와 초기 로딩 성능 문제를 해결할 수 없다. 그렇다면 해답은?
안녕하세요, 백엔드와 프론트엔드 사이의 경계가 모호해지는 요즘입니다. 특히 React 생태계에서 활발하게 사용되는 Next.js의 서버 사이드 렌더링은 이제 선택이 아닌 필수 기술이 되었습니다. 5년간 여러 프로젝트에서 Next.js를 사용해본 경험을 바탕으로, 오늘은 서버 사이드 렌더링의 핵심 개념부터 실전 최적화 전략까지 현실적인 관점에서 살펴보겠습니다. 화려한 마케팅 문구가 아닌 실제 개발 현장에서의 경험을 공유합니다.

목차
SSR의 기본 개념과 작동 원리
서버 사이드 렌더링(SSR)이란 웹 페이지의 초기 HTML을 클라이언트가 아닌 서버에서 생성하는 방식이다. 간단히 말해서, 사용자가 페이지를 요청하면 서버에서 필요한 데이터를 모두 가져와 완성된 HTML을 생성한 후 브라우저에 전송하는 것이다. 이게 왜 중요한지는 React나 Vue 같은 클라이언트 사이드 프레임워크의 한계를 생각해보면 분명해진다.
전통적인 SPA(Single Page Application)에서는 초기에 거의 비어있는 HTML을 받고, JavaScript가 로드된 후에야 콘텐츠가 렌더링된다. 이 과정에서 발생하는 문제점들:
- SEO(검색 엔진 최적화) 불리: 검색 엔진 크롤러는 JavaScript 렌더링 결과를 제대로 인식하지 못할 수 있음
- 초기 로딩 시간 증가: JS 번들을 다운로드하고 실행한 후에야 콘텐츠 표시 가능
- 사용자 경험 저하: 빈 화면이나 로딩 스피너를 오래 보게 됨
SSR은 이러한 문제를 해결하기 위한 접근 방식이다. Next.js는 React 애플리케이션에서 SSR을 쉽게 구현할 수 있게 해주는 프레임워크다. 기본적인 SSR 작동 방식은 아래에 설명을 달아두었다.
- 사용자가 URL에 접속하면 요청이 Next.js 서버로 전달된다.
- 서버는 해당 페이지를 렌더링하는데 필요한 데이터를 외부 API 등에서 가져온다.
- 데이터를 기반으로 React 컴포넌트를 실행해 HTML을 생성한다.
- 완성된 HTML을 클라이언트에 전송한다.
- 브라우저는 HTML을 즉시 렌더링하고 사용자에게 콘텐츠를 보여준다.
- 이후 JavaScript가 로드되면 React가 "하이드레이션(Hydration)" 과정을 통해 정적 HTML에 이벤트 리스너 등을 연결한다.
이 과정은 JavaScript가 완전히 로드되기 전에도 사용자에게 의미 있는 콘텐츠를 보여줄 수 있다는 큰 장점이 있다. 덕분에 초기 페이지 로드 경험이 빨라지고 SEO도 개선된다.
SSR vs CSR: 성능과 사용성 비교
서버 사이드 렌더링(SSR)과 클라이언트 사이드 렌더링(CSR)은 각각 장단점이 있다. 프로젝트 특성에 따라 적절한 방식을 선택해야 하는데, 다음 표를 통해 주요 차이점을 비교해보자.
비교 요소 | 서버 사이드 렌더링 (SSR) | 클라이언트 사이드 렌더링 (CSR) |
---|---|---|
초기 로딩 시간 | HTML이 미리 생성되어 빠르게 표시됨 | JS 번들 다운로드 및 실행 후 표시 (더 느림) |
SEO | 우수 (완성된 HTML을 크롤러가 바로 읽을 수 있음) | 불리 (JS 실행 결과를 크롤러가 제대로 인식 못할 수 있음) |
페이지 전환 속도 | 페이지마다 서버 요청 필요 (상대적으로 느림) | 클라이언트에서 즉시 처리 (매우 빠름) |
서버 부하 | 높음 (모든 요청 처리) | 낮음 (정적 자산만 제공) |
개발 복잡성 | 높음 (서버/클라이언트 환경 차이 고려 필요) | 낮음 (단일 환경에서 개발) |
인터랙티브 콘텐츠 | 하이드레이션 과정 이후 가능 | JS 로드 후 즉시 가능 |
위 표를 보면 SSR과 CSR은 확실히 트레이드오프 관계가 있다. 그래서 Next.js는 SSR, CSR, 정적 생성(Static Generation)을 모두 지원하며, 페이지별로 다른 렌더링 방식을 선택할 수 있게 해준다. 이는 실제 프로젝트에서 매우 유용한 유연성을 제공한다.
그렇다면 언제 SSR을 선택해야 할까?
- SEO가 중요한 콘텐츠 중심 사이트 (블로그, 뉴스, 상품 페이지 등)
- 초기 로딩 성능이 중요한 서비스
- 네트워크 속도나 디바이스 성능이 낮은 환경의 사용자가 많은 서비스
- 소셜 미디어 공유 시 미리보기가 중요한 콘텐츠
Next.js에서 SSR 구현하기
Next.js에서 SSR을 구현하는 방법은 버전에 따라 다소 차이가 있다. 과거에는 getServerSideProps 함수가 주로 사용됐지만, Next.js 13부터는 App Router와 React Server Components를 통한 새로운 접근 방식이 도입됐다. 두 가지 방식 모두 살펴보자.
Pages Router에서 SSR 구현 (기존 방식)
Next.js의 Pages Router에서는 getServerSideProps 함수를 사용해 SSR을 구현한다. 이 함수는 매 요청마다 서버에서 실행되며, 페이지 렌더링에 필요한 데이터를 가져온다.
// pages/products/[id].js
export default function ProductPage({ product }) {
return (
<div>
<h1>{product.name}</h1>
<p>{product.description}</p>
<p>가격: {product.price}원</p>
</div>
);
}
// 이 함수는 매 요청마다 서버에서 실행됩니다
export async function getServerSideProps(context) {
const { id } = context.params;
// 외부 API에서 데이터 가져오기
const res = await fetch(`https://api.example.com/products/${id}`);
const product = await res.json();
// 404 처리
if (!product) {
return {
notFound: true,
};
}
// 페이지 컴포넌트에 props로 데이터 전달
return {
props: {
product,
},
};
}
위 예제에서는 제품 상세 페이지를 SSR로 구현했다. 사용자가 특정 제품 페이지에 접속하면,
- 서버에서 getServerSideProps 함수가 실행된다.
- URL 파라미터에서 제품 ID를 추출하고 API에서 해당 제품 정보를 가져온다.
- 가져온 데이터를 사용해 서버에서 React 컴포넌트를 렌더링한다.
- 완성된 HTML이 클라이언트로 전송된다.
App Router에서 SSR 구현 (Next.js 13 이상)
Next.js 13부터 도입된 App Router와 React Server Components를 사용하면 더 간결하게 SSR을 구현할 수 있다. 기본적으로 모든 컴포넌트는 서버 컴포넌트로 취급되며, 별도의 함수 없이도 서버에서 데이터를 가져올 수 있다.
// app/products/[id]/page.jsx
// 기본적으로 서버 컴포넌트
export default async function ProductPage({ params }) {
const { id } = params;
// 컴포넌트 내에서 직접 데이터 페칭
const product = await getProduct(id);
if (!product) {
// 404 처리
notFound();
}
return (
<div>
<h1>{product.name}</h1>
<p>{product.description}</p>
<p>가격: {product.price}원</p>
</div>
);
}
// 데이터 페칭 함수
async function getProduct(id) {
try {
const res = await fetch(`https://api.example.com/products/${id}`);
if (!res.ok) {
return null;
}
return res.json();
} catch (error) {
console.error('상품 정보 로딩 실패:', error);
return null;
}
}
이 접근 방식의 장점은:
- 코드가 더 간결해진다 (별도의 getServerSideProps 함수 필요 없음)
- 컴포넌트 내에서 직접 비동기 데이터를 가져올 수 있다
- 자동 코드 분할 및 스트리밍을 지원한다
Q: "하이드레이션(Hydration)"이란 정확히 무엇인가요?
A: 하이드레이션은 서버에서 생성된 정적 HTML에 클라이언트 측 JavaScript를 "주입"하는 과정입니다. 서버에서 렌더링된 HTML은 처음에는 인터랙티브하지 않습니다. React가 클라이언트에서 이벤트 리스너를 연결하고 상태를 초기화하면서 정적 마크업에 "생명을 불어넣는" 과정이 하이드레이션입니다.
Q: SSR을 사용할 때 발생할 수 있는 문제점은 무엇인가요?
A: 대표적인 문제는 하이드레이션 불일치(hydration mismatch)입니다. 서버에서 렌더링된 HTML과 클라이언트에서 React가 렌더링하려는 결과물이 다를 경우 발생합니다. 또한 window나 document 같은 브라우저 전용 객체를 서버 컴포넌트에서 접근하려 할 때 오류가 발생할 수 있습니다.
Q: 모든 페이지를 SSR로 구현해야 하나요?
A: 아니요. Next.js는 페이지별로 최적의 렌더링 전략을 선택할 수 있습니다. 자주 변경되지 않는 페이지는 정적 생성(Static Generation)을, 대시보드처럼 개인화된 데이터를 보여주는 페이지는 CSR이나 SSR을 선택하는 것이 좋습니다.
서버 사이드 데이터 페칭 전략
SSR의 핵심은 데이터 페칭이다. 서버에서 어떻게 데이터를 가져오느냐에 따라 애플리케이션의 성능과 사용자 경험이 크게 달라진다. Next.js는 다양한 데이터 페칭 방법을 제공하는데, 각 방법의 특징과 적합한 사용 사례를 살펴보자.
fetch API 활용 (Next.js 13+)
Next.js 13부터는 기본 fetch API를 확장해 캐싱과 재검증 옵션을 제공한다. 이는 서버 컴포넌트에서 데이터를 가져올 때 특히 유용하다.
// 기본 데이터 페칭 (캐시 없음, 매 요청마다 실행)
async function fetchProductWithoutCache(id) {
const res = await fetch(`https://api.example.com/products/${id}`, {
cache: 'no-store'
});
return res.json();
}
// 데이터 캐싱 (한 번 가져온 데이터 재사용)
async function fetchProductWithCache(id) {
const res = await fetch(`https://api.example.com/products/${id}`, {
next: { revalidate: 3600 } // 1시간마다 재검증
});
return res.json();
}
// 조건부 캐싱 (인증된 사용자는 캐시 X, 익명 사용자는 캐시 O)
async function fetchProductConditional(id, isAuthenticated) {
const res = await fetch(`https://api.example.com/products/${id}`, {
cache: isAuthenticated ? 'no-store' : 'force-cache'
});
return res.json();
}
이런 접근 방식을 사용하면 각 데이터 소스별로 적절한 캐싱 전략을 선택할 수 있다. 자주 변경되지 않는 데이터는 캐싱해 서버 부하를 줄이고, 실시간 데이터는 매번 새로 가져오는 방식이다.
병렬 데이터 페칭
여러 API에서 데이터를 가져와야 할 때는 순차적으로 요청하면 성능이 저하된다. 대신 병렬로 데이터를 가져오는 것이 좋다.
// 비효율적인 순차적 데이터 페칭
async function fetchSequential(productId) {
const product = await fetchProduct(productId);
const reviews = await fetchReviews(productId);
const relatedProducts = await fetchRelatedProducts(productId);
return { product, reviews, relatedProducts };
}
// 효율적인 병렬 데이터 페칭
async function fetchParallel(productId) {
const [product, reviews, relatedProducts] = await Promise.all([
fetchProduct(productId),
fetchReviews(productId),
fetchRelatedProducts(productId)
]);
return { product, reviews, relatedProducts };
}
병렬 방식을 사용하면 세 개의 API 요청이 동시에 실행되므로, 응답 시간이 가장 느린 API에 의해서만 전체 지연이 결정된다. 이는 순차적 방식보다 훨씬 빠르다.
서버 액션을 통한 데이터 변경 (Next.js 13+)
Next.js 13.4부터는 서버 액션(Server Actions)을 통해 서버에서 직접 데이터를 변경할 수 있게 되었다. 이는 폼 제출 같은 사용자 상호작용에 특히 유용하다.
'use server'; // 이 파일의 함수들이 서버에서 실행됨을 명시
async function addToCart(productId, quantity) {
const user = await getCurrentUser();
if (!user) {
throw new Error('로그인이 필요합니다');
}
try {
await db.cart.create({
data: {
userId: user.id,
productId,
quantity
}
});
// 캐시 무효화 (장바구니 데이터 갱신)
revalidatePath('/cart');
return { success: true };
} catch (error) {
console.error('장바구니 추가 실패:', error);
return { success: false, error: error.message };
}
}
// 클라이언트 컴포넌트에서 사용
'use client';
export default function AddToCartButton({ productId }) {
const handleClick = async () => {
const result = await addToCart(productId, 1);
if (result.success) {
toast.success('장바구니에 추가되었습니다');
} else {
toast.error(result.error || '장바구니 추가 실패');
}
};
return (
<button onClick={handleClick}>
장바구니에 추가
</button>
);
}
SSR 성능 최적화 기법
SSR은 초기 로딩 시간을 개선할 수 있지만, 잘못 구현하면 오히려 성능이 저하될 수 있다. 다음은 SSR 애플리케이션의 성능을 최적화하는 주요 기법들이다.
최적화 기법 | 설명 | 구현 방법 |
---|---|---|
스트리밍 SSR | 페이지를 작은 청크로 나눠 준비되는 대로 점진적으로 전송 | Loading.js 컴포넌트와 Suspense 활용 |
선택적 하이드레이션 | 사용자 상호작용이 필요한 부분부터 우선 하이드레이션 | use client 지시문으로 클라이언트 컴포넌트 최소화 |
캐싱 및 재검증 | 반복 요청 시 서버 부하 감소 및 응답 시간 단축 | fetch API의 cache 및 next.revalidate 옵션 활용 |
코드 분할 | 필요한 JavaScript만 로드하여 번들 크기 축소 | dynamic import 및 React.lazy 활용 |
Edge 런타임 | 사용자에게 가까운 위치에서 렌더링하여 지연 시간 감소 | export const runtime = 'edge' 설정 |
이미지 최적화 | 이미지 크기 및 형식 최적화로 로딩 시간 단축 | Next.js Image 컴포넌트 활용 |
스트리밍 SSR 구현 예시
스트리밍 SSR은 Next.js 13의 가장 강력한 기능 중 하나다. 전체 페이지가 준비될 때까지 기다리지 않고, 준비된 부분부터 점진적으로 사용자에게 보여줄 수 있다.
// app/products/[id]/page.jsx
import { Suspense } from 'react';
import ProductDetails from './ProductDetails';
import RelatedProducts from './RelatedProducts';
import ProductReviews from './ProductReviews';
import Loading from './loading';
export default function ProductPage({ params }) {
const { id } = params;
return (
<div>
{/* 우선순위가 높은 콘텐츠는 바로 로드 */}
<ProductDetails id={id} />
{/* 우선순위가 낮은 콘텐츠는 Suspense로 감싸서 스트리밍 */}
<Suspense fallback={<Loading message="관련 상품 로딩 중..." />}>
<RelatedProducts productId={id} />
</Suspense>
<Suspense fallback={<Loading message="리뷰 로딩 중..." />}>
<ProductReviews productId={id} />
</Suspense>
</div>
);
}
// app/products/[id]/loading.jsx
export default function Loading({ message = '로딩 중...' }) {
return (
<div style={{ padding: '20px', background: '#f5f5f5', borderRadius: '4px' }}>
{message}
</div>
);
}
위 예제에서는 제품 상세 정보를 즉시 렌더링하고, 관련 상품과 리뷰는 준비되는 대로 점진적으로 렌더링한다. 이를 통해 우선순위가 높은 콘텐츠를 사용자에게 빠르게 보여줄 수 있다.
실제 프로젝트 적용 사례와 교훈
이론은 좋지만, 실제 프로젝트에 SSR을 적용하면서 배운 교훈들이 더 중요하다. 다음은 Next.js의 SSR을 이용해 개발한 대규모 전자상거래 사이트에서 얻은 실질적인 교훈들이다.
SSR 도입 후 측정된 실제 개선 효과
- SEO 개선: 상품 페이지의 검색 엔진 노출이 약 35% 증가했다. 특히 모바일 검색에서 큰 효과를 보였다.
- First Contentful Paint(FCP) 단축: 3.2초에서 1.1초로 약 65% 개선되었다. 특히 네트워크 속도가 느린 환경에서 체감 효과가 컸다.
- 사용자 이탈률 감소: 페이지 로딩 지연으로 인한 이탈률이 18% 감소했다.
- 전환율 상승: 빠른 초기 로딩 덕분에 상품 페이지 방문자의 구매 전환율이 7.5% 증가했다.
도입 과정에서 겪은 어려움과 해결책
-
문제: 서버/클라이언트 환경 차이로 인한 window 객체 참조 오류
해결책: useEffect와 조건부 렌더링을 통해 클라이언트 환경에서만 특정 코드가 실행되도록 수정 -
문제: 서버 응답 시간 증가로 인한 TTFB(Time To First Byte) 지연
해결책: 캐싱 전략 도입 및 불필요한 API 호출 최소화, CDN 활용 -
문제: 하이드레이션 불일치 오류
해결책: 서버와 클라이언트에서 동일한 데이터를 사용하도록 상태 관리 재설계 -
문제: 개발 환경과 프로덕션 환경의 차이로 인한 예상치 못한 오류
해결책: 스테이징 환경을 프로덕션과 동일하게 구성하고 철저한 테스트 프로세스 도입
Q: SSR이 항상 CSR보다 빠른가요?
A: 아니요. SSR은 초기 로딩과 SEO에 유리하지만, 서버 응답 시간이 느리거나 네트워크 지연이 심한 경우 오히려 더 느릴 수 있습니다. 사용자 인터랙션이 많은 앱은 부분적으로 CSR을 활용하는 것이 더 나을 수 있습니다.
Q: 서버 비용이 크게 증가하지 않을까요?
A: SSR은 서버 자원을 더 많이 소비합니다. 하지만 효과적인 캐싱 전략과 ISR(Incremental Static Regeneration)을 활용하면 비용을 크게 줄일 수 있습니다. 또한 Vercel 같은 서버리스 플랫폼을 활용하면 트래픽에 따라 자동으로 확장되므로 비용 관리가 용이합니다.
Q: SSR과 SSG 중 어떤 것을 선택해야 할까요?
A: 자주 변경되지 않는 콘텐츠(블로그, 마케팅 페이지 등)는 SSG가 더 효율적입니다. 반면 사용자별 맞춤 콘텐츠나 실시간 데이터가 필요한 페이지는 SSR이 적합합니다. Next.js 13부터는 페이지의 일부만 동적으로 렌더링하는 하이브리드 접근법도 가능해졌습니다.
맺음말: 현실적인 SSR 도입 전략
지금까지 Next.js의 서버 사이드 렌더링에 대해 살펴보았다. SSR은 많은 장점이 있지만, 무조건적인 정답은 아니다. 현실적인 접근 방식은 하이브리드 렌더링이다. 모든 페이지를 SSR로 구현하려는 욕심을 버리고, 각 페이지와 컴포넌트의 특성에 맞는 최적의 렌더링 방식을 선택해야 한다.
SEO가 중요한 랜딩 페이지와 상품 상세 페이지는 SSR 또는 ISR로, 대시보드나 관리자 페이지 같은 비공개 페이지는 CSR로 구현하는 식이다. Next.js 13+에서는 이전보다 더 세밀한 제어가 가능해져 페이지 내에서도 일부만 동적으로 렌더링하고 나머지는 정적으로 제공할 수 있게 되었다.
결국 가장 중요한 것은 사용자 경험이다. 초기 로딩 속도, 인터랙티브 시간(TTI), 검색 엔진 최적화, 유지보수 용이성 등을 종합적으로 고려하여 프로젝트에 맞는 렌더링 전략을 수립해야 한다. Next.js는 이러한 다양한 요구사항을 유연하게 충족시킬 수 있는 생태계를 제공한다.
마지막으로, SSR 도입은 단순히 기술적 선택이 아니라 비즈니스 목표와 연결되어야 한다. 전환율 향상, 이탈률 감소, 검색 트래픽 증대 등 구체적인 KPI를 설정하고 SSR 도입 후 이를 측정하여 효과를 검증해야 한다. 이러한 데이터 기반 접근법이 SSR의 진정한 가치를 입증하는 방법이다.
Next.js의 SSR은 지속적으로 발전하고 있으며, App Router와 React Server Components는 웹 개발의 새로운 패러다임을 제시하고 있다. 이러한 변화에 발맞춰 실험하고 학습하되, 항상 사용자와 비즈니스 가치를 최우선으로 고려하는 균형 잡힌 시각을 유지하길 바란다.
'Developer > Web Frontend' 카테고리의 다른 글
[Next.js] app.js와 _document.js의 역할과 차이점 (0) | 2025.04.02 |
---|---|
[Next.js] 동적 라우팅 (0) | 2025.04.02 |
[Next.js] 정적 페이지 생성(SSG) 제대로 활용하기 (0) | 2025.03.31 |
[Next.js] pages 폴더 구조와 라우팅 (0) | 2025.03.31 |
[Next.js] 설치부터 첫 프로젝트 시작까지. (0) | 2025.03.13 |