Front-End/Next

[Next.js] Pre-Rendering

Voyage_dev 2022. 11. 4. 17:02

 

Pre-Rendering에 들어가기 전, SPA의 특성을 먼저 생각해보자.

 

SPA (Single Page Application)은 처음이 느리고 그 다음부터 빠르다.

 

SPA에서는 사용자가 실제 컨텐츠를 보려면 서버로 요청을 한 뒤에 HTML, JS, ASSETS 파일을 다 다운 받기 전 까지 대기해야한다. 그리고 다 다운받고 난 뒤 JavaScript를 이용해서 매 화면의 컨텐츠를 동적으로 바꾼다.

 

이러한 것들이 SPA를 느리게 하는 이유가 되는데, 이런 문제를 해결하기 위해서는 SSR위에 HTML 파일을 반환해줄 서버를 둬야한다.

Next.js에서는 이런 문제를 Pre-Rendering으로 SSR보다 복잡하지 않은 비용으로 해결할 수 있다.

Next.js의 Pre-Rendering

React는 CSR로, 처음에 브라우저가 빈 HTML 파일을 받아 아무것도 보여주지 않다가, JavaScript 다운을 완료하면 사용자의 기기에서 렌더링이 진행되어 한 번에 화면을 보여준다.

출처 : https://nextjs.org/learn/foundations/how-nextjs-works/rendering

 

하지만 Next.js는 모든 페이지를 사용자에게 전해지기 전에 미리 렌더링 즉, Pre-Render한다. 이는 Next.js가 모든 일을 클라이언트 측에서 모든 작업을 수행하는 것이 아니라, 각 페이지의 HTML을 미리 생성하는 것이다. 생성된 HTML은 해당 페이지에 필요한 최소한의 자바스크립트 코드와 연결된다. 그후 브라우저에 의해 페이지가 로드되면, 자바스크립트 코드가 실행되어 페이지와 유저가 상호작용할 수 있게 된다.

 

이러한 과정을 Hydration이라 한다.

출처 :  https://nextjs.org/learn/foundations/how-nextjs-works/rendering

 

Pre-Rendering 과정

  • initial load
  • hydration

initial load html

  • js 동작만 없는 HTML을 먼저 화면에 보여주는데, 아직 js 파일이 로드되기 전 이므로 <Link> 같은 컴포넌트는 동작하지 않는다

hydration

  • initial load에서 HTML을 로드한 뒤 js 파일을 서버로부터 받아 HTML을 연결시키는 과정
  • 해당 과정에서 react 컴포넌트는 초기화되고 사용자와 상호작용할 준비를 마친다

만약 Pre-Rendering이 없다면 js 전체가 로드되어야 하기 때문에 최초 load에서 사용자에게 보여지지 않게 된다. 즉, 전체 페이지가 로도되기 전 사용자는 페이지를 볼 수 없다.

Pre-Rendering의 2가지 SSG와 SSR

Next.js에서 미리 렌더링 하는 방식은 두 가지로 나뉜다.

  • 빌드 타임에 HTML에 생성되어 매 요청마다 재사용하게 해주는 SSG(Static-Site Generation)
  • 다른 하나는 매 요청마다 HTML을 생성하는 SSR(Server-Side Rendering)

SSG (Static Site Generation)

SSG는 동일한 HTML을 매 요청마다 생성해서 쓰는 SSR의 단점을 보완하기 위해 탄생한 기법이다.

Next.js 내부에 존재하는 Pre-Render 메서드가 최초에 HTML을 build할 때 동작한다. 그리고 Pre-Render된 HTML 파일은 요청에 따라 재사용된다.

SSR (Server Side Rendering)

위에 내용을 토대로 보면 SSR이 SSG보다 비효율적으로 생각할 수도 있다. 하지만 SSR은 HTML을 각 리퀘스트가 일어날 시점마다 생성을 해주기 때문에 SSR을 써야하는 부분은 명백히 존재한다. 즉, 주로 사용자마다 페이지의 데이터가 변경되어야 하는 페이지에서 사용된다.

