NextJS 이해 해보기_1 - CSR, SSR, SSG, ISR, Hydration

박재현·2024년 6월 7일
5

NEXT.JS

목록 보기
9/17
post-thumbnail

하고싶은 알고리즘 공부할 짬도 없고 프로그래밍 문제를 진득하게 고민하면서 풀 심적인 여유 조차 없는 나날이다.

몸이 5개면 좋겠다라는 생각을 퇴사하고나서도 할줄이야... 이유는 모르겠지만 일할때보다 더 멘탈이 짜치는 요즘이라 다 차단하고 혼자서만 공부하고 싶다는 요즘이다.

쓸데없는 말을 좀 했는데, 각설하고 NextJS를 최근에 알고있지만 다시한번 점검해보고 싶었던것들, 부족하다고 느낀 부분들, 애매모호해서 이해가 완벽하지 않다고 느낀 부분들에 대해서 찾아보면서 이해해보려고 애썼다.

CSR, SSR, SSG, ISR, Hydration, Server Component, Client Component, Server Action, getServerSideProps, getStaticProps, getStaticPaths, generateStaticParams, dynamicParams, Cache 등등등.

개인적으로 CSR과 RCC, SSR과 RSC에 대해서 애매모호 했었다.

그래서 NextJS 14버전 App Route와 12버전 Page Route를 번갈아 가면서 이해해보려 애썼는데 꽤 많은 부분이 App Route와 React 18버전에서 달라졌다고 느꼈다.

그리고 19버전에서는 useActionState 이라는 훅도 사용가능해 보이는데, 개인적으로 벌써부터 기대가 되는 부분이다.

여튼 이번 글에서는 제목과 같이 CSR, SSR, SSG, ISR, Hydration, getServerSideProps, getStaticProps, getStaticPaths, generateStaticParmas, dynamicParams에 대해서 이해한 내용을 바탕으로 정리해 보려고 한다.

그리고 다음 포스팅에서는 Server Component, Client Component, Server Action에 대해서 정리할 생각이다.

그 다음은 Server Component가 어떻게 동작하는지 좀 더 깊게 찾아볼 생각이고, 다음으로는 NextJS의 Route Segment Config를 정리 해볼생각이다.


CSR(Client Side Rendering)

React.js 만으로 Application을 개발하면 보통 이를 SPA(Single Page Application) 이라고 부르는데, 이는 CSR로 동작한다. (그니까 쉽게 React로 CRA해서 만든 결과물이 CSR임)

  • 초기에는 아무것도 없는 빈 HTML을 응답받음

  • 렌더링을 하는 주체가 클라이언트(보통 웹에서 의미하는 클라이언트는 크롬이나 사파리 같은 유저의 브라우저를 생각하면 됨)

  • HTML, JS, CSS 등등을 서버로부터 다운로드 받아서 클라이언트 측에서 내용을 출력하는걸 CSR 이라고 부름

빈 HTML 파일 로딩이 끝나고, React와 같은 JS등에 리소스 로딩이 끝나서 실행이 되면 렌더링이 일어나게 된다.

장점

  • 한번 로딩이 되면 빠른 UX를 제공해준다.
    리액트를 생각해자, 전체 페이지를 다시 그리는게 아닌 필요한 부분만 업데이트 하기때문에 앱을 사용하는것 같은 경험을 제공해 준다.

  • 서버입장에서 부하가 적다.

단점

  • 페이지 로딩 시간이 길다.
    서버로 부터 필요한 모든 리소스를 다운로드하고 난 다음 렌더링을 하기 때문에, 브라우저에서 JS가 Disable되어 있거나 어떤 이유로 데이터 네트워크 성능이 안좋으면 초기 구동 속도가 느리다. (엘레베이터에서 인터넷하면 잘 안되는걸 상상해보자)

  • 자바스크립트 Enable 필수.
    위에서 설명했듯이 자바스크립트가 일 못하니까 화면을 못그린다.

  • SEO 불리함.
    SEO는 Search Engine Optimization으로 검색엔진최적화 라는 뜻이다. SCR은 초기에 빈 HTML파일을 받아서 사용하기 때문에 어떤 내용물이 들어있고 meta tag들이 있는지 확인이 어렵다.
    따라서 "아 이건 그냥 아무것도 없이 비어있구나." 라고 판단하게 되기에 불리하다.

  • 보안에 취약함
    클라이언트에서 모든 코드를 내려받아 실행하기 때문에, 중요한 로직이나 key값이 노출될 수 있다.
    (나중에 말하겠지만 SSR과 RSC는 진짜 대박이다...)


SSR(Server Side Rendering)

렌더링의 주체가 서버다.
즉 서버에서 HTML을 생성해서 이를 브라우저에게 응답으로 전달해 주고 바로 눈으로 볼 수 있는 상태다.

