Next.js 에 대한 거의 모든 것

sj_dev_js·2022년 9월 11일
197

면접 준비를 하면서 Next.js 를 깊게 공부할 기회가 생겼다.

이번 기회에 평소 헷갈렸던 Next.js 개념들을 정리해보고자 한다.

상당히 내용이 많으므로 Next.js 가 CSR을 사용하는지,

SSG 는 언제 사용하는지 SSR 는 어떻게 사용하는지 빠르게 알고싶다면 맨 밑부터 읽으면 된다.

Next.js 를 시작하기 전에 : CSR와 SSR

CSR

CSR(Client Side Rendering)은 뼈대가 되는 빈 HTML 과 JS 파일을 전부 가져와 클라이언트(브라우저) 측에서 HTML 을 구성하는 방식이다. 초기에 빈 HTML 파일을 보여준 후 API 요청을 통해 데이터를 받아와 다시 렌더링 하기 때문에 사용자가 화면을 인식하는데 상대적으로 오래걸린다. 하지만 초기 로딩 후 버튼이나 링크를 클릭해 페이지간 전환이 일어날 때 클라이언트에서 화면을 다시 구성하므로 화면 전환 속도가 빠르다.

SSR

SSR(Server Side Rendering)은 서버에서 HTML을 구성하여 클라이언트로 보내주는 방식이다. (여기서 서버란, 프론트엔드가 발전하지 않았던 시절에는 말그대로 백엔드 서버를 의미 했으나, 프론트엔드단이 발전한 현재에는 프론트엔드 서버를 의미한다.) 서버에서 구성한 화면은 유저가 인식할 수 있으나, JS 를 다운받아 실행하기 전에는 이벤트 등이 작동하지 않는다. 따라서 유저의 interaction을 기록했다가 JS 파일을 다운 받은 후 실행시킨다.

CSR vs SSR

CSR은 최초 화면을 유저에게 보여주기까지의 시간이 SSR 보다 오래걸린다. 또한 검색엔진이 크롤링을 했을 때 빈 HTML 만을 확인할 수 있기 때문에 SEO(검색엔진 최적화)에 불리하다.

SSR은 매번 서버에서 연산을 수행해야하기 때문에 서버에 부하가 더 크다. 또한 화면 전환시 깜빡임이 있다.

이렇게 각각 장단점이 있지만, 초기 로딩과 SEO 측면에서는 SSR의 장점이 두드러진다.


Next.js 란 : React 를 기반으로 하는 웹 개발 프레임워크

React는 기본적으로 CSR 방식을 사용하는데, SSR을 사용하려면 개발자가 직접 환경을 구성해야한다.

Next.js 는 직접 환경을 구성할 필요 없이, SSR (SSG) 을 쉽게 사용할 수 있도록 도와주는 React 기반 프레임워크다. Next.js 가 SPA(Single Page Application) 이 아니라든가, CSR 없이 SSR(SSG) 만을 사용한다는 이야기는 아니다. Next.js 는 SPA 이며 SSG를 기본으로 사용하고, SSR을 사용할 수 있으며 페이지 전환시에는 CSR을 사용한다.

SSG는 무엇인지, CSR을 사용하는 근거에 대해서는 아래에서 서술할 예정이다.

다만 알아두어야할 것은 Next.js 는 Pre-rendering과 CSR의 장점을 모두 사용할 수 있게 해준다는 것이다.

Next.js 의 특징

  • Page 기반 라우팅
  • Pre-rendering
  • 코드 스플리팅과 pre-fetch

Page 기반 라우팅

Next.js 에서는 직관적인 라우팅 방식을 제공한다. pages 폴더에서 생성한 tsx(jsx) 파일의 경로 그대로 url 의 path가 된다. 예를 들어 파일 경로가 pages/users/detail.tsx 라면 url은 hostname/users/detail 이 된다.

