TIL #64 | [Next.js] Next.js의 렌더링 기법 (SSG, ISR, SSR, CSR)

kibi·2024년 1월 16일
1

TIL (Today I Learned)

목록 보기
64/83

Next.js는 기본적으로 렌더링 할 경우 서버에서 사전 렌더링(pre-rendering) 하는 방식을 사용합니다.
HTML을 미리 렌더링 한 후 사용자의 요청이 오면 Chunk 단위로 Javascript를 보내주어 이벤트가 작동하도록 합니다. 이를 Hydrationd 이라고 합니다.
서버에서 사전 렌더링 하는 방법은 SSG와 SSR로 나뉘어집니다. (둘의 차이는 HTML을 생성하는 시기에 있습니다.)

Next.js는 다양한 렌더링 방식을 사용는데 각각의 방식의 동작원리와 특징에 대해 알아보려합니다.

SSG

Static Site Generation: 정적 사이트 생성
빌드 시 HTML이 생성되고 이는 CDN으로 캐시됩니다. 그 후 모든 요청 시 캐시된 HTML을 재사용합니다.
웹 페이지를 미리 렌더링하여 HTML 파일로 저장한다.
빠른 페이지 로딩 및 SEO가 우수하다.

SSG는 요청 시 새로운 데이터를 가져올 필요가 없거나 사전 렌더링 된 HTML을 캐시하려는 경우 사용하는 것이 좋습니다.

SSG를 사용하기 위해선 getStaticProps를 사용하면 됩니다.
이 함수는 서버 측에서만 실행되는 함수로써 클라이언트에서는 실행되지 않습니다.

이 함수는 API와 같은 외부 데이터를 받아 Static Generation 하기 위한 용도이며 빌드 시에 딱 한번 호출됩니다.

export default function Blog({ posts }) {
  // Render posts...
}
 
// This function gets called at build time
export async function getStaticProps() {
  // Call an external API endpoint to get posts
  const res = await fetch('https://.../posts')
  const posts = await res.json()
 
  // By returning { props: { posts } }, the Blog component
  // will receive `posts` as a prop at build time
  return {
    props: {
      posts,
    },
  }
}

pages/posts/[id].ts 와 같이 동적 라우팅을 사용할 경우 getStaticPath()로 처리할 수 있습니다.

// This function gets called at build time
export async function getStaticPaths() {
  // Call an external API endpoint to get posts
  const res = await fetch('https://.../posts')
  const posts = await res.json()
 
  // Get the paths we want to pre-render based on posts
  const paths = posts.map((post) => ({
    params: { id: post.id },
  }))
 
  // We'll pre-render only these paths at build time.
  // { fallback: false } means other routes should 404.
  return { paths, fallback: false }
}

장점

  • 미리 렌더링하기 때문에 페이지 로딩 속도가 빠르다.
  • SEO에 유리하다
  • 캐싱이 용이하다

어떤 경우에 사용하기에 적절한가?
SSG는 SSR 보다도 높은 성능을 가지고 있기 때문에 Next.js에서 권장하고 있는 렌더링 방식입니다.
SSG는 정적 콘텐츠가 자주 변경되지 않는 웹 사이트에 적절합니다. 블로그, 포트폴리오 등의 요청이 올 때마다 동일한 정보로 반환하는 정보 전달 사이트를 만들 경우 사용됩니다.

ISR

Incremental Static Regeneration: 증분 정적 재생성
정적 페이지를 정기적으로 다시 빌드하는 방식이다.
일부 페이지를 업데이트할 수 있다.

ISR은 SSG에 포함되는 개념으로 SSG와 달리 설정한 시간마다 페이지를 새로 렌더링 합니다. 빌드 시 사전 렌더링 된 페이지에 대한 요청이 이루어지고 처음엔 캐시된 페이지가 표시됩니다. 설정한 시간이 지나면 이전 캐시를 무효화하고 업데이트된 페이지를 표시합니다.