장점

  • 페이지 로딩 시간이 빠르다. 왜냐면 이미 렌더링된 HTML을 서버로 부터 전달 받으니까.

  • 자바스크립트가 필요 없다.

  • SEO에 좋다.
    왜냐면 빈 HTML이 아니라 이미 렌더링된 HTML이기에 필요한 정보들이 HTML에 다 포함이 되어있기 때문에.

  • 보안이 뛰어나다.

  • 실시간 데이터를 보여준다.
    서버에서 가장 최신의 데이터를 사용해서 렌더링할 수 있기 때문에.

  • 사용자별 필요한 데이터를 사용할 수 있다(ex.MyPage).

단점

  • 요청 마다 HTML을 생성하기 때문에, SSG 또는 ISR에 비해 느리다.
    또한 Server작업이 오래걸리면 그동안 유저는 아무것도 보지못하고 기다려야 한다.

  • 너무 많은 요청으로 서버에 많은 부하가 걸릴 수 있다.

  • Data Cached가 되지 않는다. (매 요청마다 서버에서 컨텐츠를 읽어서 HTML에 포함해 렌더링 하기 때문에 보통 cache하지 않음)

  • 페이지 이동시 화면이 깜빡인다.


CSR vs SSR

CSR은 최초 화면을 유저에게 보여주기까지의 시간이 SSR보다 오래걸린다.

내 생각에 CSR이 무조건 오래 걸리는건 아닐 수 있다. SSR의 경우 Server의 부하가 많아서 Working Time이 길어지면 그 동안 유저는 아무것도 보지 못하게 된다.
다만 CSR은 React18 이후의 경우 Suspnese를 통해 스켈레톤과 같은것들을 유저에게 보여줄 수 있다.
즉, CSR에서 번들링된 JS를 다운로드받고 JS를 실행시키는것 보다 SSR에서 Server의 Working 시간이 길어지면 SSR이 최초 화면을 유저에게 보여주기까지 더 오래걸릴수도 있다는 생각이다.

또한 Search Engien이 크롤링을 할때 빈 HTML만 확인 가능한 CSR은 SEO에서 SSR대비 불리하다.

반면 SSR은 매번 서버에서 모든 연산을 수행하기 때문에 서버가 받는 부하가 더 크고, 화면 전환시 깜빡임이 생긴다.

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


Hydration

"수화시키다" 라는 의미로 물로 가득 채운다 혹은 물과 섞다 라는 의미의 단어다.

여기서 물을 리액트라고 생각하면 편하다.

서버에서 사용자에게 응답해주는 HTML에 React로 가득 채운다 라고 생각하면 이해가 편할것 같다.

즉, 위 사진에서 바싹 마른 건조된 딸기가 서버에서 내려다주는 Dry한 HTML파일이고, 이 Dry한 HTML파일에 React라는 물로 가득채워서 생기있게 만들어 User와 Interactive한 동작을 할 수 있도록 하는 작업이 바로 Hydration이다.

정적인 HTML 파일을 받으면 JS가 연결되어 있지 않기때문에 Event Listener들이나 ReactHook과 같은 Interaction이 동작하지 않는다.

이후에 React 및 JS를 응답받아서 로딩이 되면 정적인 HTML에 React를 가득 채우게된다.

정리해보면 아래와 같다.

  1. NextJS는 서버에서 컴포넌트를 읽어서 HTML을 생성하고, 이를 클라이언트에 응답한다.

  2. 클라이언트는 말 그대로 정적인 HTML을 전달받았다. 여기에 React로 작성한 자바스크립트 코드와 연결을 해줘야하는데, 번들링된 JS도 함께 내려받는다.

  3. 클라이언트는 전달받은 HTML을 hydrateRoot()를 호출해 렌더링된 HTML에 자바스크립트 코드를 연결시켜준다. (React로 가득 채워주기)


SSG(Static Site Generation)

서버에서 정적인(Static) HTML을 미리 생성하는걸 말하는데, NextJS 빌드할때 만든다.

따라서 서버측에서 렌더링을 하는게 Runtime중에 지속적으로 만들지 않고 빌드할때 1회만 한다.

그리고 기본적으로 NextJS의 렌더링 방식은 SSG다.

장점

  • 페이지 로딩 시간이 빠름
  • 자바스크립트를 필요로 하지 않음
  • SEO에 좋음
  • 보안이 뛰어남
  • Cached 상태임, 즉 아무리 새로고침을해도 로딩이 없음

단점

  • 정적인 내용, pre-rendered된 페이지 이니까
  • 실시간 데이터가 아님, pre-rendered다.
  • 사용자별 정보 제공이 어려움
    Profile Page를 만든다라고 할때, 회원가입한 모든 사용자별로 HTML파일을 Pre-rendered하기는 좀... 만약 회원이 2천만명이라면...??

