본문으로 건너뛰기

안동민 개발노트

본문 시작

서버 사이드 렌더링 (SSR)

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

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

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

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

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

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

서버 사이드 렌더링 (SSR)

이전 절에서는 SSG로 빌드 시점에 페이지를 미리 생성하는 방법을 다뤘습니다. SSG는 매우 빠르지만 모든 서비스에 항상 맞는 해법은 아닙니다.

  1. SSR 개념

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

  2. App Router에서 SSR 구현하기

    요청 정보가 필요하면 cookies() · headers() 를, 요청마다 계산만 해야 하면 await connection() 을 사용합니다. 요청 시점 경계

  3. SSR과 SSG의 선택 기준

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

  4. 서버 사이드 렌더링 기준

    정확성 요청 API가 없는 Server Component는 빌드 시점에 정적으로 프리렌더될 수 있습니다. 비용 connection() 또는 동적 API 아래의 계산이 요청마다 실행되므로 경계를 작게 유지합니다. 확장성 클라이언트는 완성된 HTML을 먼저 표시하고, 이후 JavaScript 로드 뒤 상호작용을 이어받습니다. 예외 외부 데이터를 매번 새로 읽어야 할 때만 no-store 를 사용하며 렌더 경계와 데이터 캐시를 구분합니다.

  5. 정확성 요청 API

    없는 Server Component는 빌드 시점에 정적으로 프리렌더될 수 있습니다.

  6. 비용 connection() 또

    동적 API 아래의 계산이 요청마다 실행되므로 경계를 작게 유지합니다.

  7. 확장성 클라이언트

    완성된 HTML을 먼저 표시하고, 이후 JavaScript 로드 뒤 상호작용을 이어받습니다.

  8. 예외 외부 데이터

    매번 새로 읽어야 할 때만 no-store 를 사용하며 렌더 경계와 데이터 캐시를 구분합니다.


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의 선택 기준

결과가 달라져야 하는 시점으로 렌더링 전략을 고른다

기술 이름보다 어떤 사건이 새 HTML을 필요로 하는지를 먼저 정의한다.

  1. 빌드 때
    Static

    공개 콘텐츠가 배포 사이에 거의 변하지 않음

  2. 일정 시간
    ISR

    stale을 허용하며 주기적으로 재검증

  3. 변경 직후
    On-demand

    path는 revalidatePath, SWR tag는 revalidateTag(tag, 'max')

  4. 매 요청
    Dynamic

    connection() ·cookie·header로 요청 경계 진입

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은 요청 문맥이 화면을 바꿀 때 선택한다

최신성·개인화 이득과 API 지연·동시 접속 비용을 동시에 평가한다.

  1. 재고·시세
    Fresh SSR

    오래된 값이 곧 오류가 되는 데이터

  2. 세션·권한
    Personal SSR

    쿠키와 사용자 문맥이 HTML을 바꾸는 화면

  3. 요청마다 계산
    connection()

    요청 데이터 없이도 실행 시점을 요청 뒤로 미룸

  4. 공개·공유
    SSG / ISR

    사용자 차이가 없으면 캐시 가능한 결과 우선

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

렌더링 전략은 신선도와 사용자 차이로 선택한다

SSR, SSG, ISR, CSR은 빠름과 최신성의 균형이 다르므로 페이지 요구사항부터 문장으로 고정합니다.

  1. freshness
  2. per user
  3. build time
  4. SSR
    요청마다 최신

    SSR 동적 API 또는 connection() 으로 요청 경계를 만들고 필요한 범위만 서버에서 매번 렌더링합니다.

  5. SSG
    빌드 때 고정

    SSG 모든 사용자에게 같은 공개 페이지라면 빌드 결과를 CDN에 올려 요청마다 서버 렌더링을 피합니다.

  6. ISR
    시간 기준 갱신

    ISR 완전 실시간은 아니어도 되는 콘텐츠는 revalidate로 주기적 갱신을 선택합니다.

  7. 01 · 같음

    · 같음 모든 사용자가 같은지 봅니다.

  8. 02 · 변함

    · 변함 데이터 변경 주기를 봅니다.

  9. 03 · 허용

    · 허용 오래된 시간 허용치를 정합니다.

  10. 04 · 선택

    · 선택 페이지별 전략을 고릅니다.

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

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

SSR은 요청마다 달라지는 범위에만 적용한다

최신성·개인화 이득과 매 요청의 서버 지연·비용을 같은 판단 축에 놓는다.

  1. 세션·권한
    개인화 SSR

    사용자마다 HTML이 달라지는 영역

  2. 즉시 최신
    no-store fetch

    오래된 값이 위험한 데이터만 캐시 제외

  3. 느린 I/O
    Streaming 분리

    빠른 shell을 먼저 보내고 느린 영역을 뒤에 연결

  4. 공개·안정
    정적·재검증

    공유 가능한 결과는 요청 경로 밖에서 생성

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

렌더링 방식은 최신성·개인화·비용을 함께 고른다

같은 페이지도 데이터별 요구가 다르면 정적 shell과 동적 영역을 조합할 수 있다.

  1. 공개·안정
    SSG

    문서·마케팅처럼 빌드 결과를 많은 요청이 공유

  2. 요청별
    SSR

    connection() ·cookie·header 아래에서 요청마다 실행

  3. 부분 최신
    Hybrid

    정적 shell에 동적 데이터 영역만 연결

  4. 낡으면 위험
    No Store

    재고·결제 직전 값처럼 매번 새로 조회