파일 경로를 동적으로 정하고 싶다면 대괄호를 활용할 수 있다. 파일 경로가 pages/users/[username].tsx 라면 대괄호 부분에 어떤 것을 넣어도 해당 페이지로 접근한다. 또한 next/router 의 useRouter() 를 통해 대괄호에 들어가는 변수를 확인할 수 있다.

라우팅에 대한 더 자세한 내용은 아래 링크 참조

Routing: Introduction | Next.js

Pre-rendering

(프론트) 서버에서 HTML 파일을 미리 구성한다.

주소창에 url 입력시 브라우저는 pre-rendering 된 HTML 파일을 받게된다.

  • SSG (default) : 빌드 시 HTML 파일을 구성한다.
  • SSR : 해당 page에서 getServerSideProps()를 사용할 시, 요청을 받을 때마다 서버에서 HTML을 새로 구성해 반환한다.

page 파일에서 getInitialProps() 나 getServerSideProps() 를 사용하지 않으면 기본적으로 SSG를 사용하게 된다. 따라서 url 입력을 통해 접근하는 페이지는 모두 pre-rendering 된 페이지이다.

코드 스플리팅과 pre-fetch

모듈화 했던 파일들을 하나로 묶는 것을 번들링이라고 하는데, 번들링된 JS파일은 너무 커서 다운받는데 오랜 시간이 걸릴 수 있다. JS파일을 다운받는데 걸리는 시간동안 화면의 기능(버튼 이벤트 등) 작동하지 않기 때문에 하나로 번들링된 JS 파일을 페이지 별로 필요한 JS 파일로 분리한 것이 코드 스플리팅이다.

pre-fetch는 관련된 데이터를 미리 로딩하는 것을 말한다. url을 통해 페이지를 요청했을 때, 코드 스플리팅을 통해 현재 페이지에서 필요한 JS 파일을 받는다. 이후 페이지에 <Link/> 가 있다면 연결된 페이지의 JS 파일을 미리 다운로드 받아 <Link/> 클릭시 페이지 전환이 빠르게 일어나도록 한다.


SSG (Static Site Generation)

빌드시 HTML 파일을 미리 렌더링하는 것이 SSG 이다. 정적인 페이지에 주로 쓰인다. HTML 파일을 미리 생성하기 때문에 서버에서 매번 연산을 하지 않아도 될 뿐 아니라, 별다른 설정 없이 CDN 캐시 사용이 가능하기 때문에 SSR 보다도 훨씬 빠른 속도를 보여준다.

앞서 말했듯이 SSG는 Next.js 의 기본적인 렌더링 방식이다. 하지만 백엔드 서버로부터 데이터를 받아서 렌더링하는 경우를 생각해보자. 페이지에서 useEffect()를 사용한다면 url을 통한 페이지 접근시 SSG로는 데이터가 필요없는 부분만 구성하고, 데이터를 필요로하는 부분은 CSR로 렌더링하게 된다. 페이지에서 데이터를 필요로 하는 부분이 클수록 사용자가 화면을 인식하는데 시간이 오래걸리며 이는 SSG의 장점을 전혀 활용하지 못하게 되는 것을 의미한다.

이는 Next.js 에서 제공하는 페이지 단위 함수 getStaticProps()를 통해 해결할 수 있다.

getStaticProps()

아래 코드는 공식문서에서 제공하는 getStaticProps()의 예제이다.

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

// getStaticProps 함수는 server-side에서 build 타임에만 실행된다.
export async function getStaticProps() {
  // build 타임에 데이터를 받아온다.
  const res = await fetch('https://.../posts')
  const posts = await res.json()

  // { props: { posts } }를 반환함으로써, 
  // Blog 컴포넌트는 빌드타임에 props 로 posts를 받을 수 있다.
  return {
    props: {
      posts,
    },
  }
}

export default Blog

보이는 바와 같이 getStaticProps는 빌드타임에 서버로부터 데이터를 받아와 반환하고, getStaticProps를 사용하는 페이지는 빌드타임에 getStaticProps 로부터 props를 받아올 수 있다. 따라서 빌드타임에 데이터에 따른 화면 구성이 가능해진다.

