서버 사이드 렌더링 (SSR)
요청 시점의 데이터로 HTML을 만드는 SSR을 구현하고 최신성·개인화·서버 비용을 기준으로 SSG와 구분합니다.
이전 절에서는 SSG로 빌드 시점에 페이지를 미리 생성하는 방법을 배웠습니다.
SSG는 매우 빠르지만 모든 서비스에 항상 맞는 해법은 아닙니다.
실시간 데이터나 사용자별 맞춤 콘텐츠가 필요하면, 요청 시점에 데이터를 가져와 렌더링하는 방식이 필요합니다.
이때 사용하는 전략이 서버 사이드 렌더링(Server Side Rendering, SSR)입니다.
이 절에서는 Next.js App Router에서 SSR이 어떻게 작동하는지, 그리고 어떤 상황에서 SSR을 선택해야 하는지 자세히 알아보겠습니다.
SSR이란 무엇인가요?
서버 사이드 렌더링(SSR)은 사용자 요청 시점에 서버에서 화면을 렌더링하는 방식입니다. 렌더에 쓰는 데이터의 캐시·원본 조회 정책은 따로 정합니다.
서버는 준비된 HTML을 스트리밍할 수 있습니다. 브라우저는 이를 표시하고, JavaScript가 클라이언트 컴포넌트를 하이드레이션(Hydration)해 이벤트를 연결합니다.
SSR의 주요 특징 및 이점- 요청 시 데이터 조회: 페이지 요청 시점에 서버가 원본을 조회하도록 구성할 수 있습니다. 다만 외부 API·데이터베이스의 캐시와 일관성 수준까지 자동으로 최신을 보장하지는 않으므로, 재고나 결제처럼 민감한 값은 해당 원본의 일관성 계약도 확인합니다.
- 향상된 SEO: 미리 렌더링된 HTML이 검색 엔진 크롤러에게 전달되므로, SSG와 마찬가지로 SEO에 유리합니다.
- 빠른 초기 로딩: 클라이언트에서 JavaScript를 다운로드하고 실행하기 전에 사용자가 콘텐츠를 볼 수 있으므로, 초기 로딩 속도 및 사용자 경험(UX)이 향상됩니다.
- 동적인 콘텐츠 제공: 사용자 로그인 상태, 지역, A/B 테스트 그룹 등 요청에 따라 다르게 콘텐츠를 렌더링할 수 있습니다.
- 데이터의 신선도가 중요할 때: 주식 가격, 실시간 재고, 개인화된 피드 등 자주 변경되거나 실시간으로 업데이트되어야 하는 데이터.
- 사용자별 맞춤형 콘텐츠: 로그인한 사용자의 대시보드, 장바구니, 개인 설정 페이지 등 사용자 인증 또는 특정 조건에 따라 콘텐츠가 달라지는 경우.
- 큰 데이터 세트: 모든 가능한 경로를 미리 빌드할 수 없는 방대한 동적 데이터가 있을 때.
App Router에서 SSR 구현하기
이 장은 cacheComponents를 활성화하지 않은 Next.js 16을 기준으로 합니다. App Router의 페이지·레이아웃은 기본적으로 서버 컴포넌트지만, 서버 컴포넌트라는 실행 모델이 곧 요청 시점 SSR을 뜻하지는 않습니다.
요청 정보가 필요하지 않은 라우트는 서버 컴포넌트여도 빌드 시점에 정적으로 프리렌더될 수 있습니다.
요청마다 실행하려면 cookies, headers 같은 동적 API를 사용하거나 connection()으로 요청 시점 경계를 명시합니다.
- 요청 시점 경계: 요청마다 달라지는 계산만 있는 예제라면 페이지에서
await connection()을 호출합니다. - 동적 API: 쿠키나 헤더처럼 실제 요청 정보가 필요하면
cookies(),headers()를 사용합니다. - 데이터 캐시: 외부 API를 매 요청 새로 읽어야 한다면
fetch에cache: 'no-store'를 지정합니다.
connection() 뒤에서 지연·난수·시간을 계산하는 페이지입니다. 원문에는 실제 날씨 API나 fetch 호출이 없습니다.
import { connection } from 'next/server';
interface WeatherData {
city: string;
temperature: number;
condition: string;
timestamp: string; // 서버가 모의 데이터를 만든 시각
}
// 이 함수는 요청 시점에 서버에서 실행됩니다.
async function getWeatherData(): Promise<WeatherData> {
console.log('SSR 🚀: Fetching weather data on demand...'); // 요청 시에만 이 로그가 보입니다.
// 실제 날씨 API 대신 더미 데이터와 지연 시간을 시뮬레이션합니다.
await new Promise(resolve => setTimeout(resolve, 1000)); // 1초 지연
const cities = ['Seoul', 'Busan', 'Jeju'];
const conditions = ['맑음', '흐림', '비', '눈'];
const randomCity = cities[Math.floor(Math.random() * cities.length)];
const randomCondition = conditions[Math.floor(Math.random() * conditions.length)];
return {
city: randomCity,
temperature: Math.floor(Math.random() * 15) + 10, // 10~24도
condition: randomCondition,
timestamp: new Date().toLocaleTimeString('ko-KR', { hour12: false }),
};
}
// 페이지 컴포넌트를 async 함수로 정의합니다.
// 이 컴포넌트는 요청 시점에 서버에서 렌더링됩니다.
export default async function WeatherPage() {
// 이 아래 코드는 실제 요청이 들어온 뒤 실행됩니다.
await connection();
const weather = await getWeatherData(); // 요청 시마다 데이터 페칭
return (
<div style={{ padding: '20px', maxWidth: '600px', margin: '20px auto', border: '1px solid #007bff', borderRadius: '10px', boxShadow: '0 4px 8px rgba(0,0,0,0.1)' }}>
<h1 style={{ color: '#007bff', textAlign: 'center', marginBottom: '20px' }}>현재 날씨 정보 (SSR)</h1>
<div style={{ fontSize: '1.2em', lineHeight: '1.8' }}>
<p><strong>도시:</strong> {weather.city}</p>
<p><strong>온도:</strong> {weather.temperature}°C</p>
<p><strong>상태:</strong> {weather.condition}</p>
<p style={{ fontSize: '0.9em', color: '#666' }}>
모의 데이터 생성 시간: <strong>{weather.timestamp}</strong>
</p>
</div>
<p style={{ marginTop: '25px', textAlign: 'center', color: '#888' }}>
이 페이지는 요청 시점에 서버에서 렌더링되므로, 새로고침할 때마다 새로운 데이터를 가져옵니다.
</p>
</div>
);
}실습:
src/app/weather 폴더를 만들고 그 안에 page.tsx 파일을 위 내용으로 생성합니다.
개발 서버(npm run dev)가 실행 중이라면, http://localhost:3000/weather로 접속하여 페이지를 확인해 보세요.
처음 접속: 서버 콘솔의 로그와 모의 날씨 표시를 비교합니다. 개발 모드에서 로그 횟수를 정확히 한 번으로 가정하지 않습니다.
새로고침: 요청 경계 뒤의 계산이 다시 수행되는지 확인합니다. 난수나 초 단위 표시 시각은 같을 수도 있으며, 값이 같다는 사실만으로 캐시 재사용이라고 단정할 수 없습니다.
SSR과 SSG의 선택 기준
SSR과 SSG는 각각의 장단점이 명확하므로, 애플리케이션의 특정 요구사항에 따라 적절한 렌더링 전략을 선택해야 합니다.
| 방식 | 비용과 데이터 조건 |
|---|---|
| SSR | 요청 시 서버 계산이 필요합니다. 실제 신선도는 원본 조회·캐시·일관성 정책에 따릅니다. |
| SSG | 초기 계산을 빌드에 옮깁니다. 요청 시 결과를 재사용하지만 빌드 시간과 갱신 전략을 고려합니다. |
| 공통 | 초기 HTML은 크롤러와 브라우저에 제공할 수 있습니다. 검색 순위나 체감 속도는 이 방식만으로 보장되지 않습니다. |
-
데이터가 거의 변하지 않고, 모든 사용자에게 동일하게 보여야 한다면? SSG (빌드 시 생성)
-
데이터가 자주 변하고 요청 시 원본 조회가 필요하다면? SSR (요청 시 생성)
-
데이터는 자주 변하지만, 사용자가 최신 데이터를 즉시 볼 필요는 없고, 빠른 로딩이 더 중요하다면? ISR (SSG +
revalidate옵션) -
사용자 상호작용이 많고, 초기 로딩 후 클라이언트에서 데이터가 자주 업데이트된다면? CSR (Client Side Rendering) - 6장 5절에서 다룰 내용