Fallback과 Next.js의 렌더링 (feat. SSG vs SSR)

2024. 2. 6. 01:04

Next.js의 공식문서를 통해 블로그 만들기 연습을 하던 중, 폭탄같은 오류를 만났다.

어떠한 stackoverflow의 답변도 나의 케이스에 해당되지 않았다.

 

모든 건 내가 fallback 옵션을 대충 이해한 상태로 코드를 막 쓰다 발생한 문제였다^^

 

이번 기회로 Next.js의 렌더링 방식과, fallback에 대한 모든 걸 마스터해보려고 한다.


SSG vs SSR

SSGStatic Site Generation, SSR은 Server Side Rendering의 약자다.

 

SSR은 "request time"에 데이터를 가져오고 렌더링하며, 이는 getServerSideProps함수를 통해 구현할 수 있다.

- 해당 함수로 넘겨진 props 내용은 처음 로드되는 HTML의 컴포넌트에 전달된다.

- page에서만 export될 수 있다

- JSON을 반환한다

- data fetching을 할 때, Next.js의 API Route를 꼭 활용하지 않아도 된다. DB나 써드파티 api등을 해당 함수 내부에서 직접 불러낼 수 있다. 

 

그런데, 굳이 "request time"에 data fetching을 할 필요가 없다면, (개인화된 정보에 의존하거나, 요청 타임에만 알 수 있는 정보가 필요하지 않다면) getStaticProps가 더 낫다.

 

- - - - - - - - - -

 

SSG는 "build time"getStaticProps가 반환한 props를 활용하여 페이지를 pre-rendering한다. 

 

빌드타임에 렌더링되면, 사용자의 요청 이전에 렌더링되므로 훨씬 빠르게 사용자에게 페이지를 보여줄 수 있다.

SSR 방식과 마찬가지로, 서버측에서 렌더링 하는 것이지만 시점이 다르다.

 

- 빌드타임에 프리렌더링할 때, 페이지의 HTML파일뿐만 아니라, 해당 함수의 실행 결과를 담은 JSON파일을 생성한다.

➡️ 이는 next/link나 next/router를 통한 Client Side Routing때 사용될 수 있다. (즉, 클라이언트 사이드에서의 페이지 전환이 일어날 때 함수를 호출하는 게 아니라, 추출된 JSON만 사용됨)

 

- next dev모드에서는 매 요청마다 호출되므로, 정확한 동작을 확인하고 싶다면 빌드 후 start하여 확인해보는 것이 좋다.

 

- - - - - - - - - -

 

그런데 묘하게 감이 잘 안 온다. "렌더링"에 대한 이해가 부족한 느낌.

또한, 각각의 렌더링 방식을 "왜" 쓰는지가 중요한데 잘 와닿지 않는다.

 

Rendering부터 살펴보자. 

브라우저의 렌더링

렌더링: HTML, CSS, JS로 작성된 문서를 파싱하여 브라우저에 보이도록 출력하는 것이다.

여기서 파싱이란, 텍스트 문서를 읽어와서 분석해서 "파스 트리"를 생성해가는 과정이다. 즉, 우리가 문법에 맞게 작성한 문서를 "해석"하는 것이다. 

 

기본적으로는 다음과 같은 단계를 거친다.

 

1. 브라우저가 HTML / CSS / JS / 이미지, 폰트 파일 등 리소스를 서버에 요청하고 응답 받음.

2. 브라우저의 렌더링 엔진이 응답받은 HTML, CSS를 파싱하여 각각 DOM, CSSOM을 생성함.

3. 이 둘을 바탕으로 "렌더 트리(Render tree)"를 생성함.

4. 브라우저의 자바스크립트 엔진은 응답받은 자바스크립트를 파싱하여 "AST(Abstract Syntax Tree)"를 생성하고 (바이트코드로) 변환하여 실행함. 

(JS를 통해 DOM, CSSOM을 변경할 수 있으며, 변경된 버전으로 다시 렌더 트리로 결합됨)

5. 렌더 트리를 기반으로 HTML의 레이아웃을 계산하고 / 브라우저에 요소를 페인팅한다.

 

여기서 핵심인 DOM(Document Object Model)에 대해 더 살펴보자. DOM은 어떻게 생성될까? 

 

