NEXT.js 알아보기 - Rendering

데브현·2023년 9월 19일
1

NEXT.js를 알아보자

목록 보기
1/3
post-thumbnail

Next.js로 프로젝트를 해보았지만 다시 한번 공식문서를 확인해보면서 정리해보려고 한다.
이 글은 Next.js의 공식문서를 100% 참고하고 만들었습니다.

Next.js는 모든 페이지들을 Pre-render하고 각각의 HTML에 좋은 성능을 가져오고 SEO최적화에 도움을 준다. 각각의 HTML들은 코드에 필요한 최소한의 JS와 연관되어 있게 생성된다. 페이지가 로드될때 js코드가 실행되고 페이지가 상호작용 되도록 만들어준다. 이러한 과정을 React에서는 hydration(수화)라고 부른다.

Pre-rendering

next.js는 두 가지 형태의 pre-rendering을 제공한다.(Static Generation, Server-side Rednering)

  • Static Generation: 빌드 타입에 HTML이 생성되고 매 요청마다 재사용 된다.
  • Server-side Rednering: 매 요청마다 HTML이 생성된다.

Next.js에서는 Static Generation방식을 추천한다. 그 이유는 CDN에 의해 캐쉬되고 추가적인 설정이 없어 성능적인 이점을 볼 수 있기 때문이다. 그러나 Server-side 또한 선택지가 될 수 있으므로 사용이 가능하다.

Server-side Rendering(SSR)

SSR 또는 Dynamic Rendering이라고도 불린다.
Server-side Rendering을 사용하는 페이지의 경우 각 요청마다 HTML을 만들어준다.

Server-side Rendering을 페이지에 사용한다면, exportasync키워드를 통해
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 } }
}

위처럼 외부 API에 의해 주기적으로 페이지가 업데이트가 필요할때 사용이 가능하다.
getStaticProps와 유사하지만, getServerSideProps는 빌드타임대신에 매요청에 실행되는 점이 다르다.

Server-side Generation(SSG)

Server-side Generation은 빌드 타임에 HTML을 생성한다. 그렇기에 next build 명령어를 통해 HTML을 생성할 수 있고 매요청에 HTML은 재사용된다.

데이터를 사용하지 않는 Static Generation

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

위의 페이지는 data fetch과정이 필요없는 페이지이고, 이는 빌드타임에 미리 생성된다.

데이터를 사용한 Static Generation

두가지 시나리오가 존재할 수 있다.
1. 페이지 내용이 외부의 데이터에 의존되어 있다 -> getStaticProps
2. 페이지의 path가 외부의 데이터에 의존되어있다. -> getStaticPaths(getStaticProps 대부분 같이 사용)

첫번째 시나리오

export default function Blog({ posts }) {
  return (
    <ul>
      {posts.map((post) => (
        <li>{post.title}</li>
      ))}
    </ul>
  )
}

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

getStaticProps를 실행하여 props를 통해 fetch한 posts를 넘겨주고 이는 빌드 타임에 가져와 해당 페이지를 pre-render할 수 있게 해준다.

두번째 시나리오
동적인 routes를 사용할때, id에 따라 다른 경로의 페이지들이 생성될 수 있다. pages/posts/[id].js가 id에 따라 외부 데이터를 fetch해야 하고 pre-render가 필요할때 다음과 같이 할수 있다.

export default function Post({ post }) {
  // Render post...
}
 
// 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 }
}
 
// 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 } }
}

getStaticPaths에서 데이터를 fetch하고 post들의 id로 path들을 만들어준다.
그후 id를 통해 해당 post의 data들을 fetch해와서 props로 넘겨주게 되면 pre-render를 할 수 있게 된다.

언제 사용해야 할까?

공식 문서에서는 되도록 가능한곳이라면 최대한 활용하는 것을 추천하고 있으며, 빌드타임때 한번 생성되고 캐싱을 사용하면 훨씬 더 빠르고 성능이 좋은 패이지를 렌더링할 수 있기 때문이다.

유저의 요청보다 먼저 페이지를 pre-render할 수 없다면 Static Generation은 좋은 선택이 아닐 수 있다.

  • Client-side data fetching과 함께 Static Generation 사용하기: 페이지의 일부분을 pre-render를 스킵하고 client js에 계산을 맡기는 것이다.
  • Server-Side Redendering 사용하기: 매 요청마다 페이지들을 pre-render하는 방법이다. 이건 Static Generation보다 느릴 수 있지만 매 패이지가 항상 업데이트된 데이터일 것이다.

Incremental Static Regenration(ISR)

ISR은 전체 페이지를 새로 빌드할 필요 없이 특정 페이지의 속성들 기반으로 Static-generation을 사용할 수 있게 해준다. ISR을 사용하려면 revalidate속성을 getStaticProps에 추가해주면 된다.

function Blog({ posts }) {
  return (
    <ul>
      {posts.map((post) => (
        <li key={post.id}>{post.title}</li>
      ))}
    </ul>
  )
}
 
