React의 RSC, RCC 집중 탐구(with Next.js)
프로젝트 수정 계획을 세우며 서칭하다 RCC(리액트 클라이언트 컴포넌트)는 App Router 기준으로 SSR을 한다는 사실을 알았습니다. 이로 인해 기존에 제가 SSR, CSR, RSC, RCC 등에 대해 잘못 알고 있던 지식과, 새롭게 배운 지식을 공유하고자 합니다.
이 블로그의 Post List 부분에 검색 기능을 넣기 위해 새롭게 아이디어를 짜며 피그마로 어떻게 만들지 시각화를 하는 도중
무언가 걸리는 점이 있었다.
내 블로그 페이지는 기존에 SSG로 만들었으나, Client Component를 쓰기 위해서 SSR로 동작해야만 했다.
그런데 내가 만든 ListLayout은 페이지가 아니니까, RSC가 맞지 않나..? 하는 생각이 들며 폭풍 검색을 했다.
아무래도 내가 RSC와 SSR을 제대로 이해하지 못하는 구나 싶어 열심히 검색을 했는데
이런 글을 발견했다.
이 분은 클라이언트 컴포넌트는 SSG인지 SSR인지 물어본다.
부끄럽지만 나는 이 글을 쓰기 전까지 내가 SSR, CSR, RSC 등에 대해 제대로 이해를 하고 있다고 생각했다.
그래서 당연히
아 맞지 맞지 RCC는 당연히 CSR 이잖아😇
라고 생각했었다...
이미지 출처
근데 CSR도 아니고, SSR인지 SSG인지를 물어..? 근데 심지어 위의 답변을 보면, RCC는 App Router에서는 기본적으로 SSR이라고 한다.
단순히 클라이언트 컴포넌트라고 생각해서 무조건 클라이언트 에서만 렌더링이 된다 생각했는데.. 아니었던 것일까?
그래서 이렇게 기존 지식의 혼란을 느낀 기회에, 차라리 제대로 RSC, RCC의 차이점 및 SSR, CSR과 같은 렌더링 기법에 대해서도 다시 한번 정리해보고자 한다.
React Client Component는 CSR인가 SSR인가?
1. 우선 공식문서에 따르면
우선 RCC의 렌더링이 어떻게 이루어지는지에 대해 알려면 공식문서를 읽는 것이 가장 정확하다고 생각했다. Next.js 공식문서에 나타난 Client Component에 대한 정의를 요약하자면
- Client Component는 Server에서 Pre-rendering 된다.
- Client Component는 클라이언트 JavsScript를 사용해 브라우저에서 실행할 수 있다.
- Client Component를 사용하기 위해서는, React의
use client
지시어를 파일 상단에 사용한다. - Client에서 렌더링해야 하는 모든 컴포넌트에,
use client
를 정의할 필요는 없다. 한번 경계를 지정하면, 하위의 모든 자식 컴포넌트와 모듈은 클라이언트 번들의 일부로 간주된다. - Next.js에서 Client Component는 전체 페이지 로딩의(초기 방문이나 브라우저 새로고침)일부인지 혹은 후속 탐색(Subsequent Navigations)의 일부인지에 따라 다르게 렌더링 된다.
아무래도 5번이 내가 찾는 가장 핵심 내용인 것 같아 좀 더 자세히 살펴보겠다.
a. 전체 페이지 로딩의 일부인 경우
초기 페이지 로딩의 최적화를 위해 Next.js는 React의 API를 사용해 Client Component, Server Component 모두에 대해 정적 HTML preview를 렌더링 한다.
즉 사용자가 어플리케이션을 처음 방문하면, client가 Client Component의 javascript 번들을 다운, 파싱, 실행할때까지 기다릴 필요 없이 페이지의 콘텐츠를 즉시 볼 수 있다.
서버에서는
- 리액트는 Server Component를 Client Component에 대한 참조를 포함하는 특별한 데이터 형식인 RSC Payload로 렌더링 한다.
- Next.js는 RSC Payload와 클라이언트 컴포넌트 자바스크립트 명령어를 사용해 서버에서 해당 경로에 대한 HTML을 렌더링 한다.
그리고 나서, 클라이언트 에서는
- 해당 HTML은 non-interactive한 preview를 즉시 표기하는데 사용된다.
- RSC Payload는 client 와 server 컴포넌트 트리를 reconcile하고, DOM을 업데이트 하는데 사용된다.
- JavsScript Instructions는 Client Component를 수화(hydrate)하고, UI를 Interactive하게 하는데 사용된다.
→ 즉, 서버에서 렌더링 되어 나온 HTML, 해당 컴포넌트에서 import해온 각종 패키지들의 js 번들 파일들을 클라이언트에게 내려주고 클라이언트는 해당 HTML을 렌더하며, js를 적용해나가는 Hydration과정이 진행된다.
b. Subsequetn Navigations(후속 탐색)
(후속 탐색) 이라는 번역이 적절하지는 않은 것 같지만, 어떻게 번역해야 할지 모르겠어서 우선은 직역하여 후속 탐색이라 했다.. 내 생각에는, server에 의해 초기 렌더링이 완료된 이후, client에서 후속 요청에 대한 렌더링이 발생하는 경우를 말하는 것 같다.
후속 탐색에서는,
- Client Component는 Server에서 render된 HTML 없이 전적으로 client에서 렌더링 된다.
이는, Client Component JavaScript 번들이 다운로드 되고 파싱 된다는 뜻이다. 번들이 준비 된다면, React는 RSC Payload를 이용해 Client와 Server 컴포넌트의 트리를 reconcile하고, DOM을 업데이트 한다.
RSC Payload란 무엇일까? The RSC Payload is a compact binary representation of the rendered React Server Components tree. It’s used by React on the client to update the browser’s DOM. The RSC Payload contains: (RSC 페이로드는 렌더링된 React 서버 컴포넌트 트리의 압축된 바이너리 표현입니다. 클라이언트에서 React가 브라우저의 DOM을 업데이트하는 데 사용됩니다. RSC 페이로드에는 다음이 포함됩니다:)
- The rendered result of Server Components (서버 컴포넌트의 렌더링 결과물)
- Placeholders for where Client Components should be rendered and references to their JavaScript files (클라이언트 컴포넌트가 렌더링될 위치와 해당 자바스크립트 파일에 대한 참조를 위한 플레이스홀더)
- Any props passed from a Server Component to a Client Component (서버 컴포넌트에서 클라이언트 컴포넌트로 전달된 모든 Props)
즉 쉽게 말하자면, JSON과 비슷한 형식의(response format) 직렬화 된 포맷이다.
여기까지, 공식문서에 기반해서 내가 이해하고 추측한 React Client Component의 개념에 대해 요약하자면
- 기본적으로 Server Component, Client Component 상관 없이 초기 로딩 최적화를 위해 이들은 서버에서 렌더링 된다. (직렬화를 이용해)
- 그러나, 만약 Event Handlers, 혹은 useEffect같은 js 상호작용이 추가 된다면 이후 초기 렌더링 및 확장을 위해 클라이언트에서 전달 받아 진행이 된다.
그러나 어딘가 아직 미흡하다. 제대로 이해하는것이 맞는지 궁금하다.
따라서 좀 더 자세하게 이해하기 위해 이번에는 여러 웹사이트를 탐구하며, 나와 비슷한 생각을 하거나 궁금한 점이 생긴 사람들의 질문과 답변을 찾아보겠다.
2. 커뮤니티의 질문과 답변들
a. Client Components Rendering on Client Side or Server Side #54114
질문자의 질문을 요약하자면 다음과 같다.
- 공식문서의 클라이언트 컴포넌트 설명 방식이 혼란스럽다
- Client Component가 서버에서 미리 렌더링 되고, 클라이언트에서 수화된다고 하지만 동시에 모듈 그래프 컴포넌트는 주로 클라이언트에서 렌더링 된다고도 한다.
- 그렇다면 Client Component가 서버에서 Pre-rendered 되는 상황과, 클라이언트에서 렌더링 되는 상황에 대해 알고싶다.
답변
- Client Component는 사전 렌더링 뿐만 아니라, 모든 형태의 SSR 중에 렌더링 됩니다.
- 그러나, SSR중 React Tree의 일부가 아닌 경우는 예외입니다.
- Primarily라는 단어가 최선의 선택은 아니겠지만, React Server Component와의 대조를 보여주기 위해서라고 생각됩니다.
- Server Component는 전적으로 서버에서 렌더링 됩니다.
- Client Component는 양쪽 환경에서 렌더링 됩니다. 또한 클라이언트에서 stateful/interactive한 동작을 수행할 수 있습니다.
- 즉, Client Component는 SSR output tree의 일부가 되도록 설계 되었으며, Client Component를 사용할 때 가장 중요한 목표는 상호작용성을 확보하는 것입니다.
답변 2
- React Server Component 트리는 VDOM과 비슷하지만, 직렬화 되어 RSC로 알려져 있습니다. 이 형식은 VDOM을 포함하지만, 직렬화 되어있고, 클라이언트 컴포넌트를 넣을 수 있는 일종의 구멍이 있습니다.
- Client Component tree와 Server Component tree를 reconcile 하는 것 == Client Component를 Server Component tree에 삽입하는 것입니다.
- 여전히 HTML을 생성할 수 있으며, 완전한 HTML 문자열을 가질 수 있습니다.
답변 3
- Client Component가 있다면, 이를 구동하는 JS 번들이 있다고 생각하세요.
- React Server Tree가 생성되면, Client Component 경계('use client')에 도달할 때마다 React Server Tree에 참조, 즉 구멍이 생깁니다.
- 그 동안, Client Component tree도 생성이 되지만 이것은 이러한 컴포넌트를 구동할 js 청크와 비슷합니다. 함수, 콜백등을 직렬화 할 수 없기 때문에 대신 이 모든 것들이 포함된 js 번들에 의해 참조됩니다.
답변 4
- Client Component는 반드시 Client에서만 HTML이 생성된다는 것을 의미하지는 않습니다.
- Client Component는 서버에서 사전 렌더링이 되거나, 혹은 빌드/런타임에서 next.js에 의해 normal한 SSR component처럼 사전 렌더링이 됩니다.
- 단지, 특정 component에 대한 client component의 js가 dom요소를 수화하기 위해 번들로 전송될 뿐입니다.
b. Why do Client Components get SSR'd to HTML? #4
RSC 이전의 멘탈모델은 이와 같았을 것입니다.
RSC는 기존의 멘탈 모델을 바꾸지 않습니다. 다만, 존재하는 모든 코드가 실행되기 전에 한 층의 레이어를 추가할 뿐입니다.
이 RSC 서버 레이어는, Remix의 loaders, Astro의 Templates, 빌드 타임 스크립트, 그리고 다른 코드들이 미리 실행 되는 것을 상기 시킬 것입니다. 그러나 React Component의 형태입니다. 명확하게 하자면, "이미 알고 있는 React의 모든 기능"을 Client라고 부릅니다.
즉, 이러한 이유로 Client Component는 SSR을 통해 HTML을 생성하는 것입니다. Client Component는 여러분이 이미 알고 있던 컴포넌트입니다.
이는 초기 로딩의 최적화를 위해 SSR을 할 수 있고, 그렇기에 Client Component는 SSR을 통해 HTML을 생성하게 되는 것입니다.
"Server", "Client"는 말 그대로 실제의 server와 client를 의미하지는 않습니다. 더 정확하게는 "React Server"와 "React Client"입니다.
Props는 언제나 React Server에서 React Client로 내려 갑니다. 그리고 그들 사이에는 직렬화의 경계(Serialization Boundary)가 있습니다.
React Server는 대개 빌드타임(default)에 실행되거나, 실제 서버에서 실행됩니다. React Client는 보통은 양쪽 환경(브라우저에서는 DOM을 관리하기 위해, 서버에서는 초기 HTML을 생성하기위해)에서 실행됩니다.
즉, 기존의 리액트 컴포넌트는 '클라이언트 컴포넌트'와 마찬가지며, 새롭게 추가된 영역의 컴포넌트가 '서버 컴포넌트'라고 정의 된다는 것이다.
c. How React server components work: an in-depth guide
Dan은 뼈, 살 모델이라고도 설명한다.
Next.js 기준에서 RSC 아키텍처의 렌더링 사이클을 보면
- 서버 컴포넌트가 먼저 렌더링 된다.
- 비동기 호출이 필요한 컴포넌트(
suspense:true
,use() hook
) 들은 fallback을 대신 그린 채 건너 뛰고, 렌더링 가능한 부분부터 먼저 렌더링을 완료하고 첫 응답으로 보낼 HTML을 생성한다. => 즉 Suspense가 서버측 렌더링 경계가 된다.
여기서 뼈와 살의 경계는 바로 use client
와 use server
인데.
밑에 있는 내용을 자세히 읽어보자.
use client
는 React Server Components app의 모듈 의존성 트리를 나눕니다. InspirationGenerator.js를 마크하고, 이것의 모든 의존성들을 클라이언트에서 렌더합니다.
여기까지 수행이 되더라도, 아직 상호작용이 안되는 서버 컴포넌트(서버 컴포넌트는 직렬화 되므로 이벤트 핸들러나 hooks를 사용 할 수 없음)와 클라이언트 컴포넌트의 정적인 버전(하이드레이션)만 렌더링 된 상태이다. 즉 그냥 HTML이다.
이렇게 만들어진 HTML 청크와 앱 실행에 필요한 js 번들이 함께 클라이언트에 전송이 되고, 하이드레이션까지 일어나면 드디어 정상적인 SPA앱의 모양을 갖춘다.
정리하자면
- 서버에 해당 페이지를 띄우기 위해 요청이 들어온다면, 서버는 컴포넌트 트리를 root부터 실행하며 직렬화된 형태로 재구성한다.
- 그러나 이때, 함수는 직렬화가 될 수 없다.(실행 컨텍스트, 스코프, 클로저까지 직렬화가 안되기 때문에)
- 직렬화 과정은 모든 서버 컴포넌트에 대해 실행하며, json 객체 형태의 트리로 재구성 할 때 까지 진행이 된다.
- RCC의 경우에는 건너 뛰게 된다. 그러나 RCC를 서버에서 해석하지 않고 건너 뛰게 된다면, 실제 컴포넌트 트리와 괴리가 생기므로 직접 해석하는 대신 "이곳은 RCC가 렌더링 되는 위치입니다." 라는 placeholder를 배치한다.
- RCC는 곧 함수이므로 직렬화가 되지 않는다. 즉, 이에 함수를 참조하는 것이 아니라 "module reference"라는 새 타입을 적용하고, 해당 컴포넌트의 경로를 명시함으로써 직렬화를 우회한다.
- 이렇게 도출된 트리를 Stream 형태로 Client가 전달 받고, 함께 다운로드한 js bundle을 참조하여, module reference 타입이 등장할 때 마다 RCC를 렌더링해서 빈공간을 채운 뒤 DOM에 반영을 하면 실제 화면에 스크린이 보여진다.
또, 블로그를 서칭하다 이러한 도표를 발견했다. RSC의 요청 흐름에 대해 정리한 도표
3. 결론
위의 공식문서와 여러 커뮤니티, 블로그 포스팅들을 통해 머릿속을 정리한 결과는 이러하다.
- 브라우저에서 요청이 들어온다면, React Server는 RCS Payload를 통해 React Server Component를 먼저 렌더링 한다. (직렬화 한 상태로, SSR와 달리 Hydration 단계를 건너 뛰고 렌더링이 된다.)
- 이 과정에서 React Server Component는 React Client Component의 참조를 가지고 있다. (구멍이 뚫린 채로)
- 즉 이로인해, React Client Component 또한 SSR을 통해 상호작용 부분을 건너 뛰고 HTML을 생성한다.
- 그러나 해당 HTML은 어디까지나 '정적인' HTML일 뿐, 여전히 js 번들을 전달 받지 않은 상태이다. 이들은 초기 렌더링을 위해 빠르게 서버에서 렌더링이 되어 유저에게 보여진다.
- 이후에는 RSC Payload에 의해 특정 상호작용이 필요한 클라이언트 컴포넌트 트리들은 reconcile되고, DOM 업데이트가 진행된다. → 즉, 서버에서 렌더링 되어 나온 HTML, 해당 컴포넌트에서 import해온 각종 패키지들의 js 번들 파일들을 클라이언트에게 내려주고 클라이언트는 해당 HTML을 렌더하며, js를 적용해나가는 Hydration과정이 진행된다.
- 클라이언트에서 완벽하게 수화가 이루어진다면, 유저는 컴포넌트와의 상호작용이 가능하다.