본문 바로가기
프로그래밍 개발/NextJs

NextJs - 2편: NextJs 버젼 12에서 13으로 넘어오면서

by Jinseok Kim 2023. 9. 11.
반응형

2편: NextJs 버전 12에서 13으로 넘어오면서

 

NextJs는 12 버젼에서 13으로 넘어오면서 사용방법이 바뀔 정도로 많은 변화가 있었습니다. 당연히 최신 버전인 13을 바로 사용해도 되지만 아직도 많은 프로젝트가 12 버전을 쓰고 있으며 유지보수를 위해서라도 13 버전 이전의 NextJs 사용 방법을 알고 가는 것이 맞다고 생각합니다.

아직 13버전의 Appdir와 같은 기능들이 공식 Docs에서 공식적으로 스테이블 단계에 들어갔다고 발표한 것도 몇 달이 채 되지 않았으며 12과 13의 차이점을 알고서 개발에 임하는 것이 매우 중요할 거 같아 이렇게 2편에 12과 13 버전의 차이점과 사용방법을 기록하고자 합니다.



우선 1편을 보고 읽는 것을 추천드립니다. 기본 이론 개념들이 많이 스킵됩니다.

 

NextJs - 1편: NextJs 시작을 위한 기본 이론 그리고 프로젝트 생성하기

1편: NextJs 시작을 위한 기본 이론 그리고 프로젝트 생성하기 NextJS는 ReactJs의 몇몇 한계점을 극복한 프레임워크라고 말할수 있습니다. ReactJs의 기반에서 더 좋은 SEO(검색 엔진 최적화), SSR을 통한

k0502s.tistory.com

 

 

 

 

 

NextJs 12 ver.


NextJs 12 ver.의 가장 큰 특징인 프리 렌더링(pre-rendering)을 다루는 옵션이 3가지가 있습니다.  SSG, SSR, ISR 이렇게 나뉠 수 있습니다. 이 세 가지 프리 렌더링 방식의 특징과 차이점을 통해 기록하고자 합니다.

하나의 프로젝트 애플리케이션에서 하나의 방식만 사용해야 하는 것은 아니며, 필요에 따라 어떤 페이지에선 SSG 방식을 사용하고 나머지 페이지에선 SSR 방식을 사용하는 것도 가능합니다. 

 

또한 이 방식들과 클라이언트 사이드 렌더링 방식을 혼합하여 사용하는 것도 가능합니다. (즉 어떤 페이지의 특정 부분은 전적으로 클라이언트 사이드에서 렌더링 되도록 하는 방식)


 

 

Static-site Generation (SSG)

1. HTML이 빌드 타임에 생성되며, 매 요청 시 재사용 됩니다.

2. SSR 방식보다 성능이 더 좋고 빠릅니다. 정적으로 생성된 페이지는 한 번 생성되면 CDN에 캐시 될 수 있기 때문에, SSR 방식을 통해 매 요청마다 페이지를 생성하는 경우보다 훨씬 빠릅니다.

3. 외부 데이터가 없는 경우와 있는 경우 모두 정적 페이지 생성이 가능합니다.

 

데이터가 없는 경우 (예: 회사 소개 페이지) 

Next.js는 외부 데이터를 가져올 필요가 없는(=data fetch를 하지 않는) 페이지는 SSG 방식으로 프리 렌더링한다.

 

데이터가 있는 경우

두 가지 Case가 있는데, 하나만 적용할 수도 있고 둘 다 적용할 수도 있습니다.

  • Case1: 페이지의 내용이 외부 데이터에 의존한다면 getStaticProps를 쓸 것 (예: CMS로부터 블로그 포스트 목록을 가져오려고 할 때)
  • Case2: 페이지의 경로가 외부 데이터에 의존한다면 getStaticProps를 씁니다. 예를 들어 동적 라우트(dynamic routes)로 pages/posts/[id].js페이지를 생성하려는 경우 id값에 들어갈 값들을 먼저 받아와야 할 때가 있습니다.

 

/* Case 1 */

function PostDetail({ posts }) {
  // 포스트 목록 렌더링...
}

// getStaticProps는 빌드 시에 호출된다.
export async function getStaticProps() {
  // 포스트들을 가져오기 위해 외부 API 엔드포인트를 통해 요청을 보낸다.
  const res = await fetch('https://.../posts')
  const posts = await res.json()

  // { props: { posts } } 객체를 반환하면 블로그 컴포넌트가 빌드 타임에
  // `posts`를 prop으로 전달 받게 된다.
  return {
    props: {
      posts,
    },
  }
}

