본문 바로가기
프론트엔드/Next.JS

렌더링 전략 - SSG / SSR / CSR / ISR

by 학습하는 청년 2023. 7. 24.

최종 수정 : 2024-06-21

렌더링 전략

웹 페이지 또는 웹 애플리케이션을 웹 브라우저에 제공하는 방법을 의미한다.

 

# 장점

프리 렌더링을 하면 검색 엔진 최적화(SEO)에 도움이 되고, 페이지의 로딩 속도가 빠르다.

 

# 프리 렌더링(Pre-rendering)

웹 브라우저가 페이지를 로딩하기 이전에 렌더링 하는 것

1) 정적 생성(Static Generation) : 빌드할 때 렌더링 

2) 서버 사이드 렌더링(Server-side Rendering) : 리퀘스트가 들어오면 렌더링 

Next.js에서는 기본적으로 모든 페이지를 정적 생성한다.

리퀘스트가 들어올 때마다 매번 렌더링을 하는 것보다 미리 렌더링을 해서 저장해 둔 것을 보내 주는 게 훨씬 빠르기 때문이다.

--

코드 스플리팅

 


서버 사이드 렌더링(SSR, server side rendering)

Next.js 서버에 리퀘스트가 도착할 때마다 페이지를 렌더링해서 보내주는 방식

 

SSR은 웹 페이지를 제공하는 가장 흔한 방법이다. Next.js에서는 각 요청에 따라 서버에서 HTML 페이지를 동적으로 렌더링하고 웹 브라우저로 전송할 수 있다. 또한 서버에서 렌더링한 HTML 페이지에 스크립트 코드를 넣어 나중에 웹 페이지를 동적으로 처리할 수 있는데, 이를 하이드레이션(hydration)이라고 한다. 하이드레이션 덕분에 웹 앱은 싱글 페이지 애플리케이션(SPA) 처럼 작동할 수 있다. 클라이언트 사이드 렌더링(CSR)과 SSR의 장점을 모두 갖는다.

 

# 하이드레이션(hydration)

화학 용어로 수화 작용, 즉 어떤 물질이 물을 흡수하거나 물에 어떤 물질이 녹아서 마치 하나의 분자 상태처럼 바뀌는 현상을 의미한다.

이와 같이, 리액트에서의 하이드레이션은 서버 측에서 생성한 HTML 페이지에 클라이언트 측에서 실행하는 자바스크립트 코드를 추가해서 애플리케이션 상태를 관리하고 렌더링하는 기법을 의미한다. 즉, 서버 측에서 생성한 HTML 페이지를 구성하는 각각의 DOM 객체에 필요한 자바스크립트 코드를 추가해서 클라이언트가 동적으로 렌더링할 수 있는 것이다. 서버 측에서 렌더링한 DOM과 클라이언트가 렌더링한 DOM이 한데 섞여 싱글 페이지 애플리케이션처럼 보인다는 점이 한데 어우러져 하나의 분자처럼 보이는 것과 비슷하다.

# 언제 사용하는 게 좋은가?

  • 항상 최신 데이터를 보여 줘야 하는 경우
  • 데이터가 자주 바뀌는 경우
  • 리퀘스트의 데이터를 사용해야 하는 경우 (예: 헤더, 쿼리스트링, 쿠키 등)

# SSR의 장점

TTV가 빠르다.

자바스크립트 필요 없음

SEO 최적화가 좋다.

보안이 뛰어나다.

실시간 데이터를 사용할 수 있다.

사용자별 필요한 데이터를 사용할 수 있다.

 

1) 더 안전한 웹 애플리케이션

  • 페이지를 서버에서 렌더링한다는 것은 쿠키 관리, 주요 API, 데이터 검증 등과 같은 작업을 서버에서 처리한다는 뜻이다.
  • 또한, 중요한 데이터를 클라이언트에 노출할 필요가 없기 때문에 더 안전하다.

 

2) 더 뛰어난 웹 사이트 호환성

  • 클라이언트 환경이 자바스크립트를 사용하지 못하거나 오래된 브라우저를 사용하더라도 웹 페이지를 제공할 수 있다.

 

3) seo에 좋다.

  • 클라이언트에서 서버가 렌더링한 HTML 콘텐츠를 받기 때문에 검색엔진 웹 문서 수집기가 페이지를 렌더링할 필요가 없다. 덕분에 웹 애플리케이션의 SEO 점수가 높아진다.

