기본적인 웹사이트의 렌더링 과정은 다음과 같다.
1. 클라이언트에서 HTTP를 통해 서버로 데이터를 요청한다.
2. 서버는 요청에 따라 데이터를 처리하여 HTTP로 응답한다.
3. 클라이언트는 응답받은 리소스를 파싱하여 인터페이스를 렌더링한다.
하지만 NextJS는 서버사이드렌더링에 좀 더 최적화된 렌더링을 가지고 있는데, NextJS에서는 클라이언트 컴포넌트와 서버 컴포넌트의 경계를 나누어 각각 다른 렌더링 과정을 거친다. 이번 포스팅에서는 서버 컴포넌트와 클라이언트 컴포넌트의 렌더링 과정에 대해 공식문서를 바탕으로 정리하였다.
Server Componets는 경로(route)와 서스펜스 Boundary에 따라 청크로 분할되는데, 분할된 각각의 청크는 다음의 두가지 단계를 거쳐 렌더링된다.
✅ RSC(React Sever Components Payload)란?
렌더링된 React Server Components 트리의 이진표현으로 다음과 같은 정보가 담겨있다.
1. 서버 컴포넌트의 렌더링 결과
2. 클라이언트 컴포넌트가 렌더링 될 곳의 Placeholder와 자바스크립트 파일의 참조
3. 서버 컴포넌트에서 클라이언트 컴포넌트로 전달할 모든 데이터
이후 클라이언트 환경에서 다음과 같은 작업을 거친다.
서버 컴포넌트를 렌더링 할 때에는 다음과 같은 전략을 선택할 수 있다.
정적 렌더링은 빌드될 때 혹은 revalidation 시 백그라운드에서 실행된다. 이는 블로그나 제품 페이지 같이 공통적인 데이터를 보여주고 자주 변경되지 않는 페이지에서 유용하다.
동적 렌더링은 각 사용자별로 요청할 때 마다 렌더링된다. 따라서 사용자 맞춤형 데이터나 검색결과 같이 요청에 맞춰 데이터를 보여줘야 할 때 유용하다.
웹사이트는 한가지 전략만을 사용하기보다는 대부분 두가지를 동시에 사용한다. 동적인 정보가 필요한 곳에는 동적 렌더링을, 그렇지 않은 곳은 정적 렌더링을 적절히 혼합하여 사용한다.
요청 시에만 알 수 있는 정보에 의존하는 API들을 사용하면 전체 경로를 동적 렌더링으로 전환할 수 있다. 동적 API 로는 cookies, headers, connection, draftMode, searchParams props, unstable_noStore가 있다.
스트리밍은 기본적으로 Next.js App router에 내장되어 있으며, 스트리밍을 사용하면 전체 콘텐츠가 렌더링되기 전에 일부를 보여줄 수 있다.
클라이언트 컴포넌트는 첫 방문이거나 새로고침으로 전체 페이지를 로드한 것인지(Full page load), 아니면 방문한 페이지에서 탐색을 한 것인지(Subsequent Navigations)에 따라 다르게 렌더링된다.
NextJS는 초기 페이지 로드 최적화를 위해 React의 API를 사용하여 클라이언트 컴포넌트와 서버 컴포넌트 모두를 서버에서 정적 HTML 미리보기(Static HTML preview)로 렌더링한다.
따라서 첫 방문시에는 정적 HTML 미리보기(Static HTML preview)를 사용해 즉시 페이지를 볼 수 있다.
만약 Full page load가 아니라면 클라이언트 컴포넌트는 전적으로 클라이언트에서 렌더링된다. 즉, 클라이언트 컴포넌트의 자바스크립트 번들이 다운되고 준비된 이후, RSC를 사용하여 클라이언트와 서버 컴포넌트 트리를 조정하고 DOM을 업데이트하는 과정을 거친다.
리액트 18버전 기준으로, 리액트는 Client React DOM APIs를 사용하여 클라이언트(브라우저)에서 React 컴포넌트를 렌더링 한다. 이때 일반적인 CSR 방식으로 리액트를 실행하면 createRoot()를 사용하여 React 루트를 생성하고, root.render를 호출하여 React 컴포넌트를 표시한다. 그런데 root.render를 처음 호출한다면 React 루트 내부의 모든 HTML 콘텐츠를 지우고 클라이언트에서 렌더링 된 컴포넌트로 교체하는데, 서버에서 렌더링한 HTML 콘텐츠가 있다면 이 과정에서 사라지게 된다. 그럼 어떻게 리액트에서 SSR을 구현할 수 있는 것일까?
그 이유는 HTML 콘텐츠가 담긴 DOM을 바탕으로 리액트를 사용한다면 createRoot()가 아닌 hydrateRoot()를 사용하여 렌더링하기 때문이다. hydrateRoot()를 사용하여 Hydrate를 완료한 후에 root.render
를 호출하게 되는데, 그렇다면 기존 HTML을 없애지 않고 HTML 콘텐츠가 담긴 DOM 노드 안에 React 컴포넌트를 넣을 수 있게 된다. 물론 이 작업들은 NextJS에서 호출하고 있기 때문에 별도로 사용하지 않아도 된다.
출처: https://nextjs.org/docs/app/building-your-application/rendering,
https://react.dev/reference/react-dom/client