SSG 페이지의 데이터가 변경되면 어떡하지? : revalidate

여기까지 서술한 바에 따르면 SSG는 몇가지 문제점이 존재한다.

빌드 타임에 정적파일을 생성하기 때문에 이후 데이터 변경이 일어나도 페이지에 반영이 되지 않는다. 이런 문제를 해결하기 위해 getStaticProps() 에는 revalidate라는 옵션이 존재하며 초 단위로 입력할 수 있다.

빌드를 통해 페이지가 생성된 후 revalidate 값(초)이 지나기 전 들어오는 요청에 대해서는 기존의 페이지를 반환한다. revalidate 값이 지나고 들어오는 최초 요청에 대해서는 기존 페이지를 반환하고 페이지를 새로 빌드한다. 이후 들어오는 요청에 대해서는 새로 빌드된 페이지를 반환한다.(반복) 즉 revalidate에 10을 입력했다면 10초마다 재빌드 되는 것이 아니라, 10초 이후 요청이 새로 들어왔을 떄 재빌드 하여 새로운 페이지를 만드는 것이다.

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, // 초 단위
  }
}

더 자세한 사항에 대해서는 아래에서 ISR(Incremental Static Regeneration) 에 대해 서술할 것이다.

getStaticPaths()

SSG의 또 한가지 문제점으로 동적라우팅을 사용하는 경우가 있다. (ex: hostname/users/[username])

동적라우팅을 사용하여 query에 들어오는 값은 빌드타임에는 알 수가 없다. 따라서 getStaticPaths()를 사용하여 동적라우팅으로 가능한 모든 path에 대한 정보를 getStaticProps()에 전달할 수 있다.

아래는 공식문서에서 제공하는 예제이다.

// pages/posts/[id].js

function Post({ post }) {
  // post 렌더링 하는 코드
}

export async function getStaticPaths() {
  // API 호출을 통해 posts 에 대한 데이터를 가져온다.
  const res = await fetch('https://.../posts')
  const posts = await res.json()

  // paths 를 추출한다.
  const paths = posts.map((post) => ({
    params: { id: post.id },
  }))
  /*
	  [{params : {id : 1} }, {params : {id : 2} }]
  */

  return { paths, fallback: false }
}

export async function getStaticProps({ params }) {
  // getStaticProps로부터 params 를 받아 path 를 구성하고 데이터를 받아온다.
  const res = await fetch(`https://.../posts/${params.id}`)
  const post = await res.json()

  return { props: { post } }
}

export default Post

모든 페이지를 미리 만들 수는 없다 : fallback 옵션

getStaticPaths() 를 통해 미리 만들어진 path가 아닌, 임의의 path가 url 로 들어온다면 어떻게 해야할까? 당연히 404페이지를 띄워야한다는 생각이 들 수도 있다. 하지만 상품 상세페이지를 정적 페이지로 생성한다고 생각해보자. 쇼핑몰의 모든 상품에 대한 페이지를 미리 빌드하는 것은 아주 오랜 시간이 걸릴 수도 있는 일이다. 뿐만 아니라, 새로운 상품이 등록된다면 그 상품에 대한 상세페이지도 만들어야하는데 새로운 상품을 빌드 타임에 알 수는 없다. 따라서 미리 만들어놓지 않은 path에 대해서도 페이지를 보여줄 수 있어야한다. 이를 위해 getStaticPaths() 에는 fallback 옵션이 존재한다.

export async function getStaticPaths() {
  return {
    paths: [
      { params: { ... } } 
    ],
    fallback: true, false or "blocking"
  };
}

fallback 옵션에 들어갈 수 있는 값은 true, false, “blocking” 이며,