export default PostDetail;

 

/* Case 2 */

function PostDetail({ post }) {
  // 개별 포스트 렌더링..
}

// getStaticPaths는 빌드 시에 호출된다.
export async function getStaticPaths() {
  // 포스트들을 가져오기 위해 외부 API 엔드포인트를 통해 요청을 보낸다.
  const res = await fetch('https://.../posts')
  const posts = await res.json()

  // 프리 렌더링 해야 하는 페이지 경로를 `posts`로부터 뽑아낸다. 
  // Get the paths we want to pre-render based on posts
  const paths = posts.map((post) => ({
    params: { id: post.id },
  }))

  // 이렇게 { paths, fallback: false } 객체를 반환하면,
  // `paths`에 포함된 페이지들만 빌드 시에 프리 렌더링 된다.
  // { fallback: false }에 따라 다른 라우트는 404가 된다.
  return { paths, fallback: false }
}

// getStaticProps도 역시 빌드 시에 호출된다.
export async function getStaticProps({ params }) {
  // params에는 포스트의 `id`가 포함되어 있다.
  // (예를 들어 라우트가 /posts/1일 경우, params.id는 1)
  const res = await fetch(`https://.../posts/${params.id}`)
  const post = await res.json()

  // 포스트 데이터를 페이지에 props로 전달한다.
  return { props: { post } }
}

export default PostDetail;

 

 

SSG 방식을 사용하면 좋은 상황

1. 마케팅 페이지

2. 블로그 포스트

3. 이커머스 상품 목록

 

 즉, 사용자의 요청에 앞서 프리 렌더링해도 되는 페이지

 

 

SSG 방식이 적합하지 않은 상황

1. 사용자의 요청에 앞서 프리 렌더링할 수 없는 페이지에서는 적합하지 않습니다. 예를 들어, 페이지가 빈번하게 변경된다거나, 페이지의 내용이 매 요청마다 변경되어야 하는 페이지를 예를 들 수 있습니다.

 

 

SSG 방식이 적합하지 않은 경우의 해결책

1. SSG와 클라이언트 사이드 렌더링 방식 함께 사용하기: 페이지의 특정 부분에선 프리 렌더링을 하지 않고, 그런 부분은 클라이언트 사이드에서 자바스크립트를 통해 생성되도록 합니다.

2. SSR 사용하기: SSR 방식을 사용하여 생성하는 페이지는 CDN에 캐시 될 수는 없어 속도는 느려질 수 있지만, 매 요청마다 다시 프리 렌더링되기 때문에 늘 최신화(up-to-date) 된 데이터를 보여줄 수 있습니다. 

 

 

 

 

 

 

Server-side Rendering (SSR)

 

1. HTML이 매 요청마다 생성됩니다.

 

2. getServerSideProps를 사용하며 서버사이드 렌더링을 할 수 있습니다.

 

3. getServerSideProps을 getStaticProps에 비교하자면 getServerSideProps은 매 요청 시에 실행되고  getStaticProps은 빌드 시에 실행되며 즉, 한 번만 실행됩니다.

 

function PostDetail({ data }) {
  // data를 렌더링..
}

// getServerSideProps는 매 요청 시 호출된다.
export async function getServerSideProps() {
  // 외부 API로부터 데이터를 가져온다.
  const res = await fetch(`https://.../data`)
  const data = await res.json()

  // 이렇게 가져온 데이터를 페이지에 props로 전달한다.
  return { props: { data } }
}

export default PostDetail;

공식 문서에서는 SSR이 SSG에 비해 느리기 때문에 절대적으로 필요한 경우에만 사용할 것을 권고하고 있는 것이 특징입니다.

 

 

 

 

Incremental Static Regeneration (ISR)

 

1. stale-while-revalidate 캐싱 전략을 따르는 일종의 하이브리드 방식입니다.

2. ISR을 사용하면 SSR을 사용할 때보다 성능을 향상할 수 있고 사용성도 개선할 수 있습니다.

3 ISR은 페이지 단위로 정적 생성되도록 하는 방식이며 즉, 전체 사이트를 다시 빌드하지 않아도 됩니다.

 

