Next.js의 다양한 Rendering

BDD·2024년 1월 24일
0

해당 글은 도리가 작성했습니다. 🐠

프론트엔드 팀에서는 Next.js를 사용하기로 함에 따라 다양한 Rendering 방식을 알아야 할 필요성을 느꼈고, 따라서 공식문서를 통해 알아보았습니다.


Pre-rendering

기본적으로 Next.js는 모든 페이지를 pre-rendering 합니다.

  • Client-side의 JavaScript로 실행되는 것 대신, 사전에 각 페이지의 HTML을 생성하는 것을 의미함.
  • 각 생성된 HTML은 최소한의 JavaScript code로만 이루어짐.
  • 브라우저에서 하나의 페이지가 로드될 때 그 페이지의 JavaScript code가 실행되고 페이지를 반응성 있게 만드는 hydration이 이루어짐.

Pre-rendering의 두 가지 방식

이 둘의 가장 큰 차이점으로는 페이지의 HTML이 생성되는 방식에서 차이가 있습니다.

  • Static Generation: HTML은 build-time에 생성되며 모든 요청에서 미리 생성된 HTML을 재사용함. (Next.js에서 권장하는 방법)
  • Server-side Rendering: HTML은 각각의 요청마다 생성함. 즉, 재사용은 하지 않음.

Next.js는 각 페이지에 사용하고자 하는 pre-rendering 방식을 선택할 수 있게 해주기에, 더 나아가 대부분의 페이지에서는 Static Generation을 사용하고 다른 페이지에서는 Server-side Rendering을 이용하여 Hybrid 하게 만들 수도 있습니다.

Next.js에서는 성능상의 이유로 Server-side Rendering 방식보다 Static Generation 방식을 사용하는 것을 추천합니다.
=> 성능 향상을 위한 추가 구성없이 CDN에서 캐싱을 하기 때문
(이를 통해 미리 생성해 놓은 HTML을 다시 재사용 가능)


Server Side Rendering (Dynamic Rendering)

페이지에서 server-side rendering을 사용하는 경우 각 요청마다 페이지 HTML이 생성됩니다. 페이지에 대해 server-side rendering을 사용하려면 getServerSideProps라는 비동기 함수를 내보내야 하며, 이 함수는 모든 요청 시 서버에서 호출됩니다.

예를 들어 페이지에서 자주 업데이트되는 데이터(외부 API에서 가져옴)를 사전 렌더링해야 한다고 가정하면, 이 데이터를 가져와 페이지로 전달하는 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 } }
}

Client Side Rendering

1. useEffect()

server-side rendering method인 getStaticPropsgetServerSideProps 대신 hook을 이용하는 방법으로 다음과 같이 작성할 수 있습니다.

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>
}

위의 방법은 기존의 React Hooks를 사용하는 형태로, 더 나은 성능이나 캐싱, 업데이트 등을 위해서는 data-fetching library를 사용하는 것이 좋습니다.

2. data fetching library(SWR or TanStack Query)

data fetching library를 통해 클라이언트에서 데이터를 가져오는 것으로 Next.js에서 권장하는 방법입니다.

  • SWR 을 이용한 예시
import useSWR from 'swr'

const fetcher = async (url) => {
  const response = await fetch(url);
  if (!response.ok) {
    throw new Error(`HTTP error! status: ${response.status}`);
  }
  const result = await response.json();
  return result
};

export function Page() {
  const { data, error, isLoading } = useSWR(
    'https://api.example.com/data',
    fetcher
  )
 
  if (error) return <p>Failed to load.</p>
  if (isLoading) return <p>Loading...</p>
 
  return <p>Your Data: {data}</p>
}
import { useQuery } from 'react-query'

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()
  return result
}

export function Page() {
  const { data, error, isLoading } = useQuery('data', fetchData)

	if (error) return <p>Failed to load.</p>
  if (isLoading) return <p>Loading...</p>

  return <p>{data ? `Your data: ${data}` : 'No data available'}</p>
}

SSR vs CSR

  1. 렌더링 위치
    • SSR: 서버에서 초기 HTML 마크업을 생성하고 브라우저에 전송하며, 사용자가 페이지를 요청할 때마다 서버에서 새로운 HTML을 생성하여 전달합니다.
    • CSR: 초기에 서버는 빈 HTML을 전송하고, 클라이언트에서 JavaScript를 사용하여 동적으로 컨텐츠를 렌더링합니다. 이후에는 클라이언트가 서버에 데이터를 요청하고, 받은 데이터를 이용해 화면을 업데이트합니다.
  2. 로딩 시간
    • SSR: 초기 페이지 로딩이 빠르며, 검색 엔진 최적화(SEO)에 유리합니다. 하지만 페이지 전환이 발생할 때마다 서버에 새로운 페이지를 요청해야 하므로 네트워크 비용이 발생할 수 있습니다.
    • CSR: 초기 페이지 로딩은 빠르지만, JavaScript 파일을 다운로드하고 실행하는 시간이 추가로 소요될 수 있습니다. 검색 엔진은 초기에 빈 페이지를 볼 수 있으므로 SEO 측면에서는 조심해야 합니다.
  3. 검색 엔진 최적화 (SEO)
    • SSR: 검색 엔진은 초기에 서버에서 받은 HTML을 읽을 수 있기 때문에 SEO에 유리합니다.
    • CSR: 초기에는 빈 페이지가 전송되므로 검색 엔진은 JavaScript를 실행해야 페이지 컨텐츠를 볼 수 있어 SEO에 미치는 영향이 있을 수 있습니다.