ISR(Incremental Static Regeneration)

진짜 강의 번역을 누가했는지 모르겠는데, 구글번역기 돌렸는지 "증분 정적 재생" 으로 번역해놔서 강의 처음에 이해하는데 애좀 먹었다...ㅋㅋ

서버측에서 주기적으로 HTMl을 서버에서 생성해두고 내려주는걸 말하는데, 처음 빌드 시점에 만들어두고 설정한 주기마다 다시 랜덩리해서 HTML을 생성한다.

장점

  • 페이지 로딩 속도가 빠름
  • 자바스크립트를 필요로 하지 않는다.
  • SEO에 좋음
  • 보안이 뛰어남
  • Cached 상태
  • 데이터가 주기적으로 업데이트가 이루어짐

단점

  • 실시간 데이터가 아님
  • 사용자별 정보 제공이 어려움

Hybrid Rendering

NextJS는 Hybrid Web App 이라고도 불리는데, 성능이 좋은 Web App을 만들기 위해 두개 이상의 렌더링 방법을 사용하는걸 의미한다.

예를 들어서 메인 홈페이지는 ISR을 통해서 특정 interval로 업데이트를 하도록 하고,
About Us, QnA와 같이 변경이 거의 이루어 지지 않는 페이지는 SSG로 렌더링을 하고,
사용자의 Profile 페이지는 CSR / SSR을 하이브리드해서 만들 수 있다.

이처럼 하나의 Application에서 페이지의 특성에 따라 적절한 렌더링 방식을 사용해서 만들 수 있고, 심지어는 하나의 페이지 내에서도 하이브리드가 가능하다.

이렇게 처리하면 좋은점은 서버에서 HTML을 미리 생성해서 내려주면 SEO에 유리하고 빠르게 HTML을 응답해줄 수 있다.
그리고 CSR로 처리되는 컴포넌트에 대한 JS만 다운로드가 이루어지기 때문에 사용자는 조금 더 빠르게 Application 이용이 가능해진다.


getServerSideProps, getStaticProps, getStaticPaths 등등등 설명이 부족하다고 생각되어서 SSR, SSG, ISR에 대해서 조금 더 자세히 정리해본다.

일단 React18 이전버전과 NextJS 13이전버전, 즉 PageRoute를 기반으로 이해한 내용을 먼저 정리하겠다


SSR과 getServerSideProps()

먼저 ServerSideRendering과 getServerSideProps() 함수다.

SSR은 위에서 정리한것처럼, "페이지에 대한 요청이 있을때마다 서버에서 페이지를 만들어 반환한다."

이 부분을 잘 이해해야 한다. "페이지에 대한 요청이 있을때마다".

A라는 페이지를 서버에서 렌더링하는 SSR 방식으로 구현한 페이지라고 가정해보자.

그렇다면,

  • 사용자가 A라는 페이지를 초기에 방문했을때도
  • 사용자가 B라는 페이지를 갔다가 다시 A라는 페이지로 돌아올때도
  • 사용자가 A라는 페이지에 위치해서 새로고침을 눌를때도

계속 서버에게 야, 일해 짜식아 빨리 HTML만들어서 내놔라!!!🤬 라고 요청하는 것이다.

따라서 Build될때 딱 한번 HTML을 만드는 SSG와 다르게 SSR은 Runtime내내 요청이 있을때마다 서버에서 HTML을 만든다.

그러면 SSR을 언제 쓰는걸까??

User에게 Loading State를 보여주지않고, 한번에 필요한 데이터를 모두 보여주고 싶을때 SSR을 사용할 수 있다.

무슨말이냐?! 아래와 같은 코드가 있다고 예를 들어보자.

export default async function SomePage() {
	const data = await (await fetch("/api/products")).json();
  
  	return (
		<>
          	<span>상품 목록</span>
      		<ul>
                {data.map((product) => <li key={product.number}>상품 이름은 {product.name} 입니다.</li>)}
          	</ul>
		</>
    );
}

당장 생각나는 예시가 없어서 막 적었는데 위 코드가 정상적으로 실행이 되는지는 모르겠다.

일단 대충 GET Request를 이용해서 API한테 전체 상품 목록을 달라고 요청하고, 받아온 전체 상품 목록들을 화면에 뿌려준다고 가정해보자.

위 코드가 일반적인 CSR로 유저의 브라우저에서 실행이 된다면 아마도 아래와 같은 모습일거다.

  1. 상단에 "상품 목록" 이라는 글자만 보인다. 왜냐면 전체 상품 목록을 아직 불러오고 있기 때문이다.
  2. 전체 상품 목록을 다 불러왔다면, 상품의 이름들이 보인다.