사용 방법은 getStaticProps에 revalidate prop을 추가해 주면 사용 가능합니다.

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

// getStaticProps에 revalidate prop을 추가해주면 된다.
// getStaticProps는 서버사이드에서 빌드 시에 호출된다.
// 유효성을 재확인(revalidation)하도록 설정해두었다면, 새로운 요청이 들어올 때 다시 호출된다.
export async function getStaticProps() {
  const res = await fetch('https://.../posts')
  const posts = await res.json()

  return {
    props: {
      posts,
    },
    // NextJs는 요청 후 10초가 지난 이후에 또 요청이 들어오면 페이지를 다시 생성하려는 시도를 한다.
    revalidate: 10, // 단위: 초
  }
}

export default PostDetail;

빌드 타임에 프리 렌더된 페이지에 대해 요청이 발생하면, 우선은 초기에는 캐시 된 페이지를 보여줍니다.

이 페이지에 대한 최초 요청 이후 10초 내로 들어오는 모든 요청들은 캐시 됩니다. 10초 이후 들어오는 요청에도 일단은 캐시 된(즉, stale한) 페이지가 표시됩니다. 요청이 들어온 순간 재생성을 시작하기 때문에 그 전까지 캐시된 페이지를 보여주는 것입니다.

NextJs는 백그라운드에서 페이지를 재생성하기 시작하고 페이지 재생성이 성공하면, NextJs는 캐시된 것은 더 이상 유효하지 않도록 만들고, 업데이트된 페이지를 보여줍니다. 페이지 재생성이 실패하면, 기존 페이지가 변경되지 않고 계속 보이게 합니다.

만약 10초(즉 revalidate 기준 시간)가 흐른 뒤에도 해당 페이지에 대한 새로운 요청이 발생하지 않는다면 페이지 재생성은 이루어지지 않습니다.

 

 

 

 

 

NextJs 13 ver.


 

NextJs 13 ver.으로 넘어오면서 큰 변화가 일어났습니다. 기존 12 ver. 에서는 pages 디렉터리로 라우터를 설정하였는데 13 ver.에서부터는 새로운 디렉토리 구조를 사용할 수 있게 되었습니다.

새로운 디렉토리의 구조의 명칭은 AppDir입니다. 그리고 동시에 AppDir이 도입되면서 변화가 있는 것은 서버 컴포넌트가 기본적으로 적용된다는 점입니다.

 

 

 

AppDir

 

13 ver. 부터는 app 폴더가 pages 폴더를 대체하게 되었습니다. 

 

아래와 같이 프로젝트 루트의 next.config.js에서  appDir이 true로 해야 app directory가 작동합니다. 반대로 사용하지 않는다면 false로 설정합니다.

 

next.config.js

const nextConfig = {
	+ experimental: {
	+	appDir: true
	+ }
	...
}

module.exports = nextConfig

 

 

app 폴더의 기본 구조

	app
	|__ head.{js,tsx}
	|__ layout.{js,tsx}
	|__ page.{js,tsx}

 

 

app/header.tsx

export default function Head() {
	return (
		<>
		<title></title>
		<meta content="width=device-width, initial-scale=1" name="viewport" />
		<link rel="icon" href="/favicon.ico" />
		</>
	)
}

 

 

app/layout.tsx

export default function RootLayout({children}: {children: React.ReactNode}) {
	return (
		<html>
		<head />
		<body>{children}</body>
		</html>
	)
}

 

 

app/page.tsx

기존 pages의 루트 파일과 동일하게 사용합니다.
대신 달라진 점은 서버 컴포넌트 RSC이 기본적으로 설정된다는 점입니다. 

 

 

 

 

RSC(React Server Component)

 

중요한 또 하나의 포인트는 app directory 내부에서는 모든 컴포넌트가 기본적으로 'RSC(React Server Component)'로 동작한다는 사실입니다.

 

서버 컴포넌트란.
서버에서 데이터 Fetch 및 다운 받아야하는 종속성 등이 다 처리된 후 클라이언트에게 최소한의 자바스크립트 번들을 제공하여 성능을 극대화하는 것이 특징입니다.

 

만약 app directory 내부에서 RSC를 쓰지 않고 RCC(React Client Component)을 쓰기 위해서는파일 최상단에 use client라는 directive를 명시해줘야 합니다.

"use client"; //클라이언트 컴포넌트를 사용하려면 파일 상단에 꼭 선언해줘야한다.