Static Site Generation

Static Generation을 사용한다면, 페이지의 HTML은 build-time(빌드 시점)에 생성됩니다. 즉, 페이지의 HTML이 next build를 실행할 때 생성된다는 의미입니다.

생성된 HTML은 각각의 모든 요청(request)에서 재사용될 것이고, CDN에 의해 캐싱될 수도 있습니다. 생성되는 페이지에서는 데이터 포함 여부에 따라 방식이 달라집니다.

Static Generation without data

function About() {
  return <div>About</div>
}
 
export default About

이 페이지는 pre-rendering 하기 위해 외부 데이터를 가져올 필요가 없습니다. 이와 같은 경우 build-time 동안 페이지당 단일 HTML 파일을 생성합니다.

Static Generation with data

일부 페이지에서 pre-rendering을 위해 외부 데이터를 가져와야 할 때 두 가지 시나리오가 있습니다.

  1. page content가 외부 데이터에 의존하는 경우 ⇒ getStaticProps
  2. page paths가 외부 데이터에 의존하는 경우 ⇒ getStaticPaths
    => 일반적으로 getStaticProps 와 함께 사용

Scenario 1 - 페이지의 content가 외부 데이터에 따라 다른 경우

예시로, blog page는 CMS(Content Management System)로부터 blog post 리스트를 가져와야 할 때

// TODO: Need to fetch `posts` (by calling some API endpoint)
//       before this page can be pre-rendered.
export default function Blog({ posts }) {
  return (
    <ul>
      {posts.map((post) => (
        <li>{post.title}</li>
      ))}
    </ul>
  )
}

Next.js는 이 데이터를 pre-rendering 하기 위해 동일한 파일에서 getStaticProps함수를 비동기 방식으로 export하여 사용합니다. 이 함수는 build-time 시 호출되며, pre-rendering 시 가져온 데이터를 page의 contents로 전달할 수 있습니다.

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,
    },
  }
}

Scenario 2 - 페이지의 paths가 외부 데이터에 따라 다른 경우

Next.js는 Dynamic Routing을 통해 페이지를 생성합니다. 예를 들어, pages/posts/[id.js]라는 파일을 만들어 id를 기반으로 하나의 블로그 게시물을 보여줄 때 posts/1에 접속하면 id:1의 블로그 게시물을 보여줄 수 있는 겁니다.

하지만 id 값은 pre-rendering 시 외부 데이터에 따라 달라질 수 있습니다. 이를 처리하기 위해 동적 페이지(pages/posts/[id].js)로 부터 getStaticPaths 함수를 비동기 방식으로 사용하는데, 이 함수는 build-time에 실행되며 pre-rendering을 위해 원하는 path를 지정할 수 있습니다.

// 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 }
}

또한 page/posts/[id.js]에서도 getStaticPropsexport하면 이 id로 게시물에 대한 데이터를 가져와 페이지를 pre-rendering 하는 데 사용할 수 있습니다.

export default function Post({ post }) {
  // Render post...
}
 
export async function getStaticPaths() {
  // ...
}
 
// This also gets called at build time
export async function getStaticProps({ params }) {
  // params contains the post `id`.
  // If the route is like /posts/1, then params.id is 1
  const res = await fetch(`https://.../posts/${params.id}`)
  const post = await res.json()
 
  // Pass post data to the page via props
  return { props: { post } }
}

Static Generation을 사용해야 할 때

Next.js 공식 문서에서는 Static Generation(with or without data)을 사용하는 것을 가능한 권장합니다. build-time에 생성된 HTML을 CDN 캐시를 통해 제공하기 때문에, 각 요청에 따라 Server-side rendering 하므로 더 빠른 성능을 보여줄 수 있기 때문입니다.

예를 들어 이와 같은 페이지에서 사용됩니다.

  • Marketing pages
  • Blog posts and portfolios
  • E-commerce product listings
  • Help and documentation

💡 스스로에게 "사용자의 요청에 앞서 이 페이지를 pre-rendering해도 되는가?"를 물어봤을 때, 대답이 "예"라면 Static Generation을 사용하면 됩니다.

반면, 사용자가 요청하는 데이터를 보여주어야 하는 경우에는 적합하지 않습니다. 페이지에 따라 데이터가 자주 업데이트 되는 경우 매 순간 페이지의 content가 변경될 수 있기 때문입니다.

  1. Client-side Rendering과 함께 Static Generation을 사용하는 경우
    페이지의 일부를 pre-rendering 하지 않고 Client-side에서 JavaScript를 실행하여 content를 채울 수 있습니다.
  2. Server-side Rendering을 하는 경우
    페이지를 매번 서버에서 pre-rendering하면 CDN을 통한 캐싱이 의미가 없기 때문에 rendering 성능이 저하되지만 데이터와 content는 항상 최신을 유지할 수 있습니다.

보시다시피 getServerSidePropsgetStaticProps와 유사하지만 getServerSideProps는 빌드 시간이 아닌 모든 요청에 대해 실행된다는 점이 다릅니다.

💡 서버에서 pre-rendering 하는 것까지가 Next.js의 특징이고,
pre-rendering을 동적으로 페이지를 생성하느냐, 정적으로 페이지를 생성하느냐의 차이가 SSRSSG의 차이라고 생각하면 됩니다.

profile
부산 개발 동아리

0개의 댓글