여기서 우리는 유저의 입장에서 생각해보면 아래 두가지를 경험하게 된다는걸 알 수 있다.

  1. 전체 상품을 아직 다 받아오지 못해도 일부 UI는 먼저 볼 수 있구나?
  2. 필요한 데이터 로딩이 완료가 될때까지 나는 로딩이 되는걸 보면서 기다려야 하구나? (Ex. Spinner, 빈화면, 로딩 텍스트 등등등)

이럴때 getServerSideProps 함수로 SSR을 이용하면 유저에게 로딩되는 과정을 보여주지 않고 한번에 필요한 모든데이터를 보여줄 수 있다!

getServerSideProps()

어떻게?

function SomePage({ data }) {
  	return (
		<>
          	<span>상품 목록</span>
      		<ul>
                {data.map((product) => <li key={product.number}>상품 이름은 {product.name} 입니다.</li>)}
          	</ul>
		</>
    );
}

export async function getServerSideProps() {
  	const data = await (await fetch("/api/products")).json();
  
  	return {
    	props: {
          	data
        }
    };
}

export default SomePage;

이렇게!

getServerSideProps 함수를 사용하면 서버에서 렌더링할 수 있도록 도와준다.

그리고 해당 함수 클라이언트가 아닌 서버에서 실행되기 때문에 Data Fetch나 DB조회가 가능하다!!!

또 그렇게 Data Fetch 혹은 DB조회한 결과를 props: {결과값} 형태로 반환해주면, NextJS가 알아서 해당 반환값을 SomePage 컴포넌트의 인자로 넣어준다!!!

정리하면 아래와 같다!

  • getServerSideProps 함수는 서버단에서 Data Fetch나 DB 조회와 같은 기능을 가능하게 한다.
  • NextJS는 개쩌는 프레임워크라서 getServerSideProps 함수를 사용하면, 해당 함수의 반환값을 화면을 렌더링하는 Component의 입력값으로 자동으로 넣어준다.
  • 결과적으로 서버단에서 필요한 외부데이터를 모두 확보해서 HTML을 모두 렌더링해서 클라이언트한테 넘겨준다!!!!
  • 마지막으로 getServerSideProps 함수는 페이지에 대한 요청이 있을때마다 실행이 된다.

결과적으로 위와같이 getServerSideProps 함수를 사용하면 사용자는 로딩상태를 볼 필요없이 필요한 정보를 한번에 볼 수 있다.

근데 몇가지 단점이 있다.

먼저 페이지에 대한 요청이 있을때마다 실행이 되기때문에 Cached되지 않는다. 그래서 새로고침이 생기면 서버에서 새로 렌더링을해서 HTML을 내려주는걸 기다려야한다.

또 유저는 Server에서 렌더링이 완료될때까지 아무것도 볼 수 없다.

일반적으로는 CSR보다 SSR이 좀 더 빨리 유저가 무언가를 볼 수 있다고 하는데, 내 생각에 약간의 구멍이 있다고 생각한다.

Server가 어떠한 이유로 Busy한 상태라면 필요로하는 외부 데이터를 Gathering하는데 오래걸리고 그 결과 pre-rendered된 HTML을 만드는데 오래걸린다고 가정해보자.

약 10초가 걸렸다고 가정하면, 유저는 10초동안 아무것도 못본채 계속 하염없이 기다려야한다.

이 경우 CSR이라면 유저는 로딩되는 상태를 지켜볼지언정 다른 UI는 볼 수 있다 예를들어 로딩 스피너라던지, 탭바 혹은 헤더라던지.

그래서 로딩이 오래 걸려서 유저가 어떠한 UI도 못보는 상황은 피하고 싶은데??? 하지만 유저에게 로딩없이 화면은 바로 보여주고싶은걸? 이라는걸 만족하기 위해서 SSG라는게 있다!


SSG와 getStaticProps, getStaticPaths

먼저 바로 위에서 정리한 getServerSideProps를 활용해서 SSR방식을 사용하면 아래와 같은 문제가 생길수 있다고 했다.

  • 서버가 바쁘고 재수가 없으면 서버가 pre-redered된 HTML을 만들때까지 User는 아무런 UI도 못보고 멍때려야 한다.
  • 페이지 요청이 있을때마다 HTML을 새로 만들기 때문에 Data가 Cached되지 않고, 서버에 부하를 줄 수 있다.

위 단점을 보완할수 있는 방법이 바로 SSG이다.

SSG는 Static Site Generation으로 말 그대로 정적인 HTML을 만들어두는걸 이야기한다. 언제? NextJS Build할때!