Next.js는 기본적으로 빌드 시점에 정적으로 페이지를 만든다. 페이지에서 외부 API를 호출하거나 데이터베이스에 접근하는 등 동적 작업을 해야 한다면 getServerSideProps() 함수를 구현하고 export 하면 된다. 리턴 값으로는 객체를 반환한다. 정적 생성때와 마찬가지로 props 프로퍼티로 Props 객체를 넘겨주면 페이지 컴포넌트에서 받아서 사용할 수 있다.

export async function getServerSideProps() {
  const userRequest = await fetch('https://example.com/api/user');
  const userData = await userRequest.json();
  
  return {
    props: }
      user: userData,
    },
  };
}

function IndexPage(props) {
  return <div>Welcome, {props.user.name}!</div>
}

export default IndexPage;

 

1) getServerSideProps라는 비동기 함수를 익스포트한다. 빌드 과정에서 Next.js는 이 함수를 익스포트하는 모든 페이지를 찾아서 서버가 페이지 요청을 처리할 때 getServerSideProps 함수를 호출하도록 만든다. 해당 함수 내의 코드는 항상 서버에서만 실행된다.

 

2) getServerSideProps 함수는 props라는 속성값을 갖는 객체를 반환한다. Next.js는 이 props를 컴포넌트로 전달하여 서버와 클라이언트 모두가 props에 접근하고 사용할 수 있다. fetch API는 Next.js를 통해 서버에서 실행되기 때문에 fetch API를 별도의 폴리필로 끼워넣을 필요는 없다.

 

3) IndexPage 함수를 수정해서 props를 인자로 받는다. 이 props는 getServerSideProps 함수에서 반환한 props의 모든 내용을 가지고 있다.

 

위 코드를 배포하고 실행하면 Next.js는 외부 API를 호출해서 필요한 데이터를 가져온 다음 IndexPage를 서버에서 동적으로 렌더링한다. 그럼 사용자별로 서로 다른 페이지를 볼 수 있다.

 

# 주의해야 할 점

계속 서버에 요청해야 하므로 비교적 느릴 수 있다.

서버에 과부하가 걸릴 수 있다.

CDN에 캐시가 안 된다.

 

SSR을 사용하면 클라이언트가 요청할 때마다 페이지를 다시 렌더링할 수 있는 서버가 필요하다. 반면 CSR이나 정적 사이트 생성(SSG) 방식을 사용한다면 정적 HTML 파일을 Vercel이나 Netlify와 같은 클라우드 서비스에 배포하고 클라이언트에 제공할 수 있다. 웹 앱을 서버에 배포하면 다른 방식보다 SSR 애플리케이션이 더 많은 자원을 소모하고 더 많은 부하를 보인다. 그만큼 유지 보수 비용도 증가한다.

 

또한 SSR을 사용할 경우 페이지에 대한 요청을 처리하는 시간이 길어진다. 페이지가 외부 API 또는 데이터 소스에 접근해야 할 때 해당 페이지를 렌더링할 때마다 API나 데이터 소스를 다시 요청하기 때문이다.

 

마지막으로, 브라우저 전용 API를 사용해야 하는 컴포넌트가 있다면 해당 컴포넌트를 반드시 브라우저에서 렌더링하도록 명시적으로 지정해야 한다. Next.js는 페이지를 기본적으로 서버에서 렌더링하기 때문에, window, document와 같은 객체나 API를 제공하지 않는다. 이런 부분에서는 CSR이 필요하다.


클라이언트 사이드 렌더링(CSR, client side rendering)

실제 렌더링은 클라이언트로 전송한 웹 애플리케이션에서 이루어진다. 다시 말해, 클라이언트가 렌더링 하는 주체이다.

# CSR의 장점

한번만 로딩되면 빠른 UX를 제공한다.

 

1) 네이티브 애플리케이션처럼 느껴지는 웹 애플리케이션

  • 전체 자바스크립트 번들을 다운로드한다는 것은 웹 애플리케이션이 렌더링할 모든 페이지가 이미 브라우저에 다운로드되어 있다는 뜻이다. 다른 페이지로 이동하면 서버에서 그 페이지에 해당하는 새로운 콘텐츠를 다운로드하지 않고 그냥 페이지의 콘텐츠를 새로운 것으로 바꾼다. 이를 위해 페이지를 새로 고칠 필요가 없게 된다.

 

2) 쉬운 페이지 전환

  • 그래서 페이지 간 전환에 멋진 효과를 쉽게 넣을 수 있다. 애니메이션을 방해할 요소가 없기 때문에 가능하다.

 

