본문으로 건너뛰기
안동민 개발노트 아이콘

안동민 개발노트

본문 시작
6장 : 데이터 페칭

서버 사이드 렌더링 (SSR)

요청 시점의 데이터로 HTML을 만드는 SSR을 구현하고 최신성·개인화·서버 비용을 기준으로 SSG와 구분합니다.

이전 절에서는 SSG로 빌드 시점에 페이지를 미리 생성하는 방법을 배웠습니다.

SSG는 매우 빠르지만 모든 서비스에 항상 맞는 해법은 아닙니다.

실시간 데이터나 사용자별 맞춤 콘텐츠가 필요하면, 요청 시점에 데이터를 가져와 렌더링하는 방식이 필요합니다.

이때 사용하는 전략이 서버 사이드 렌더링(Server Side Rendering, SSR)입니다.

이 절에서는 Next.js App Router에서 SSR이 어떻게 작동하는지, 그리고 어떤 상황에서 SSR을 선택해야 하는지 자세히 알아보겠습니다.


SSR이란 무엇인가요?

서버 사이드 렌더링(SSR)은 사용자가 페이지를 요청할 때마다 서버에서 데이터를 가져와 페이지를 HTML로 렌더링한 후 클라이언트로 전송하는 방식입니다.

클라이언트는 완성된 HTML을 받아 빠르게 페이지를 표시하고, 이후 JavaScript가 로드되면 상호작용 가능한 애플리케이션으로 전환됩니다 (이 과정을 하이드레이션(Hydration)이라고 합니다).

SSR의 주요 특징 및 이점
  • 요청 시 데이터 조회: 페이지 요청 시점에 서버가 원본을 조회하도록 구성할 수 있습니다. 다만 외부 API·데이터베이스의 캐시와 일관성 수준까지 자동으로 최신을 보장하지는 않으므로, 재고나 결제처럼 민감한 값은 해당 원본의 일관성 계약도 확인합니다.
  • 향상된 SEO: 미리 렌더링된 HTML이 검색 엔진 크롤러에게 전달되므로, SSG와 마찬가지로 SEO에 유리합니다.
  • 빠른 초기 로딩: 클라이언트에서 JavaScript를 다운로드하고 실행하기 전에 사용자가 콘텐츠를 볼 수 있으므로, 초기 로딩 속도 및 사용자 경험(UX)이 향상됩니다.
  • 동적인 콘텐츠 제공: 사용자 로그인 상태, 지역, A/B 테스트 그룹 등 요청에 따라 다르게 콘텐츠를 렌더링할 수 있습니다.
언제 SSR을 사용해야 할까요?
  • 데이터의 신선도가 중요할 때: 주식 가격, 실시간 재고, 개인화된 피드 등 자주 변경되거나 실시간으로 업데이트되어야 하는 데이터.
  • 사용자별 맞춤형 콘텐츠: 로그인한 사용자의 대시보드, 장바구니, 개인 설정 페이지 등 사용자 인증 또는 특정 조건에 따라 콘텐츠가 달라지는 경우.
  • 큰 데이터 세트: 모든 가능한 경로를 미리 빌드할 수 없는 방대한 동적 데이터가 있을 때.

App Router에서 SSR 구현하기

Next.js App Router의 컴포넌트는 기본적으로 서버 컴포넌트지만, 서버 컴포넌트라는 실행 모델이 곧 요청 시점 SSR을 뜻하지는 않습니다.

요청 정보가 필요하지 않은 라우트는 서버 컴포넌트여도 빌드 시점에 정적으로 프리렌더될 수 있습니다.

요청마다 실행하려면 cookies, headers 같은 동적 API를 사용하거나 connection()으로 요청 시점 경계를 명시합니다.

SSR 구현의 핵심
  • 요청 시점 경계: 요청마다 달라지는 계산만 있는 예제라면 페이지에서 await connection()을 호출합니다.
  • 동적 API: 쿠키나 헤더처럼 실제 요청 정보가 필요하면 cookies(), headers()를 사용합니다.
  • 데이터 캐시: 외부 API를 매 요청 새로 읽어야 한다면 fetchcache: 'no-store'를 지정합니다.
실습: 실시간 날씨 정보 페이지

매번 접속할 때마다 현재 날씨 정보를 가져와 보여주는 페이지를 만들어 SSR을 경험해 봅시다. (실제 날씨 API 대신 더미 데이터를 사용합니다.)

src/app/weather/page.tsx (새로 생성할 페이지)
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 🚀: Fetching weather data on demand..." 로그가 터미널(서버 콘솔)에 한 번 찍히고, 날씨 정보가 표시됩니다.

새로고침: 브라우저에서 페이지를 새로고침할 때마다 터미널에 다시 로그가 찍히고, timestamp와 날씨 데이터가 변경되는 것을 확인할 수 있습니다.

이는 매 요청마다 서버에서 새로운 데이터를 가져와 렌더링하고 있음을 의미합니다.


SSR과 SSG의 선택 기준

SSR과 SSG는 각각의 장단점이 명확하므로, 애플리케이션의 특정 요구사항에 따라 적절한 렌더링 전략을 선택해야 합니다.

특징서버 사이드 렌더링 (SSR)정적 사이트 생성 (SSG)
데이터 신선도요청 시 원본 조회 정책에 따름빌드 시 데이터와 요청 기반 ISR 재검증
성능빠른 초기 로딩, 그러나 요청마다 서버 처리가장 빠른 로딩 (CDN), 빌드 시간 소요
SEO우수우수
복잡성서버 부하 고려 필요빌드 시간, 데이터 업데이트 전략 고려 필요
활용 분야실시간 정보, 사용자별 맞춤 콘텐츠블로그, 문서, 포트폴리오, 마케팅 페이지
Next.js 구현connection()·동적 API·no-store fetch정적 프리렌더링과 필요 시 ISR
결정 가이드라인
  • 데이터가 거의 변하지 않고, 모든 사용자에게 동일하게 보여야 한다면? SSG (빌드 시 생성)

  • 데이터가 자주 변하고 요청 시 원본 조회가 필요하다면? SSR (요청 시 생성)

  • 데이터는 자주 변하지만, 사용자가 최신 데이터를 즉시 볼 필요는 없고, 빠른 로딩이 더 중요하다면? ISR (SSG + revalidate 옵션)

  • 사용자 상호작용이 많고, 초기 로딩 후 클라이언트에서 데이터가 자주 업데이트된다면? CSR (Client Side Rendering) - 다음 절에서 다룰 내용

렌더링 전략을 고를 때는 데이터 신선도, 사용자별 차이, 빌드 가능성, 재검증 허용 시간을 차례로 물어보면 SSR과 SSG 사이의 선택이 훨씬 명확해집니다.

Next.js App Router는 이러한 렌더링 전략들을 하나의 애플리케이션 내에서 페이지별로 유연하게 혼합할 수 있게 해줍니다.

이를 통해 각 페이지의 특성에 가장 적합한 최적의 성능을 달성할 수 있습니다.

마지막으로 SSR이 필요한 조건을 데이터 신선도, 사용자별 화면, 캐시 비용 관점에서 정리합니다.