따라서 서버에서 Pre-Rendered 페이지를 방문하면 유저는 어떠한 기다리는 과정없이 바로 페이지를 확인할 수 있다.

왜?

이미 서버에서 HTML을 미리 다 만들어뒀고, 만들어둔거 받아와서 바로 보여주기만 하면 되니까.

이렇게 하면 페이지 요청을 아무리 많이 보내도 서버에서는 추가적인 일을 할 필요없이 그냥 HTML그대로 주면된다, 또한 만들어둔 HTML그대로 쓰기때문에 보여주고자 하는 Data들또한 이미 Cached된 상태다.

무슨말이냐!?

getServerSideProps + SSR로 만든 페이지를 새로고침하면, 새로고침 할때마다 Server에서 HTML을 만들때까지 기다려야한다.

하지만 SSG로 만들어진 페이지를 아무리 새로고침해봤자 Cached상태 이기 때문에 기다릴 필요가 없다.

때문에 SSR보다도 훨씬 더 빠른 속도를 보여준다!

오케이 알겠어, 그러면 SSR + getServerSideProps의 단점은 어떻게 보완해???

  • 서버가 바쁘고 재수가 없으면 서버가 pre-redered된 HTML을 만들때까지 User는 아무런 UI도 못보고 멍때려야 한다.

  • 페이지 요청이 있을때마다 HTML을 새로 만들기 때문에 Data가 Cached되지 않고, 서버에 부하를 줄 수 있다.

위 2가지 단점을 어떻게 보완할까??

이럴때 사용하는게 바로 getStaticProps 함수다!

getStaticProps()

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

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

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

export default Page

getServerSideProps 함수와 크게 다르지 않다!

getStaticProps 함수는 Build할때 필요로 하는 외부 데이터를 서버단에서 모두 받아와서 해당 데이터를 포함해서 HTML 페이지를 렌더링한다.

그리고 그 렌더링해둔 HTML페이지를 유저가 요청이 올때마다 전달해주면 된다!

이렇게 표시할 화면이 많이 변하지 않는 페이지, 예를 들어서 QnA 혹은 About Us와 같은 페이지들을 SSG로 빌드하기 좋다.

오케이! 그러면 표시할 화면이 자주 변하는 페이지는?? 그니까 URL이 막 변하는 동적 라우팅은 SSG 어찌 적용해...??


getStaticPaths()

SSG가 갖고있는 문제점 중 하나가 바로 동적라우팅을 사용하는 경우다.

여기서 동적라우팅이 뭐냐면 URL이 변하는것들을 의미하는데, URL에 변수가 들어가는 경우라고 생각하면 편하겠다. (Ex: /products/[id])

동적라우팅을 사용해서 query에 들어오는 값들은 NextJS가 빌드할때 알 수 없다.

무슨말이냐!? 위와같이 /products/[id] 에 해당하는 부분을 SSG를 적요하고 싶다고 가정해보자.

그러면 NextJS는 Build할때 모든 데이터가 포함된 HTML을 미리 만들어 두려고 할건데 이렇게 말할거다.

NextJS: 그래서 HTML을 몇개나 만들어야 하는데 씹덕아 ㅡㅡ

아!! 그렇다! NextJS한테 몇개의 HTML을 만들어줘야 하는지 말해줘야 하는데, 이 경우는 Dynamic URL에 대응하는 HTML이기 때문에 getStaticPaths 함수를 사용하게 된다.

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

정리하면 getStaticPaths() 함수로 우리가 만들고자 하는 동적라우팅 페이지들의 Path를 NextJS에게 말해준다. (id:1 / id:2 이런식으로)

그 다음으로 만들고자 하는 Path들의 변수, 즉 /products/[id] 라고 가정하면 [id]에 들어가는 값들을 getStaticPaths()함수가 반환하게 되는거고, 이 반환되는 변수를 getStaticProps() 함수가 인자로 받게된다.

그럼 getStaticProps() 함수는 인자로 받은 변수 [id]를 기반으로, 외부 데이터를 fetch해서 필요한 정보들을 화면을 렌더링하는 컴포넌트에게 props로 넘겨준다.

그러면 NextJS는 몇개의 HTML을 어떤 데이터로 만들고 어떤 URL에 대응해야 하는지 알 수 있게 된다!!

오호!

근데 문제가 있다. 예를 들어서 현재 내 데이터베이스에 등록된 상품의 갯수 약 500만개라고 해보자.

그러면 빌드할때 500만개의 HTML을 미리 만들어둔다...? 이건 미친짓이다.

그럼 어찌해..?

fallback 옵션!

모든 페이지를 사전에 미리 만들어둘 수 없다. 그건 미친짓이다 허허

그래서 필요한게 바로 fallback 옵션이다.

fallback옵션은 true | fasle | "blocking" 이렇게 3개의 옵션이 있다.