1. 브라우저가 요청하는 것에 따라 서버는 HTML파일을 읽어서 메모리에 저장한 후, 여기에 저장된 바이트를 인터넷을 통해 응답해준다.

2. 브라우저는 응답받은 바이트를 meta태그의 charset 어트리뷰트에 지정된 인코딩 방식을 기반으로 문자열로 변환한다. (인코딩 방식은 response header에 담긴다)

3. 문자열 형태의 HTML문서는 문법적 의미의 최소단위인 토큰틀로 분해됨

4. 각 토큰을 객체로 변환하여 노드node들을 생성함. 

5. 생성된 노드들을 기본 요소로 하고, 태그들의 중첩 관계를 기반으로 부자 관계를 반영하여 트리 자료구조를 구성함. 이 구조가 바로 DOM이다!

 

 

큰 단계로 나누어보았을 때,

1) DOM과 CSSOM이 결합하여 "렌더 트리"를 형성하고

2) 레이아웃 계산하고

3) 페인트 되는 것

인데...

 

만약에 DOM과 CSSOM이 자바스크립트 코드에 의해 수정된다면 다시 레이아웃이 계산되고(= 리플로우 relflow) / 다시 페인트(= 리페인트 repaint)되어 리렌더링된다. 두 과정이 반드시 순차적으로는 진행되는 것은 아니다. 

 

* 이제야 그동안 지나가면서 들었던 Time to First Byte(TTFB), First Contentful Paint(FCP) 등을 알 것 같다... 우왕 ...


Next.js의 렌더링

이제야 Next.js의 렌더링에 대해 이해할 준비가 되었다 🥳

 

1) Pre-rendering

Next.js는 기본적으로 모든 페이지에 대해 프리렌더링을 진행한다. 즉, 클라이언트 사이드의 Javascript를 통해 작업이 완료되기 전에, 각 페이지를 위한 HTML을 미리 생성해둔다는 것이다. 

 

여기서 "미리 생성"이 정확히 무슨 의미일까.

이는 서버 사이드에서 ReactDOMServer.renderToString이라는 함수를 통해 페이지에 대한 HTML문서를 문자열로 가져오는 것이라고 한다. 해당 문서는 리액트가 동작하여 필요한 JS파일들을 불러올 수 있도록 하는 최소한의 script 태그가 포함된 상태다. 

 

(내가 위에서 공부한 원래 렌더링 방식에 따르면, 서버가 바이트 형태로 메모리에 저장했던 문서를 읽어들여 넘겨주고, 브라우저가 이를 받아서 인코딩하는 과정이 있었다. 이와 비교해보면 프리렌더링 방식이 확실히 클라이언트 사이드의 부담을 줄여주는 게 명확해보인다!)

 

해당 형태의 HTML로 브라우저에 의해 페이지가 로드되면, JS 코드가 실행되고 페이지를 fully interactive하게 만들어준다.

바로 이 과정이 그 유명한 "Hydration"인 것이다.

 

(클라이언트에서는 ReactDOM.render 함수를 통해 React Element를 렌더링하고 / ReactDOM.hydrate라는 함수를 통해 DOM요소에 이벤트 리스너를 적용하고 렌더링을 진행함)

 

2) 여기서 다시 정리해보자. Pre-rendering의 2가지 방식으로서 SSG와 SSR이 존재한다.

이 둘의 핵심적인 차이는 build time이나 request time이냐 였는데, 그럼 뭘 하는 타이밍을 말하는 걸까?

 

when it generates the HTML for page

= HTML이 생성되는 시점. 말그대로 프리렌더링되는 시점이 달라지는 것.

 

3) 어떠한 방식이 좋을까?

정답은 없다. 각각의 장단점을 아는 것이 필요하겠지!

 

공식문서에서는 성능상의 이유로, SSG를 권장한다.

SSG로 생성된 페이지는 별도의 설정 없이 CDN에 캐싱될 수 있기 때문이다. (요청마다 HTML을 생성하지 않고, 재사용함)

 

중요한 건, Next.js는 하이브리드하게 페이지마다 다양한 방식을 혼합하여 사용할 수 있다는 것이다.

(+) 한 페이지 안에서도 일부는 SSG나 SSR을 통해 렌더링하면서 client-side data fetching도 할 수 있다.


 

getStaticPaths와 fallback