fallback 값에 따라 path가 미리 정의되지 않은 경우 getStaticProps()의 동작이 달라진다.

  • false : 404 페이지를 반환한다.
  • true : 우선 페이지의 fallback 버전을 보여주고 getStaticProps 를 통해서 요청을 보내서 HTML 파일과 JSON 파일을 만든다. 만들어지면 브라우저는 JSON 파일을 받아서 렌더링한다. Next.js 는 만들어진 HTML 파일을 미리 렌더링된 페이지 목록에 추가한다. 이후 요청부터는 미리 렌더링된 HTML 파일을 반환한다.
  • blocking : true 와 비슷하게 작동하나, 파일을 렌더링하는 동안 fallback 버전을 보여주는 것이 아니라 SSR 처럼 작동하여 HTML파일을 새로 만들어 반환한다. 이후 요청부터는 만들어진 HTML 파일을 반환한다.

true 옵션과 blocking 옵션은 next export 에서는 작동하지 않는다.

Fallback Page

getStaticPaths() 에서 fallback : true 이면 fallback 페이지를 우선 보여준다는데 도대체 fallback 페이지는 무엇일까? 사실 fallback 페이지 또한 개발자가 만들어야한다. getStaticPaths() 는 getStaticProps()로 하여금 page 에게 isFallback: true 를 줄 뿐이다. 페이지에서 useRouter()의 isFallback을 통해 조건을 분기하여 fallback 페이지를 보여줄 수 있다.

// pages/posts/[id].js
import { useRouter } from 'next/router'

function Post({ post }) {
  const router = useRouter()

	// 만약 페이지가 아직 생성되지 않았다면 getStaticProps()가 실행되는 동안
	// isFallback을 통해 조건을 분기하여 fallback(대체) 페이지를 보여줄 수 있다.
  if (router.isFallback) {
    return <div>Loading...</div>
  }

  // post 렌더링 하는 코드 생략...
}

export async function getStaticPaths() {
  return {
    // `/posts/1`,`/posts/2`만 빌드타임에 생성된다.
    paths: [{ params: { id: '1' } }, { params: { id: '2' } }],
    // fallback 값을 true로 줌으로써 `/posts/3` 같은 추가 페이지를
    // 정적인 방식으로 생성할 수 있다.
    fallback: true,
  }
}

export async function getStaticProps({ params }) {

  const res = await fetch(`https://.../posts/${params.id}`)
  const post = await res.json()

  return {
    props: { post },
    revalidate: 1,
  }
}

export default Post

SSR (Server Side Rendering)

페이지에 대한 요청이 있을 때마다 (프론트)서버에서 페이지를 만들어 반환한다. 서버에서 매번 연산을 해야하며 캐시를 사용하는 것이 상대적으로 어렵기 때문에 SSG에 비해 느리다. 하지만 항상 최신의 정보를 보여주어야하는 경우, SSR를 사용하는 것이 좋다.

(프론트)서버에서 HTML 파일을 만들어서 보내기 때문에 CSR에 비해 사용자가 더 빠르게 화면을 인식할 수 있다. 하지만 이벤트 등 페이지의 동작을 위해서는 hydrate라는 과정을 통해서 JS 코드가 실행되어야한다.

hydrate

서버에서 렌더링된 HTML 페이지와 JS 파일을 클라이언트에게 보내면, 클라이언트에서 HTML 파일에 JS 코드를 매칭 시키는 작업이다. 이벤트 등록이나 스타일 적용이 일어난다. SSR과 SSG 모두 Hydrate가 일어나는데, SSG의 경우 빌드타임에 query 가 없기 때문에 런타임에서 query에 대한 hydrate가 일어난다.

getServerSideProps()

Next.js에서 SSR을 사용하기 위해서는 페이지에서 getServerSideProps() 를 통해 데이터를 받아와야한다.

빌드타임에만 실행되는 getStaticProps()와는 달리 getServerSideProps()는 페이지에 대한 요청이 있을 때마다 실행된다.

function Page({ data }) {
  // 데이터를 통해 페이지 렌더링...
}

export async function getServerSideProps() {
  // 페이지를 요청할 때마다 매번 실행된다.
	// 데이터를 받아온다.
  const res = await fetch(`https://.../data`)
  const data = await res.json()

  // Pass data to the page via props
  return { props: { data } }
}

export default Page