getStaticPaths() 함수를 통해서 미리 만들어진 path가 아닌 다른 path가 URL로 들어오면 어찌해야하나?

404를 반환해줘야 한다고 생각할 수 있지만, 애석하게도 해당 path는 미리 만들어둔 path는 아니지만 분명히 내 DB에 존재하는 path라면...? 아 이럼 404를 띄우면 안된다...

그렇다고 DB에 존재하는 모든 상품을 미리 HTML을 만들어두는건 진짜 미친짓이다, 그리고 새로운 상품이 등록된다면 해당 상품의 페이지도 만들어야 하는데 빌드하는 과정에서 새로운 상품이 들어왔는지 알기는 어렵다.

따라서 미리만둘어두지 않은 path에 대해서도 대응을 해야하고 이럴때 필요한게 바로 getStaticPaths 함수의 fallback 옵션이다.

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

위에서 말한것 처럼 fallback옵션은 true | fasle | "blocking" 이렇게 3개의 옵션이 있다.

그리고 이 옵션에 따라서 미리 만들어 두지 않은 path의 경우라면 getStaticProps함수의 동작이 달라진다.

  • false: 404 페이지를 반환한다. 즉 대응하지 않겠다는 소리!
  • true: 우선 페이지의 fallback에 해당하는 부분을 보여주고, 뒤에서는 getStaticProps 함수를 통해서 요청을 보내 HTML을 만들라고 시킨다.
    그 이후 HTML이 다 만들어지면 해당 파일은 Pre-Rendered 리스트에 추가하고 화면에 보여준다.
    이후 요청부터는 새로 만드는게 아닌 미리 만들어둔 Pre-Rendered된 HTML을 반환한다.
  • "blocking": true와 동작은 유사하나, 파일을 렌덜이하는 동안 fallback을 보여주는게 아니라, SSR + getServerSideProps처럼 서버에서 HTML을 다 만들때까지 무작정 기다린다.
    이후 요청부터는 만들어둔 Pre-Rendered된 HTML을 반환한다.

여기서 나는 그냥 Pre-Rendered된 HTML미리 만들생각 없고, 그냥 유저가 요청 보낼때 마다 바로바로 만들래~ 라고 하면(왜냐면 상품이 54개 라고하면 54만개 페이지 다 만들수 없으니까.), getStaticPaths() 반환값을 그냥 빈 리스트로 넘겨줘도 상관없던걸로 기억한다. 대신 fallback은 blocking으로 줘야함!

fallback

React 18의 Suspnese를 사용해본 경험이 있다면 바로 알겠다.

서버에서 HTML을 만드는 동안 대신 보여줄 화면을 이야기 한다.

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

정리해보면 SSG는 getStaticProps와 getStaticPaths를 이용하면 유저에게 로딩없이 미리 만들어진 HTML을 내려줘서 바로 화면을 볼 수 있도록 하고, 서버 부하도 적고, 필요에 따라서는 SSR처럼 바로바로 HTML도 만드니까 최강인가!!!!?

아니다!

한가지 약점이 있다! 바로 HTML을 딱 1번만 만든다라는게 약점이다.

뭔소리야 장점이라면서..? 라고 생각하겠지만 동시에 단점이다.

왜 단점이냐? SSG는 Build할때 HTML을 딱 한번 만들어두고 이를 Pre-Rendered된 HTML이라고 한다.

그렇다면 Build가 다 끝나고난 이후에 바뀐 데이터들은???? 이것들은 HTML에 포함되지 않는다.

따라서 SSG를 사용하면 Latest, Fresh한 Data를 볼 수 없다는게 문제다. OMG!!

그럼 어떻게 개선할 수 있을까?

그럴때 사용하는게 바로 ISR이다.


ISR (Incremental Static Regeneration)

진짜 아직도 강의 번역한 사람 누군지... "증분 정적 재생" 으로다가 번역을 하다니...

이게 뭐냐? 쉽게 말하면 SSG로 Pre-Redered된 HTML을 최신 데이터로 Update하도록 도와주는놈이다.

오호~~

ISR은 모든 페이지를 다시 빌드할 필요없이, 필요한 페이지만 정적으로 다시 생성한다.

ISR을 이용해서 SSG로 만들어진 HTML을 재생성(업데이트)하는 방법은 크게 2가지다.

일단 이를 Revalidate라고 부른다.

revalidate time

첫번째 방법은 재생성하는 시간 주기를 설정해주는 방법이다.

이 방법은 getStaticProps() 함수의 옵션인데, 페이지를 방문한 이후로 revalidate 시간 동안은 cached된 페이지를 보여주고 설정한 시간 이후에 들어오는 첫번째 요청에 대해서는 역시 cached된 페이지를 보여주고난 다음에 해당 페이지를 다시 Re-Build한다.