import {useState} from "react";

const ClientComponent  =() => {
 
 const [state,setState] = useState()

 return <div>Test</div>
}

export default ClientComponent

 

'서버 사이드 렌더링 SSR'와 '서버 컴포넌트 RSC'가 서로 헷갈릴 수도 있는데 다른 각자 다른 목적을 가지고 있습니다.

필요에 따라 RSC와 SSR을 함께 사용하면 큰 시너지를 낼 수도 있다는 것을 알아야합니다.

 

 

서버 사이드 렌더링 SSR

초기 HTML 를 서버에서 렌더링해서 전달하여 사용자에게 페이지를 빠르게 보여주는것이 목적.

 

서버 컴포넌트 RSC

서버에서 데이터 Fetch 및 다운 받아야하는 종속성 등이 다 처리된 후 클라이언트에게 최소한의 자바스크립트 번들을 제공하여 성능을 극대화 하는 것이 목적.

 

 

 

 

 

 

AppDir / Server Component을 사용하게 된 후 달라진 점


 

서버 컴포넌트는 서버에서 데이터 Fetch 및 다운 받아야하는 종속성 등이 다 처리하기 때문에 이제getServerSidePropsgetStaticProps  메소드를 사용할 필요가 없어집니다.

 

 

 

SSG (Static Side Generation)

 

fetch 함수의 추가 설정이 없다면 Next에서 알아서 페이지를 caching하기 때문에 Static 페이지처럼 구현

// SSG (getStaticProps equivalent)
const getData = async () => {
	const res = await fetch('https://....')
	const data = await res.json()
	return data?.items as Array<{id: string, name: string}>
}

export default async function Page() {
	const data = await getData()
	
	return (
		<div>
			{data?.map((d) => <div key={d.id}>{d.name}</div>)}
		</div>
	)
}

 

 

ISR (Incremental Static Generation)

 

Static 페이지의 revalidate가 필요하다면 next: { revalidate: number } 추가

const getData = async () => {
	// ISR 
	const res = await fetch('https://....', 
	+	{ 
	+		next: { revalidate: 10 }
	+	}
	)
	const data = await res.json()
	return data?.items as Array<{id: string, name: string}>
}

 

 

SSR (Server Side Generation)

SSR 형태의 페이지로 만들고 싶다면 아래와 같이 cache: 'no-store' 설정 추가

 

cache 추가 옵션
- force-cache : default
- no-store
- next: { revalidate : number }

// SSR (getServerSideProps equivalent)
const getData = async () => {
	const res = await fetch('https://....', 
	+	{ cache: 'no-store' }
	)
	const data = await res.json()
	return data?.items as Array<{id: string, name: string}>
}
...

 

 

 

 

Dynamic Routes

	app
	|__ ...
	|__ posts
	|   |__ loadng.{js.tsx}
	|   |__ error.{js.tsx}
	|   |__ [slug].{js,tsx}

 

loading.{js,tsx} :

  • React suspense 의 활용 (behind the scene)
  • data fetching fallback loading UI

error.{js,tsx} :

  • data fetching failure UI
  • ❗️ must be a `client component
  • error.{js,tsx}는 아래와 같은 props를 기본으로 전달받는다
    - error
    - reset : 현 에러상태를 초기화 하는 함수

 

 

 

 

마무리 하며...


12 ver. 와 13 ver.의 차이는 AppDir이 도입되면서 Server Component가 기본적으로 설정되어 RSC(React Server Component), RCC(React Client Component)와 잘 조합하고 성질에 따른 코드 설정을 고려하며 개발을 임해야하는 것입니다.

RSC(Server Component)에 대하여 더 깊하게 알아볼 필요성이 느끼게 되어 다음편에는 RSC(Server Component)에 대하여 기록해볼 예정입니다.







3편 RSC(Server Component) 알아보기: https://k0502s.tistory.com/1201

 

NextJs - 3편: NextJs에서의 서버 컴포넌트 RSC(React Server Component)

3편: NextJs에서의 서버 컴포넌트 RSC(React Server Component) 2편에서 기록하면서 알아가게 된 NextJs 13 ver.에서 등장한 서버 컴포넌트 RSC(React Server Component)에 대하여 개인적으로 많이 생소하여 좋은 블로

k0502s.tistory.com

 

반응형

댓글