CSR (Client Side Rendering)

Next.js 에서 CSR을 사용하는 경우는 두가지로 나누어볼 수 있다.

  • url을 입력을 통해 pre-rendering 된 페이지를 받고 useEffect()를 통해 데이터를 추가로 불러오는 경우
  • <Link/>router.push()를 통해 페이지 전환이 일어나는 경우이다.

CSR을 권장하는 경우

( 페이지 전환시 CSR을 사용하는 것은 당연하므로 url 입력을 통해 페이지를 요청하는 경우를 이야기한다. )

만약 페이지를 pre-rendering 할 필요가 없거나, 데이터의 업데이트가 자주 일어난다면 CSR을 사용하는 것을 권장한다. 예를 들어 유저 대시보드 페이지는 해당 유저만을 위한 비밀 페이지이기 때문에 SEO가 필요하지 않으며 따라서 pre-rendering할 필요도 없다. 또한 데이터가 자주 변경되기때문에 CSR이 적합하다.

페이지 전환시 CSR을 사용한다는 근거

공식문서 에 적혀있다.

Client-side transitions between routes can be enabled via the Linkcomponent exported by next/link.

Handles client-side transitions, this method is useful for cases where next/link
is not enough. (router.push() 에 대한 설명)

페이지 전환시 getStaticProps와 getServerSideProps의 작동

getStaticProps()

빌드타임에 getStaticProps()가 실행되면, HTML 뿐만 아니라 JSON 파일 또한 생성된다. 이 JSON 파일은 next/linknext/router를 통해 CSR 화면전환이 일어날 때 쓰인다. getStaticProps 를 사용하는 페이지로 이동할 때 이 JSON 파일을 받아와 페이지 컴포넌트의 props로 사용한다.

getServerSideProps()

next/link 또는 next/router를 통해 페이지 전환 시 Next.js가 getServerSideProps를 실행하는 (프론트)서버에 API 요청을 보낸다.


ISR (Incremental Static Regeneration)

Next.js는 사이트를 빌드한 이후에도 정적페이지를 생성하거나 업데이트 할 수 있는 기능을 제공하는데 이것이 ISR 이다. ISR은 모든 페이지를 재빌드할 필요없이 각 페이지를 정적생성한다.

ISR에는 두가지 방법이 있는데 하나는 앞서 살펴보았던, getStaticProps()의 revalidate 속성을 사용하는 것이다. 페이지를 방문한 이후로 revalidate 시간동안은 캐시된 페이지를 보여주고, 이후 들어오는 첫번째 요청에 대해서는 역시 캐시된 페이지를 보여주며 해당 페이지를 재빌드한다. 이후에는 새로 빌드된 페이지를 보여준다.

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, // 초 단위
  }
}

On-demand Revalidation

다른 한가지 방법은 데이터 변경시 재빌드를 하는 것이다.

앞서 살펴본 revalidate 옵션을 사용하면 revalidate 의 값만큼은 캐시된 페이지를 보게된다. 이 캐시를 무효화하려면 revalidate 값만큼의 시간 이후 요청이 있어야한다. on-demand revalidation 방식은 데이터 변경이 일어났으니 재빌드를 해달라는 요청을 받으면 재빌드를 함으로써 캐시를 갱신한다.

on-demand revalidation 방식에서는 revalidation 옵션을 사용하지 않는다. revalidation 을 사용하지 않으면 기본값이 false가 되어 revalidate() 함수를 사용할 때만 on-demand revalidation이 일어난다.

On-demand Revalidation을 사용하기 위해서는 Next.js 만 알고있는 토큰을 생성해서 환경변수에 저장해야한다. 해당 토큰을 사용해야 인가된 사용자만 아래와 같은 url로 Next.js api에 revalidate를 요청할 수 있다.

https://<your-site.com>/api/revalidate?secret=<token>

데이터에 변경이 일어나서 재빌드를 해야하는 상황이라면 url

https://<your-site.com>/api/revalidate?secret=<token> 를 통해 Next.js 서버에 요청을 보낸다.