// This function gets called at build time on server-side.
// It may be called again, on a serverless function, if
// revalidation is enabled and a new request comes in
export async function getStaticProps() {
  const res = await fetch('https://.../posts')
  const posts = await res.json()
 
  return {
    props: {
      posts,
    },
    // Next.js will attempt to re-generate the page:
    // - When a request comes in
    // - At most once every 10 seconds
    revalidate: 10, // In seconds
  }
}
 
// This function gets called at build time on server-side.
// It may be called again, on a serverless function, if
// the path has not been generated.
export async function getStaticPaths() {
  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: 'blocking' } will server-render pages
  // on-demand if the path doesn't exist.
  return { paths, fallback: 'blocking' }
}
 
export default Blog

만들어진 페이지에 요청을 하면 빌드 타임 때 만들어진 페이지를 캐시하여 보여줄 것이다.

  • 초기 요청 이후 10초 이전의 페이지에 대한 모든 요청은 캐쉬된다.
  • 10초가 지난뒤 여전히 캐쉬된 페이지를 보여준다.
  • next.js는 background에서 새로운 페이지를 만들것을 trigger한다.
  • 성공적으로 페이지를 생성하면, cache를 Invalidate한뒤 업데이트된 페이지를 보여준다. 실패한다면 교체되지 않고 이전의 페이지를 보여준다.

On-Demand Revalidation (온디맨드 revalidation)

revalidation이 60으로 설정되었다면 1분간 사용자는 같은 버전의 페이지를 보게될 것이고 누군가 1분뒤 새로 들어온사람 만이 cache를 무효화 할 수 있다.

이러한 사항 때문에 특정 페이지의 Next.js의 캐쉬를 수동적으로 purge 할 수 있는 기능을 v12.2.0이상 부터는 지원한다.

getStaticProps에서 revalidate를 사용하지 않으면 next.js는 기본값으로 false를 사용할 것이고 이는 revelidate()가 불러질 때만 revalidate되도록 설정이 된다.

사용법
Next.js안에 시크릿 토큰을 만들고 이는 revalidation하는 api에 검증되지 않은 접근을 막아주는 역할을 합니다.

export default async function handler(req, res) {
  // Check for secret to confirm this is a valid request
  if (req.query.secret !== process.env.MY_SECRET_TOKEN) {
    return res.status(401).json({ message: 'Invalid token' })
  }
 
  try {
    // this should be the actual path not a rewritten path
    // e.g. for "/blog/[slug]" this should be "/blog/post-1"
    await res.revalidate('/path-to-revalidate')
    return res.json({ revalidated: true })
  } catch (err) {
    // If there was an error, Next.js will continue
    // to show the last successfully generated page
    return res.status(500).send('Error revalidating')
  }
}

시크릿 토큰과 일치한다면 해당 페이지를 revalidate시킬 수 있다.

에러 핸들링하기

getStaticProps안에 에러가 있다면 수동으로 error를 던져줄 수 있다. 에러를 던져주면 가장 마지막에 성공한 페이지가 계속 보여지게 되며 다음 요청에 getStaticProps를 다시 실행할 것이다.

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

Client-side Rendering (CSR)

CSR은 최소한의 HTML을 내려받고 페이지에 필요한 JS를 같이 내려받는다. 해당 JS는 DOM을 업데이트하고 페이지를 랜더링한다. 사용자는 모든 페이지를 보기까지 약간의 딜레이가 있을 수 있다. 그 이유는 js가 다운로드되고, 파싱과정과 실행과정이 모두 완료될 때까지 페이지를 완전하게 볼 수 없기 때문이다.

client-side rendering을 사용하는 두가지 방법
1. React의 useEffect() 훅을 getStaticProps,getServerSideProps 대신에 사용할 때
2. React-query, SWR과 같은 data-fetching 라이브러리를 사용할 때

useEffect()를 사용한 예시

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

useEffect를 통해 data가 fetch되기 전까지는 loading을 보여주고 완료가 된 뒤 리랜더링이 일어난다.

data-fetching 라이브러리를 사용한 예시

import useSWR from 'swr'
 
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>
}

알아두면 좋은점
CSR은 SEO에 영향을 끼칠 수 있다. 몇몇의 검색 엔진들은 js를 실행하지 않을 수 있고 그렇게 되면 비거나 로딩중인 화면을 볼 수 있다. 또한 사용자의 인터넷 환경이 느릴때 성능적인 이슈가 발생할 수 있다. js가 모두 로드될 때까지 기다려야하고 그 전까지는 모든 화면을 볼 수 없다. Next.js는 server-side rendering이나 static-site generation, client-side rendering을 함께 사용하는 것을 제공한다. App router를 사용할 경우 Suspense를 활용한 Loading UI를 페이지가 렌더링전에 보여줄 수 있도록 활용할 수 있다.

다음 글에는 Data Fetching에 대해 알아보겠다.

profile
하다보면 안되는 것이 없다고 생각하는 3년차 프론트엔드 개발자입니다.

0개의 댓글