3) 지연된 로딩(lazy loading)과 성능

  • CSR을 사용하면 웹 앱에서는 최소로 필요한 HTML 마크업만 렌더링한다.

 

4) 서버 부하 감소

  • 전체 렌더링 과정이 브라우저에서 일어나기 때문에 서버가 할 일이라고는 아주 간단한 HTML 페이지를 클라이언트에 전송하는 것뿐이다. 강력한 서버가 필요 없다. 서버리스 환경에서 웹 앱을 제공할 수도 있는 이유이다.

 

물론 이런 장점은 곧 단점이 될 수도 있다. 서버는 간단한 HTML 페이지만 보내기 때문에 네트워크 속도가 느린 환경에서는 전체 자바스크립트 코드와 CSS 파일을 받는 것에만 수 초가 소요될 수 있다. 그러면 사용자는 빈 페이지를 바라만 보고 있어야 한다. 이러한 점에서 SEO에도 악영향을 준다. 간단한 HTML 페이지만 보내기 때문에 검색 엔진에 제공할 수 있는 정보가 적은 까닭도 있다.


# 단점

페이지 로딩 시간(TTV, time to view)이 길다. 다르게 말하면, FCP(First Contentful Paint)가 길다고 말할 수 있다.

자바스크립트 활성화가 필수이다.

SEO 최적화가 힘들다.

보안에 취약하다.

CDN(Content Delivery Network)에 캐시가 안 된다. 

 

CSR의 문제점을 해결하기 위해 등장한 것이 SSG, SSR이다.


Next.js는 기본적으로 특정 페이지 내의 모든 리액트 컴포넌트를 서버에서 렌더링하거나 빌드 시점에 미리 렌더링 한다. 그래서 서버에서 렌더링할 때 이런 브라우저 전용 컴포넌트나 API에 접근하면 렌더링에 실패한다. Next.js를 사용할 때 발생할 수 있는 이런 문제는 다양한 방법으로 해결할 수 있지만 대개 브라우저에 특정 컴포넌트를 렌더링하도록 지정하는 방식으로 처리한다.

# 1) React.useEffect 훅

함수형 컴포넌트 내에서 DOM 조작이나 데이터 불러오기 같은 사이드 이펙트 기능을 구현하면 useEffect 함수를 사용해서 컴포넌트가 마운트(mount)된 후 해당 기능을 실행하도록 만들 수 있다. 즉, Next.js가 useEffect를 하이드레이션 이후 브라우저에서 실행하도록 만들었어야 한다. 이렇게 하면 특정 작업을 반드시 클라이언트에서 실행하도록 강제할 수 있다.

import Head from 'next/head';
import hljs from 'highlight.js';
import javascript from 'hightlight.js/lib/languages/javascript';

function Highlight({ code }) {
  hljs.registerLanguage('javascript', javascript);
  hljs.initHightlighting();
  
  return (
    <>
      <Head>
        <link rel='stylesheet' href='/highlight.css' />
      </Head>
      <pre>
        <code className='js'>{code}</code>
      </pre>
    </>
  );
};

클라이언트에서 실행되는 리액트 앱에서는 문제 없이 작동하지만 Next.js의 빌드 과정에서는 문제가 생긴다. Hightlight.js 라이브러리가 document라는전역 변수를 사용하는데, 이 변수는 Node.js에서 제공하지 않으며 오직 브라우저에서만 접근할 수 있기 때문이다. 이 문제는 hljs 호출을 useEffect 훅으로 감싸서 해결할 수 있다.

...
import { useEffect } from 'react';

function Highlight({ code }) {
  
  useEffect(() => {
    hljs.registerLanguage('javascript', javascript);
    hljs.initHightlighting();
  }, []);
  
  return (
    ...
  );
};

이렇게 하면 Next.js는 컴포넌트가 반환하는 HTML 마크업을 렌더링하고 Hightlight.js 스크립트를 페이지에 끼워 넣는다. 그리고 해당 컴포넌트가 브라우저에 마운트되면 라이브러리 함수를 클라이언트에서 호출하고 실행하도록 돕는다.

 

React.useEffect와 React.useState를 함께 써서 특정 컴포넌트를 정확히 클라이언트에서만 렌더링하도록 지정할 수도 있다.

import {useEffect, useState} from 'react';
import Hightlight from '../components/Highlight';

function useEffectPage() {
  const [isClient, setIsClient] = useState(false);
  
  useEffect(() => {
    setIsClient(true);
  }, []);
  
  return (
    <div>
      {isClient &&
        (<Highlight
          code={"console.log('Hello, world!')"}
          language='js'
        />)
      }
    </div>
  );
}