getStaticProps

Static Generation 할 때, 데이터도 미리 빌드 타임에 생성할 수 있다.

 

사용법은 해당 페이지의 같은 파일에 async getStaticProps 함수를 선언 후 export 하면 된다.

// posts will be populated at build time by getStaticProps()
function Blog({ posts }) {
  return (
    <ul>
      {posts.map((post) => (
        <li>{post.title}</li>
      ))}
    </ul>
  )
}

// This function gets called at build time on server-side.
// It won't be called on client-side, so you can even do
// direct database queries. See the "Technical details" section.
export async function getStaticProps() {
  // Call an external API endpoint to get posts.
  // You can use any data fetching library
  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,
    },
  }
}

export default Blog

getStaticProps는 언제 사용할까?

  • user request 이전에, 빌드 타임 시에 미리 데이터를 불러오고자 할 때
  • 데이터는 CMS headless에서 온다
  • 공용 캐싱 처리용도로 사용할 때

즉, 해당 페이지에서 정적으로 사용되는 데이터들은 싹다 getStaticProps에서 불러와서 사용하도록 하면 된다. 하지만 데이터 변경이 있는 상품 리스트 같은 데이터는 사용하면 안 된다.

getStaticPaths

Next.js에서는 동적 라우팅 처리 **(ex: pages/posts/[id].js)**가 가능한데, 이때 id로 들어갈 각 페이지 지정을 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 }
}

함수를 보면, 모든 posts 데이터를 가져와서 각 포스트의 id 값만 가진 paths 배열 데이터를 return한다. 예를 들어 [{id:1},{id:2},{id:3}]를 반환했으면, 동적으로 pages/posts/1, pages/posts/2, pages/posts/3페이지가  빌드 타임 때 생성된다.

동적 라우팅 페이지 내에서도 당연히 이 함수를 쓸 수 있다.

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

export default Post

getServerSideProps

빌드 타임 시 말고, 유저가 페이지 서비스에 접속하려고 페이지 요청을 보낼 시에 서버에서 정적 HTML 파일을 생성하는 방법이다.

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

export default Page

getStaticProps 사용법과 동일하고, 이름도 유사하지만 getServerSideProps는 빌드 타임이 아니고 요청 시 마다 동작하게 된다.

getInitialProps

이 방식은 SSR 방식인데, Automatic Static Optimization이 비활성화 되어서 권장하지 않는 방식이다.

Automatic Static Optimization 이란?

위에 설명했던 것 처럼, 미리 빌드 타임에 정적 파일로 생성되서 CDN 등으로 캐싱되서 요청 시 사용되는 방식을 말한다. 이로 인해 요청 시마다 항상 정적 파일을 생성하지 않고, 미리 생성되어 캐싱된 파일만 바로 제공하면 되므로 굉장히 빠른 로딩이 가능하다.

만약 getServerSideProps와 getInitialProps를 페이지 내에 선언하게 되면, 해당 페이지가 요청 시마다 동작하게 되므로 Static Optimization이 되지 않는다는 점을 주의해야 한다.

정리

Next.js는 기본적으로 SSG를 이용해 정적인 페이지를 미리 생성하여 SEO에 유리하다. 따라서, 블로그, 포트폴리오, 메뉴얼 등 데이터가 바뀌지 않는 페이지는 SSG를 사용하지만 유저의 요청마다 데이터가 변경될 수 있는 맞춤 추천리스트, 장바구니 페이지 등은 SSR을 사용해야 한다.

 

출처 : 아래의 사이트들을 보면서 큰 공부 하였습니다

https://wonit.tistory.com/m/362

https://www.howdy-mj.me/next/hydrate

https://yceffort.kr/2021/12/nextjs-lesson-and-learn

https://helloinyong.tistory.com/306?category=1008748

https://velog.io/@hyounglee/TIL-93

https://develogger.kro.kr/blog/LKHcoding/133