getStaticProps 함수를 사용할 때 revalidata에 시간을 명시해줍니다.

export async function getStaticProps() {
  // If this request throws an uncaught error, Next.js will
  // not invalidate the currently shown page and
  // retry getStaticProps on the next request.
  const res = await fetch('https://.../posts')
  const posts = await res.json()
 
  if (!res.ok) {
    // If there is a server error, you might want to
    // throw an error instead of returning so that the cache is not updated
    // until the next successful request.
    throw new Error(`Failed to fetch posts, received status ${res.status}`)
  }
 
  // If the request was successful, return the posts
  // and revalidate every 10 seconds.
  return {
    props: {
      posts,
    },
    revalidate: 10,
  }
}

장점

  • 동적 데이터 업데이트 가능
  • 빠른 페이지 로딩
  • SEO에 유리

어떤 상황에서 사용하기 적절한가?
블로그와 같이 컨텐츠가 동적이지만 자주 변경되지 않는 사이트인 경우 ISR을 사용하기 적합니다.

SSR

Server Side Rendering: 서버 사이드 렌더링
모든 페이지 렌더링을 서버에서 수행하는 방식으로 페이지가 요청될 때마다 HTML 문서가 생성됩니다.
각각의 요청 시에 HTML을 만듭니다.
초기 로딩 시 클라이언트에서 렌더링된 페이지를 제공한다.

SSR을 사용하기 위해 매 요청마다 서버에 의해 호출되는 getServerSideProps 함수를 사용하는 것이 필요하고 요청이 있을 떄마다 서버에 의해 호출됩니다.
개인화된 사용자 데이터나 요청 시에만 알 수 있는 정보에 의존하는 페이지를 렌더링해야 하는 경우 사용합니다.

export default function Page({ data }) {
  // Render data...
}
 
// This gets called on every request
export async function getServerSideProps() {
  // Fetch data from external API
  const res = await fetch(`https://.../data`)
  const data = await res.json()
 
  // Pass data to the page via props
  return { props: { data } }
}

장점

  • 초기 로딩 속도 향상
  • SEO 최적화
  • 동적 데이터 지원

단점

  • 서버 부하
  • 많은 데이터를 가져올 경우 화면 멈춤 현상이 있을 수 있다.

어떤 상황에 사용하기에 적절한가?
SSR은 동적 데이터를 많이 사용하고 또한 SEO가 중요한 경우에 사용하기 적합합니다.
최신 상태를 반영해야 하는 웹 페이지, 분석 차트 등 사용자의 요청마다 동적으로 페이지를 생성하여 다른 내용을 보여줘야 하는 경우에 사용할 수 있습니다.

CSR

Client Side Rendering
모든 렌더링을 클라이언트 브라우저에서 처리하는 방식이다.
페이지는 초기 로딩 후 동적으로 업데이트 된다.

CSR에서 브라우저는 페이지에 필요한 최소한의 HTML 페이지와 JavaScript를 다운로드 합니다. 그 후 JavaSciprt를 사용하여 DOM을 업데이트하고 페이지를 렌더링합니다.
JavaSciprt의 다운로드 및 실행이 완료될 때까지 페이지가 완전히 렌더링 되지 않습니다.

Next.js는 기본적으로 SSR을 사용하기 때문에 CSR을 사용하기 위한 설정이 필요합니다.
1. CSR을 사용할 컴포넌트의 최상단에 use client를 명시하여 React Hook을 사용합니다.
2. SWR 또는 TanStack Query를 사용하여 데이터를 가져옵니다.

import React, { useState, useEffect } from 'react'
 
export function Page() {
  const [data, setData] = useState(null)
 
  useEffect(() => {
    const fetchData = async () => {
      const response = await fetch('https://api.example.com/data')
      if (!response.ok) {
        throw new Error(`HTTP error! status: ${response.status}`)
      }
      const result = await response.json()
      setData(result)
    }
 
    fetchData().catch((e) => {
      // handle the error as needed
      console.error('An error occurred while fetching the data: ', e)
    })
  }, [])
 
  return <p>{data ? `Your data: ${data}` : 'Loading...'}</p>
}

