서버 사이드 렌더링 (SSR)
이전 절에서 정적 사이트 생성(SSG)을 통해 빌드 시점에 페이지를 미리 생성하는 방법을 배웠습니다. SSG는 뛰어난 성능을 제공하지만, 모든 웹 애플리케이션에 적합한 것은 아닙니다. 실시간으로 변하는 데이터나 사용자별로 맞춤화된 콘텐츠를 제공해야 하는 경우에는 페이지를 요청하는 시점에 데이터를 가져와 렌더링하는 방식이 필요합니다. 이때 활용되는 것이 바로 서버 사이드 렌더링(Server Side Rendering, SSR) 입니다.
이 절에서는 Next.js App Router에서 SSR이 어떻게 작동하는지, 그리고 어떤 상황에서 SSR을 선택해야 하는지 자세히 알아보겠습니다.
SSR이란 무엇인가요?
**서버 사이드 렌더링(SSR)**은 사용자가 페이지를 요청할 때마다 서버에서 데이터를 가져와 페이지를 HTML로 렌더링한 후 클라이언트로 전송하는 방식입니다. 클라이언트는 완성된 HTML을 받아 빠르게 페이지를 표시하고, 이후 JavaScript가 로드되면 상호작용 가능한 애플리케이션으로 전환됩니다 (이 과정을 **하이드레이션(Hydration)**이라고 합니다).
SSR의 주요 특징 및 이점
- 항상 최신 데이터: 페이지를 요청하는 시점에 데이터를 가져오므로, 항상 최신 상태의 데이터를 사용자에게 보여줄 수 있습니다. 뉴스 사이트, 실시간 대시보드, 전자상거래의 재고 정보 등 데이터의 신선도가 중요한 경우에 적합합니다.
- 향상된 SEO: 미리 렌더링된 HTML이 검색 엔진 크롤러에게 전달되므로, SSG와 마찬가지로 SEO에 유리합니다.
- 빠른 초기 로딩: 클라이언트에서 JavaScript를 다운로드하고 실행하기 전에 사용자가 콘텐츠를 볼 수 있으므로, 초기 로딩 속도 및 사용자 경험(UX)이 향상됩니다.
- 동적인 콘텐츠 제공: 사용자 로그인 상태, 지역, A/B 테스트 그룹 등 요청에 따라 다르게 콘텐츠를 렌더링할 수 있습니다.
언제 SSR을 사용해야 할까요?
- 데이터의 신선도가 중요할 때: 주식 가격, 실시간 재고, 개인화된 피드 등 자주 변경되거나 실시간으로 업데이트되어야 하는 데이터.
- 사용자별 맞춤형 콘텐츠: 로그인한 사용자의 대시보드, 장바구니, 개인 설정 페이지 등 사용자 인증 또는 특정 조건에 따라 콘텐츠가 달라지는 경우.
- 큰 데이터 세트: 모든 가능한 경로를 미리 빌드할 수 없는 방대한 동적 데이터가 있을 때.
App Router에서 SSR 구현하기
Next.js App Router에서는 모든 컴포넌트가 기본적으로 서버 컴포넌트로 동작하며, 이는 SSR을 구현하는 가장 자연스러운 방법입니다. 별도의 설정 없이 fetch
API를 사용하면, Next.js는 기본적으로 **요청 시점(On-Demand)**에 데이터를 가져와 서버에서 렌더링합니다.
SSR 구현의 핵심
fetch
API 사용: 서버 컴포넌트 내에서fetch
를 사용하여 데이터를 가져옵니다.- 캐싱 전략:
fetch
의cache
옵션을'no-store'
로 설정하거나,revalidate
옵션을 사용하지 않으면 SSR로 동작합니다.cache: 'no-store'
는 캐시를 사용하지 않고 항상 새로운 데이터를 가져오도록 강제합니다.revalidate
옵션이 없는fetch
는 기본적으로 요청 시점에 한 번 데이터를 가져와 응답합니다 (즉, 캐시되지 않습니다).
실습: 실시간 날씨 정보 페이지
매번 접속할 때마다 현재 날씨 정보를 가져와 보여주는 페이지를 만들어 SSR을 경험해 봅시다. (실제 날씨 API 대신 더미 데이터를 사용합니다.)
// src/app/weather/page.tsx (새로 생성할 페이지)
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() {
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 🚀: Fetching weather data on demand..." 로그가 터미널(서버 콘솔)에 한 번 찍히고, 날씨 정보가 표시됩니다.
- 새로고침: 브라우저에서 페이지를 새로고침할 때마다 터미널에 다시 로그가 찍히고,
timestamp
와 날씨 데이터가 변경되는 것을 확인할 수 있습니다. 이는 매 요청마다 서버에서 새로운 데이터를 가져와 렌더링하고 있음을 의미합니다.
SSR과 SSG의 선택 기준
SSR과 SSG는 각각의 장단점이 명확하므로, 애플리케이션의 특정 요구사항에 따라 적절한 렌더링 전략을 선택해야 합니다.
특징 | 서버 사이드 렌더링 (SSR) | 정적 사이트 생성 (SSG) |
---|---|---|
데이터 신선도 | 항상 최신 데이터 | 빌드 시 데이터 (ISR로 주기적 업데이트) |
성능 | 빠른 초기 로딩, 그러나 요청마다 서버 처리 | 가장 빠른 로딩 (CDN), 빌드 시간 소요 |
SEO | 우수 | 우수 |
복잡성 | 서버 부하 고려 필요 | 빌드 시간, 데이터 업데이트 전략 고려 필요 |
활용 분야 | 실시간 정보, 사용자별 맞춤 콘텐츠 | 블로그, 문서, 포트폴리오, 마케팅 페이지 |
Next.js 구현 | 기본 동작 (fetch 사용) | generateStaticParams 와 fetch 사용 |
결정 가이드라인
-
데이터가 거의 변하지 않고, 모든 사용자에게 동일하게 보여야 한다면? SSG (빌드 시 생성)
-
데이터가 자주 변하고, 항상 최신 데이터를 보여줘야 한다면? SSR (요청 시 생성)
-
데이터는 자주 변하지만, 사용자가 최신 데이터를 "즉시" 볼 필요는 없고, 빠른 로딩이 더 중요하다면? ISR (SSG +
revalidate
옵션) -
사용자 상호작용이 많고, 초기 로딩 후 클라이언트에서 데이터가 자주 업데이트된다면? CSR (Client Side Rendering) - 다음 절에서 다룰 내용
Next.js App Router는 이러한 렌더링 전략들을 하나의 애플리케이션 내에서 페이지별로 유연하게 혼합할 수 있게 해줍니다. 이를 통해 각 페이지의 특성에 가장 적합한 최적의 성능을 달성할 수 있습니다.