이후에는 역시 새로 만든 HTML을 보여주고, revalidate시간은 다시 흐른다.

여기서 중요한건 revalidate time을 10으로 설정하면, 매 10초마다 HTML을 재생성 하는게 아니라는 것이다.

1~10초까지는 기존의 페이지를 보여주고. 10초가 지난 시점에서 들어오는 첫번째 유저에게는 기존 페이지를 보여주고 바로 페이지를 재생성한다. 그리고 그 다음으로 들어오는 유저에게는 새롭게 만든 페이지를 보여주고 revalidate time의 시계는 다시 움직인다!

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

다른 한가지 방법은 바로 데이터가 업데이트 될때 Re-Build를 하는 방법이다.

바로 위에서 살펴본 revalidate 옵션을 사용하면, 설정해둔 시간 동안은 cached된 페이지를 보게된다.

그리고 새롭게 재생성 하기 위해서는 설정해둔 시간이 지난 다음에 새로운 요청이 있어야 한다.

하지만 on-demand revalidation은 내가 원하는 시점에 "이 페이지 업데이트 해라~" 라고 일을 시키는것과 같다.

해당 기능이 베타버전일때 unstable_revalidate 함수일때는 업데이트 하고자 하는 URL을 넘겨만 주면 되었는데, 안정화가 되면서 토큰을 사용해야 하는 방법으로 바뀐것 같다.

https://<your-site.com>/api/revalidate?secret=<token> 을 통해서 NextJS에게 요청을 보내면 되는것으로 보인다.

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

여기까지 설명을 길게 했는데, 아직 설명하지 않은게 있다 바로 generateStaticParams 함수다.


generateStaticParams()

일단 위에서 설명한 SSR, SSG, ISR은 React 18 이전버전과 NextJS의 Page Route를 기반으로 먼저 설명했다.

하지만 현재는 App Route를 사용하고 React 18 버전을 사용하고 있지 않은가!!?

어떻게 바뀌었을까!!!!!!

일단 NextJS 13버전부터 App Route 방식으로 변했다, 그리고 React18을 완벽하게 지원하는데 가장 큰 변화가 바로 React Server Component다.

저녀석 때문 더 이상 getStaticProps니 getStaticPaths니 getServerSideProps니 안써도 된다.

기존의 NextJS에서는 getServerSideProps 함수 혹은 getStaticProps라는 함수를 이용해서 서버에 접근할 수 있었다.

즉 해당 함수를 사용해서 외부 데이터를 얻어오는 동작을 서버에서 렌더링하면서 할 수 있었다.

때문에, Data fetch등을 수행할때는 반드시 getServerSideProps 또는 getStaticProps 함수를 사용해야 하고, 해당 함수의 반환값을 page를 렌더링하는 컴포넌트의 props로 넘겨서 사용했어야 했다.

반면 React Server Component는 그 자체가 서버에서 렌더링되므로, 컴포넌트 내부에서 Data Fetch를 실행해도 무방하다.

즉, data가 필요한 컴포넌트에서 직접 data fetch가 가능해졌고, NextJS의 App Route의 모든 컴포넌트는 기본적으로 Server Component다.

따라서 더 이상 외부 데이터를 사용하기 위해서 번거로운 행위를 할 필요가 사라졌다.

그냥 해당 컴포넌트에서 async / await을 사용하면 된다.

진짜 대박이다!!

그럼 generateStaticParams는 뭐냐?

뭐긴! SSG 만들때 쓰는 함수다. 쉽게 말해서 getStaticProps와 getStaticPaths의 업그레이드 버전? 이라고 생각하면 편할것 같다.

getStaticProps를 대체하자

매우매우 간단하다!

export default async function ProductDetail({
    params,
}: {
    params: { id: string };
}) {
  return <span>여기서 props로 받아서 사용하면 개꿀!</span>;
}

export async function generateStaticParams() {
    const products = await PrismaDB.product.findMany({
        select: {
            id: true,
        },
    });

    return products.map((product) => ({
        id: product.id + "",
    }));
}

전체적인 사용 방법은 getStaticProps와 차이가 없다.

여기서 주의해야할 부분은 반환 형태는 리스트로 해야하는것과, 렌더링하는 컴포넌트의 함수인자와 동일하게 맞춰서 반환해주는것만 주의해주자.

├ ● /products/[id]                       187 B          96.3 kB
├   ├ /products/5
├   ├ /products/6
├   ├ /products/2
├   └ [+2 more paths]
├ ○ /products/add                        22.8 kB         107 kB
├ λ /profile                             160 B          84.5 kB
├ ○ /sms                                 1.43 kB        90.8 kB
└ ○ /test                                448 B          84.8 kB
+ First Load JS shared by all            84.3 kB
  ├ chunks/69-84289ba9e2c20ce0.js        29 kB
  ├ chunks/fd9d1056-5e221561fa023f51.js  53.4 kB
  └ other shared chunks (total)          1.9 kB