export default UseEffectPage;

이제 Highlight 컴포넌트는 브라우저에서만 렌더링된다.

 

# 2) process.browser 변수

서버에서 렌더링할 때 브라우저 전용 API로 인한 문제를 process.browser 값에 따라서 스크립트와 컴포넌트를 조건별로 실행하는 방법으로 해결할 수 있다. 이 변수는 불린(blooean)값으로, 코드를 클라이언트에서 실행하면 true, 서버에서 실행하면 false값을 갖는다.

function IndexPage() {
  const side = process.browser ? 'client' : 'server';
  
  return <div>You're currently on the {side}-side.</div>
}

export default IndexPage;

 

하지만 process.browser에 대한 Vercel 팀의 지원은 곧 중단될 예정이다. 대신 좀 더 정확한 의미를 갖는 typeof window를 사용할 수 있다.

function IndexPage() {
  const side = typeof window === "undefined" ? 'server' : 'client';
  
  return <div>You're currently on the {side}-side.</div>
}

export default IndexPage;

 

# 3) 동적 컴포넌트 로딩

Next.js는 내장 컴포넌트와 유틸리티 함수 형태로 제공한다. 이 중 하나가 dynamic 이다.

import dynamic from 'next/dynamic';

const Highlight = dynamic(
  () => import('../components/Highlight'),
  { ssr: false }
);

import styles from '../styles/Home.module.css';

function DynamicPage() {
  return (
    <div className={styles.main}>
      <Highlight code={`console.log('Hello, world! ')`} language='js' />
    </div>
  );
}

export default DynamicPage;

이 코드를 실행하면 Highlight 컴포넌트를 동적 임포트(dynamic import)로 불러온다. 즉, ssr:false 옵션으로 클라이언트에서만 코드를 실행한다고 명시하는 것이다. 이렇게 동적 임포트를 사용하면 Next.js는 해당하는 컴포넌트를 서버에서 렌더링하지 않는다. 따라서 리액트 하이드레이션이 끝날 때까지 기다려야 해당 컴포넌트를 사용할 수 있게 된다.

 

동적 웹 페이지를 만들 때 SSR보다 더 좋은 선택이다. 검색 엔진에 노출될 필요가 없는 페이지를 만드는 경우에는 웹 애플리케이션의 자바스크립트 코드를 먼저 다운로드한 다음 클라이언트에서 필요한 데이터를 직접 가져가도록 만든다. 이렇게 하면 서버 부하를 줄이고 애플리케이션을 더 쉽게 확장할 수 있다.

 

그렇다면 관리자 페이나 비공개 프로필 페이지 같이 검색 엔진을 신경 쓸 필요가 없는 동적 웹 페이지를 만들고 싶을 때는 어떻게 해야 할까? 이럴 때는 정적 사이트 생성(SSG) 렌더링 전략을 적용하면 된다.


정적 사이트 생성(SSG, Static Site Generation)

렌더링 하는 주체는 서버이다. 언제 렌더링 하느냐에 따라 SSG인지 SSR인지 달라진다. SSG는 일부 또는 전체 페이지를 빌드 시점에 미리 렌더링한다. 빌드할 때는 내용이 거의 변하지 않는 페이지는 정적 페이지 형태로 만들어 제공하는 것이 더 좋다. Next.js는 이런 페이지를 빌드 과정에서 정적 페이지로 미리 렌더링해서 HTML 마크업 형태로 제공한다. 또한 리액트 하이드레이션 덕분에 이런 정적 페이지에서도 여전히 사용자와 웹 페이지 간의 상호 작용이 가능하다. SSG는 SSR 및 CSR과 비교했을 때 다음과 같은 이점이 있다.

 

# 장점

  • TTV가 빠르다.
  • 자바스크립트 필요 없음
  • SEO 최적화가 좋다
  • 보안이 뛰어남
  • CDN에 캐시가 된다.

 

# 쉬운 확장

  • 정적 페이지는 단순 HTML 파일이므로 CDN을 통해 파일을 제공하거나 캐시에 저장하기 쉽다. 직접 웹 서버에 웹 애플리케이션을 제공하는 경우에도 정적 페이지는 별도의 연산 없이 정적 자원 형태로 제공되기 때문에 서버에 부하를 거의 주지 않는다.

 