장점

  • 빠른 초기 로딩
  • 동적 데이터 처리에 유용하다.
  • 서버 부하가 감소한다.

단점

  • SEO에 불리하다
  • 전체 페이지가 로드되기 전 지연을 느낄 수 있습니다.

어떤 상황에서 사용해야 하는가?
CSR의 최대 단점은 검색 엔진 최적화에 매우 불리하다는 것입니다. CSR은 클라이언트에서 Javascript를 실행하여 정보를 보여주기 때문에 실행되지 않으면 빈페이지일 뿐입니다. 이런 점 때문에 검색 엔진이 크롤링하는 것에 좋지 못할 수 있습니다.
그렇기 때문에 SEO 중요도가 낮거나, 데이터의 사전렌더링이 필요하지 않은 경우 사용하기 적합합니다.


Next.js에서 SSR보다 SSG 방식을 권장하는 이유?

SSG는 빌드 시 HTML이 생성되고 CDN으로 캐시 됩니다. 그 후엔 사용자의 요청이 올 때마다 캐시된 HTML, 즉 빌드 시 이미 생성되었던 HTML을 재사용합니다.
하지만 그에 비해 SSR은 모든 요청 시 getServerSideProps를 실행하여 HTML을 생성하기 때문에 응답 속도가 느릴 수 있고, 서버는 더 많은 부하를 감당하게 됩니다.


SSR vs CSR

SSR과 CSR의 가장 큰 차이점은 데이터를 불러오는 방식에 있습니다.
기존 React를 사용할 때는 기본적으로 CSR이기 때문에 데이터를 가져올 때 useEffect를 사용하곤 했습니다.
Next.js에서 기본적으로 SSR을 사용할 경우 getServerSideProps, getStaticProps, getStaticPaths 등을 활용하여 데이터를 서버에서 먼저 불러와 렌더링 시키게 됩니다.
Next.js는 pre-Rendering 중 getServerSideProps 함수를 발견하게 되면 컴포넌트 함수 호출 전 먼저 getServerSideProps를 호출하게 됩니다.
이 함수는 사용자 요청마다 호출되며 서버에서 실행됩니다.


동적 라우터를 사용하고 싶을 경우?

동적 라우터를 사용하여 id에 따른 글을 보여주고 싶을 경우엔 getStaticPaths를 사용하여 pre-rendering 되기 원하는 경로를 명시해주면 됩니다.

export const getStaticPaths = async () => {
  return {
    paths: [
      { params: { id: '1' } },
      { params: { id: '2' } },
      { params: { id: '3' } },
    ],
    fallback: true,
  };
};

export const getStaticProps = async ({ params }) => {
  const id = params.id;
  const res = await axios.get(`http://localhost:3000/${id}`)
  
  return {
    props: {
      data: res.data
    }
  }
}

const Detail = ({ data }) => {
  // data를 가져와 사용
}

fallback

동적 페이지의 경우 빌드 시 생성되지 않은 주소로 사용자가 요청을 보내는 경우가 존재하는데, 이러한 경우 fallback 값에 따라 다른 대응을 할 수 있습니다.

빌드 시 생성되지 않은 정적 페이지를 요청하는 경우

true: 빌드 시 생성되지 않은 정적 페이지를 요청하는 경우, fallback 페이지를 제공합니다. 서버에서 정적 페이지를 생성하고, 생성되면 사용자에게 해당 페이지를 제공합니다.

false: 빌드 시에 생성되지 않은 정적 페이지를 요청하는 경우 404 페이지로 응답합니다.

blocking: SSR 방식으로 제작한 정적 페이지를 사용자에게 제공합니다.

0개의 댓글