ƒ Middleware                             31.4 kB

○  (Static)   prerendered as static content
●  (SSG)      prerendered as static HTML (uses getStaticProps)
λ  (Dynamic)  server-rendered on demand using Node.js

저렇게 코드를 작성하고 Build를 해보면 위와 같이 products/[id] 해당 부분에 5개의 pre-rednerd된 페이지가 비르된걸 확인할 수 있다!

그러면 동적URL 대응은 어찌함!?

이것도 간단하다!!!

그냥 generateStaticParams 함수의 반환값을 빈 리스트를 반환한다.

대신 해당 page.tsx 파일 상단에 마법의 문장 하나만 선언해주면 되는데 바로 export const dynamicParams = true; 다.

export const dynamicParams = true;

export default async function ProductDetail({
    params,
}: {
    params: { id: string };
}) {
  return <span>여기서 props로 받아서 사용하면 개꿀!</span>;
}

export async function generateStaticParams() {
    const products = await PrismaDB.product.findMany({
        select: {
            id: true,
        },
    });

    return [];
}

위와같이 코드를 작성하면, fallback="blocking"과 같은 형태로 SSG를 구현할 수 있다.

├ ● /products/[id]                       187 B          96.3 kB
├ ○ /products/add                        22.8 kB         107 kB
├ λ /profile                             160 B          84.5 kB
├ ○ /sms                                 1.43 kB        90.8 kB
└ ○ /test                                448 B          84.8 kB
+ First Load JS shared by all            84.3 kB
  ├ chunks/69-84289ba9e2c20ce0.js        29 kB
  ├ chunks/fd9d1056-5e221561fa023f51.js  53.4 kB
  └ other shared chunks (total)          1.89 kB


ƒ Middleware                             31.3 kB

○  (Static)   prerendered as static content
●  (SSG)      prerendered as static HTML (uses getStaticProps)
λ  (Dynamic)  server-rendered on demand using Node.js

즉 구체적으로 "이런 이런 이런 페이지를 미리 만들어줘!" 라고 함수를 반환했다면 위와 같이 /products/1, /products/2 이렇게 pre-rendered HTML을 만들지만, Dynamci URL을 대응할 SSG는 위와같이 단순히 빈 리스트만 넘겨주면 된다.

자세히 보면 ● /products/[id] 는 동일하지만, 구체적으로 만들어둔 HTML페이지는 안보이지 않는가~


그럼 ISR은 어떻게 사용해?

최신 버전의 NextJS에서 SSG를 생성했다면 해당 페이지를 업데이트 해야하는데 어떻게 업데이트 하면 좋을까??

바로 이전에 Caching 관련 포스팅을 하면서 적어둔 방법을 사용하면 된다.

여기서는 간단하게 작성하고 끝내겠다.

  1. export const revalidate 사용하기.

위 예제처럼 10초를 설정해두고 싶다면, page.tsx 상단에 아래와 같이 사용하자.

export const revalidate = 10;

revalidate 공식문서

  1. revalidatePath 사용하기

번거로운 토큰은 필요없다!

내가 재생성하고 싶은 URL을 함수로 넘겨주면 끝!

import { revalidatePath } from 'next/cache'

revalidatePath('/blog/post-1')

revalidatePath 공식문서

다음은 서버컴포넌트와 클라이언트컴포넌트를 공부해서 정리해봐야겠다.

profile
기술만 좋은 S급이 아니라, 태도가 좋은 A급이 되자

2개의 댓글

comment-user-thumbnail
2024년 6월 9일

와 재현님 정리 너무 재밌게 봤어요. 항상 느끼지만 너무 멋져요. 저 정말 쉬지않고 정리하신글 쭉 읽었어요. ssg, isr은 잘 쓰지않아서 잘 몰랐는데 덕분에 더 알고 갑니다! 👍👍👍👍
next.js 에서 첫 페이지 요청시에는 ssr로 만들어지지만 그 이후에, Link 태그나 useRouter훅을 통한 이동을 할 경우에는 서버에서 데이터 페칭이 필요한 것들은 json 데이터로 받아와 csr 하는 것으로 알고있어요. 그래서 next.js를 유니버설 렌더링이라고 하더라구요.
똑똑재현님은 알고 계실 것같은데 저는 이 부분은 좀 늦게 알고 깨달아서 부끄러웠던 적이 있어서 댓글에 적어놓고 갑니다 홍홍.

1개의 답글

관련 채용 정보