그러면 Next.js api 의 해당 파일이 실행된다. Next.js API 파일에서 res.revalidate(재빌드할 경로명)을 사용함으로써 해당 경로의 파일이 재빌드 된다.

// pages/api/revalidate.js

export default async function handler(req, res) {
  // 토큰 검사
  if (req.query.secret !== process.env.MY_SECRET_TOKEN) {
    return res.status(401).json({ message: 'Invalid token' })
  }

  try {
    // 재빌드할 경로명을 정확하게 입력해야한다. req의 body를 통해 받아올 수 있다.
    // 파일명이 "/blog/[slug]" 이더라도 "/blog/post-1" 처럼 입력해야한다.
    await res.revalidate('재빌드할 경로명')
    return res.json({ revalidated: true })
  } catch (err) {
    // 만약 에러가 발생하면 최근에 생성된 페이지를 보여준다.
    return res.status(500).send('Error revalidating')
  }
}

바쁜 당신, 이것만은 알고 Next.js 사용하자

  1. Next.js로 만들어진 웹 페이지는 url로 접근시 pre-rendering(SSG, SSR) 된 페이지를 반환한다.
  2. pre-rendering 은 프론트엔드 서버에서 돌아가는 Next.js 에서 일어난다.
  3. Next.js 는 SSG가 기본이다. getServerSideProps() 을 사용하지 않은 페이지는 모두 SSG로, 빌드 타임에 미리 렌더링 된다.
  4. SSR을 하려면 getServerSideProps()를 사용해야한다.
  5. Link를 클릭하거나 router.push()를 통해 이동하면 CSR 방식으로 페이지를 전환한다.

이렇게 길게 적었음에도 불구하고 아직 공부해야할 것이 많이 남아있다. (캐시라든가… 배포라든가… )

그래도 내가 헷갈렸던 부분에 대해서는 최대한 자세하게 적어서 다른 사람들이 쉽게 의문을 풀 수 있도록 노력했다.

더 자세한 사항에 대해서는 공식문서를 읽을 것을 권장한다. 본문의 모든 것은 이미 공식문서에 적혀있는 내용이다. Next.js는 공식문서가 잘 만들어져서 상당히 간결해보이는데, 그 간결해보이는 문서 안에 정말 많은 내용이 숨어있다. 문서가 굉장히 친절하니 영어라고 겁먹지 말고 꼭 한번은 정독할 것을 추천한다.

참고

Next.js 공식문서

profile
JavaScript TypeScript React Next.js

10개의 댓글

comment-user-thumbnail
2022년 9월 11일

지나가는 행인입니다. 잘 정리된 글에 하트를 누르지 않을수 없군요. 감사합니다.

1개의 답글
comment-user-thumbnail
2022년 9월 14일

Awesome and interesting article. Great things you’ve always shared with us. Thanks. Just continue composing this kind of post.
QuickPayPortal

답글 달기
comment-user-thumbnail
2022년 9월 14일

마침 Next.js 공부중인데 좋은 글 감사합니다!

답글 달기
comment-user-thumbnail
2022년 9월 15일

NextJs 공부하고 있었는데 덕분에 한 번 더 정리하고 갑니다. 감사합니다 ㅎㅎㅎㅎㅎ 앞으로도 좋은 글 기대할게요!! 🥰

답글 달기
comment-user-thumbnail
2022년 9월 15일

항상 열심히 하는 수정🍒 너무 멋있어요! 덕분에 Nextjs에 대해 더 잘 알아가요!!

답글 달기
comment-user-thumbnail
2022년 9월 15일

재밌게 읽었습니다^^

답글 달기
comment-user-thumbnail
2022년 9월 17일

좋은 글 감사합니다 :)

답글 달기
comment-user-thumbnail
2022년 9월 20일

프론트 공부할게 많네요...

답글 달기
comment-user-thumbnail
2022년 10월 2일

잘 정리해주셔서 감사합니다 :)
너무 좋은 글이네요!!

답글 달기