# 뛰어난 성능

  • 빌드 시점에 HTML 페이지를 미리 렌더링하기 때문에 페이지를 요청해도 클라이언트나 서버가 무언가를 처리할 필요가 없다. 웹 서버는 정적 파일을 보내기만 하고 클라이언트 브라우저는 파일을 받아서 표시만 하면 된다. 따라서 각 요청별로 발생할 수 있는 지연 시간을 최소화할 수 있다.

 

# 더 안전한 API 요청

  • 페이지 렌더링을 위해 웹 서버가 민감하고 중요한 데이터를 클라이언트로 보낼 필요가 없다. 외부 API를 호출하거나, 데이터베이스에 접근하거나, 보호해야 할 데이터에 접근할 일이 없다. 필요한 모든 정보가 빌드 시점에 미리 페이지에 렌더링되어 있기 때문이다.

# 단점

데이터가 정적이다.

사용자별 정보 제공이 어렵다.

 

SSG는 높은 확장성과 뛰어난 성능을 보이는 애플리케이션을 만들고 싶을 때 가장 좋은 방법이다. 하지만 일단 웹 페이지를 만들고 나면 다음 배포 전까지 내용이 변하지 않는다는 점이 문제이다. 예를 들어 블로그에 새로운 글을 올렸는데 제목에 오타가 있는 경우, 블로그 제목의 단어 하나를 수정하기 위해 필요한 데이터를 가져오고 정적 페이지를 다시 생성하는 과정을 반복해야 한다. 정적으로 생성한 페이지는 빌드 시점에 미리 렌더링되어 정적 자원처럼 제공되기 때문이다. 이를 해결하기 위해 나온게 ISR, SSR이다.


정적 증분 재생성(ISR, Incremental Statc Regeneration)

렌더링 하는 주체는 서버이며, 주기적으로 렌더링한다. Next.js에서는 이런 문제를 해결할 수 있는 독특한 방법을 제공한다. 바로 증분 정적 재생성(ISR)이다. ISR을 사용하면 Next.js가 어느 정도의 주기로 정적 페이지를 다시 렌더링하고 해당 내용을 업데이트할지 정할 수 있다. SSG와 ISR을 함께 사용하면 SSR과 SSG를 섞어서 쓰는 효과를 낸다.

import fetch from 'isomorphic-unfetch';
import Dashboard from './components/Dashboard';

export async function getStaticProps() {
  const userReq = await fetch('api/user');
  const userData = await userReq.json();
  
  const dashboardReq = await fetch('/api/dashboard');
  const dashboardData = await dashboardReq.json();
  
  return {
    props: {
      user: userData,
      data: dashboardData,
    },
    revalidate: 600 // 10분
  };
}

function IndexPage(props) {
  return (
    <div>
      <Dashboard
        user={props.user}
        data={props.data}
      />
    </div>
  );
}

export default IndexPage;

 

1) Next.js는 빌드 과정에서 페이지의 내용을 getStaticProps 함수가 반환한 객체의 값으로 채운다. 그리고 이 페이지는 빌드를 거쳐 정적 페이지로 만들어진다.

 

2) 처음 10분간 해당 페이지를 요청하는 모든 사용자는 동일한 정적 페이지를 제공받는다.

 

3) 10분이 지나고 해당 페이지에 대한 새로운 요청이 들어오면 next.js는 이 페이지를 서버에서 다시 렌더링하고 getStaticProsp 함수를 다시 호출한다. 그리고 렌더링한 페이지를 저장해서 새로운 정적 페이지로 만들고 이전에 만든 정적 페이지를 새로 만든 페이지로 덮어쓴다.

 

4) 이후 10분간 동일한 페이지에 대한 모든 요청에 대해 새로 만든 정적 페이지를 제공한다.

 

Next.js는 ISR을 최대한 지연시켜서 처리하려고 한다. 따라서 10분이 지난 후에 페이지에 대한 새로운 요청이 없다면 Next.js는 페이지를 새로 빌드하지 않는다.

 

# 단점

주기적이기는 하지만 실시간 데이터가 아니다.

사용자별 정보 제공이 어렵다.

 

이 문제를 해결하기 위해 나온 게 SSR


추가 글

https://young-taek.tistory.com/312

 

Next.js의 정해진 함수(SSG, SSR)

최종 수정 : 2024-05-31 정적 생성과 서버사이드 렌더링은 같이할 수 없다.서버사이드 렌더링해야 하는 경우 외에는, 정적 생성 전략으로 적용하는 것이 좋다.정적 생성(Static Generation)프로젝트를

young-taek.tistory.com


참고 자료

실전에서 바로 쓰는 Next.js (p.33-48)

댓글