먼 길을 돌아왔다. fallback 하나를 정리하기 위해 이렇게까지 돌아오다니.

아직 구멍이 많다는 의미기도 하니깐.. 그래도 재밌다 🫧

 

getStaticPaths()

getStaticPaths는 동적 라우팅을 활용할 때 정적으로 프리렌더링을 하려면 필요하다.

예를 들어, 블로그에 게시물인 /posts/[id].js가 있을 때 글의 id값에 따라 다른 데이터를 프리렌더링해야 하기 때문이다.

 

이렇게 경로가 "외부 데이터"에 의존하게 될 때, getStaticPaths()를 통해 정적으로 생성할 경로들을 미리 명시해주면 된다.

 

아래처럼 명시할 경로들을 배열 형태로 담고, fallback옵션을 지정해주면 끝!

export async function getStaticPaths() {
  return {
    paths: [
      { params: {id: '1'} }, 
      { params: {id: '2'} }, 
    ],
    fallback: true, 
  }
}

 

 

fallback

드디어 fallback이다! fallback은 boolean이나 'blocking'값을 가질 수 있다.

 

1) fallback: false

getStaticPaths에서 명시하지 않은 어떠한 경로든, 404페이지가 나온다.

빌드할 때, Next.js가 fallback이 false인 것을 확인하면, 지정된 경로들만 빌드하기 때문.

 

- 새로운 데이터가 거의 추가되지 않을 때

- 아주 적은 경로들만 생성될 때

유용하다. 

 

2) fallback: true

 

getStaticPaths에 명시된 경로들은 빌드 타임에 프리렌더링 된다.

 

그럼 명시되지 않은 경로들은?

404페이지를 띄우진 않고! "fallback"버전의 페이지를 해당 경로의 첫 요청 때 제공한다. 

 

백그라운드에서, getStaticProps함수를 통해 HTML파일과 JSON을 만들어내고,

이후 요청된 path에 해당하는 JSON을 받아서 새롭게 페이지를 렌더링한다.

 

(사용자는 fallback버전을 보다가 full페이지의 순서로 변화하는 화면을 보게 됨)

(새롭게 생성된 페이지는, 기존 빌드시 프리렌더링 한 리스트에 추가됨. 이후에 동일한 요청이 들어오면 미리 생성한 페이지를 반환함)

 

"fallback"버전의 페이지는 next/router의 router.isFallback값을 확인하여 조건 분기처리하면 되며, 이때 페이지 컴포넌트는 props로 "빈값"을 받는다.

 

- 빌드 시 생성할 페이지의 부담이 클 때

유용하다. (일단 필요한 만큼만 미리 생성해두고, 요청에 따라서 추가만 되는 거니깐!)

 

3) fallback: "blocking"

 

true와 유사하지만, 명시하지 않은 경로의 요청에 대해

fallback버전의 페이지를 보여주지 않고 SSR처럼 동작한다.

즉, HTML이 생성될 때까지 기다리다가 브라우저가 받으면 full 페이지를 보여준다. (그전에 로딩이나 폴백 상태가 표시되지 않음!)

 

동시에, true때와 마찬가지로 프리렌더링 한 리스트에 해당 경로를 추가하여, 다음 요청 시에는 미리 생성된 페이지가 제공 된다.

 

(fallback: true일 때, next/link나 router로 navigate되면 'blocking'처럼 동작함)


참고

 

* Next.js 공식문서

https://nextjs.org/docs/pages/building-your-application/rendering

https://nextjs.org/docs/pages/building-your-application/data-fetching/get-static-propshttps://nextjs.org/docs/pages/building-your-application/data-fetching/get-static-pathshttps://nextjs.org/docs/pages/building-your-application/data-fetching/get-server-side-props

* MDN문서 - 브라우저는 어떻게 동작하는가

https://developer.mozilla.org/ko/docs/Web/Performance/How_browsers_work

* 모던 자바스크립트 Deep Dive (책 참고)

* 패스트캠퍼스 Next.js 완전 정복 : 확장성 높은 커머스 서비스 구축하기 (강의 참고)

 

* 블로그

https://funveloper.tistory.com/164

https://khys.tistory.com/75

https://narup.tistory.com/235

https://enjoydev.life/blog/nextjs/1-ssr-ssg-isr

https://velog.io/@mskwon/next-js-static-generation-fallback