Nextjs의 Server Component에 대해 공부하는 중, RSC(React Server Component)가 어떻게 렌더링되는가에 대한 설명을 읽다 잘 이해가 되지 않았습니다.
그래서 구체적인 이해를 위해, 좀 더 자세히 뜯어보려합니다. 이해가 가지 않는 설명을 첨부하고 이를 뜯어보는 방식으로 이해해볼게요.
설명 중 초기에 아래와 같은 설명이 있습니다. 하지만 잘 이해가 가지 않았어요.
On the server, Next.js uses React's APIs to orchestrate rendering.The rendering work is split into chunks: by individual route segments and Suspense Boundaries.
(서버에서 Next.js는 React의 API를 사용하여 렌더링을 조정합니다. 렌더링 작업은 개별 경로 세그먼트와 Suspense Boundaries(opens in a new tab)에 따라 청크로 분할됩니다.)
위 부분들이 잘 이해가 되지 않았어요.
"The rendering work is split into chunks"라는 표현은 "렌더링 작업이 청크 단위로 분리된다"는 뜻입니다. 여기서 "청크(chunk)"는 작업을 나누는 논리적 단위, 즉 작은 조각을 의미합니다.
청크로 나누는 이유
대규모 애플리케이션에서 UI를 한꺼번에 렌더링하면 시간이 오래 걸리고, 첫 화면이 나타나는 속도(First Contentful Paint)가 느려질 수 있습니다.
따라서 렌더링을 여러 작은 작업(청크)으로 나누면 다음과 같은 이점이 있습니다:
- 더 빠른 초기 로딩: 필요한 UI부터 먼저 사용자에게 보여줄 수 있음.
- 효율적인 리소스 관리: CPU나 네트워크 리소스를 효율적으로 사용.
- 사용자 경험 개선: 점진적으로 콘텐츠를 로드하면서 사용자 대기 시간을 줄임.
청크를 나누는 기준
Next.js는 다음 두 가지 기준으로 렌더링을 청크 단위로 나눕니다:
Route Segments (라우트 세그먼트)
문서에서 제가 이해가지 않았던 문구 individual route segments
에 대한 설명이에요.
라우트 세그먼트는 Next.js에서 페이지를 구성하는 계층적인 디렉토리 구조를 기반으로 합니다. 각 세그먼트는 별도의 렌더링 단위로 취급됩니다.
이런 방식으로 필요한 부분만 서버에서 렌더링하여 클라이언트에 전달할 수 있습니다. 즉, 페이지 단위별로 청크를 나누는 거네요.
Nextjs에서 빌드를 하면,아래와 같이 빌드를 하면 나타나는 파일에 페이지별 js 파일,청크를 확인할 수 있어요.
React의 Suspense는 UI에서 데이터 로딩 같은 비동기 작업을 다룰 때 사용됩니다.
Suspense Boundary는 데이터가 준비되지 않은 상태에서도 다른 부분의 렌더링을 가능하게 만듭니다.
그 다음 설명으로는 다음과 같은 설명이 있습니다.
각 청크는 두 단계로 렌더링됩니다:
1. React는 Server Components를 React Server Component Payload (RSC Payload)라는 특별한 데이터 형식으로 렌더링합니다.
2.Next.js는 RSC Payload와 Client Component JavaScript 지침을 사용하여 서버에서 HTML을 렌더링합니다.
(React renders Server Components into a special data format called the React Server Component Payload (RSC Payload).
Next.js uses the RSC Payload and Client Component JavaScript instructions to render HTML on the server.)
머리에 추상적으로 그려져서 잘 이해가 되지 않았어요.
알아보겠습니다.
문서에서는 다음과 같이 설명하고 있죠.
RSC Payload는 렌더링된 React Server Components 트리의 간결한 이진 표현입니다.
이는 클라이언트에서 React가 브라우저의 DOM을 업데이트하는 데 사용됩니다. RSC Payload에는 다음이 포함됩니다:
- Server Components의 렌더링된 결과
- Client Components가 렌더링되어야 할 위치와 해당 JavaScript 파일에 대한 참조
- Server Component에서 Client Component로 전달된 모든 props
서버 컴포넌트로 이루어진 SSG 페이지에 들어가 네트워크 탭을 확인해보면 다음과 같은 네트워크 요청을 확인할 수 있습니다.
네트워크 탭에서는 querystring으로 _rsc=tpi3d
이라는 것이 붙어있어요
응답을 보면 알 수 없는 것들로 가득차있네요?
해당 요청은 무슨 역할을 하는 것이고 응답값은 또 무엇이며 어떤 역할을 하는 것일까요?
네트워크 요청
네트워크 요청헤더
네트워크 응답
즉,위 RSC 요청에 대한 응답은 서버 컴포넌트에 대한 이진 표현으로 응답된 것인가봐요.
그리고 역할은 클라이언트 즉, 브라우저에서 React가 브라우저의 DOM을 업데이트하는 데 사용되는거네요.
결과적으로 위 RSC 요청은 서버에서 렌더링된 서버 컴포넌트 정보와 클라이언트 컴포넌트 관련 정보를 브라우저에 응답으로 전달하여, 브라우저가 이를 바탕으로 페이지를 업데이트할 수 있도록 하는 역할을 합니다.
처음에는 위와 같은 의문이 들었는데 이제는 의문이 해결되었습니다.
서버 컴포넌트가 서버에서 렌더링되는데도 클라이언트에게 정보를 넘겨주는 이유는 서버 컴포넌트와 클라이언트 컴포넌트가 협력하여 동작하기 때문입니다.
서버 컴포넌트는 서버에서 미리 렌더링된 HTML 구조를 생성합니다. 그러나 이 결과를 클라이언트로 넘기는 이유는 클라이언트 측에서 추가적인 렌더링이나 상호작용이 필요할 수 있기 때문입니다.
서버 컴포넌트는 정적인 데이터를 렌더링합니다.
하지만 해당 컴포넌트 내에서 클라이언트 컴포넌트가 포함되어 있다면, 이 클라이언트 컴포넌트를 브라우저에서 실행해야 합니다.
이를 위해 서버 컴포넌트의 렌더링 결과와 함께 클라이언트 컴포넌트의 위치 및 props 정보를 클라이언트로 전달해야 합니다.
클라이언트 컴포넌트는 브라우저에서 실행되는 JavaScript로 정의됩니다. 서버에서 클라이언트로 전달되는 "instructions"는 클라이언트 컴포넌트를 렌더링하고 작동시키는 데 필요한 정보와 명령을 포함합니다.
이 instructions는 다음을 포함합니다:
JavaScript 파일 경로
클라이언트 컴포넌트를 브라우저에서 실행하려면 해당 컴포넌트를 정의한 JavaScript 파일이 필요합니다.
React는 RSC Payload에 이 경로를 포함시켜 클라이언트가 해당 파일을 로드하도록 지시합니다.
Props 정보
클라이언트 컴포넌트로 전달되어야 할 props 데이터.
이를 통해 서버에서 처리된 데이터와 클라이언트에서의 상호작용이 일관성을 유지합니다.
상태와 이벤트 핸들링 연결
클라이언트 컴포넌트는 사용자와 상호작용(예: 클릭, 입력 등)을 처리하기 위해 이벤트 핸들러를 설정해야 합니다.
React는 클라이언트 컴포넌트가 이러한 동작을 수행하도록 지침을 포함합니다.
아까 위에서 알아본 저의 프로젝트 SSG 페이지에 대한 RSC Payload 응답값을 기준으로 구체적으로 알아볼게요.
3:I[1994,["946","static/chunks/app/(page)/quiz/layout-baaf889432faea58.js"],"default"]
4:I[9275,[],""]
5:I[1343,[],""]
6:I[3606,["231","static/chunks/231-8f68daddfd42f71b.js","185","static/chunks/app/layout-fe03fd3fabb6c2db.js"],"default"]
7:I[9791,["231","static/chunks/231-8f68daddfd42f71b.js","185","static/chunks/app/layout-fe03fd3fabb6c2db.js"],"default"]
8:{"fontFamily":"system-ui,\"Segoe UI\",Roboto,Helvetica,Arial,sans-serif,\"Apple Color Emoji\",\"Segoe UI Emoji\"","height":"100vh","textAlign":"center","display":"flex","flexDirection":"column","alignItems":"center","justifyContent":"center"}
9:{"display":"inline-block","margin":"0 20px 0 0","padding":"0 23px 0 0","fontSize":24,"fontWeight":500,"verticalAlign":"top","lineHeight":"49px"}
a:{"display":"inline-block"}
b:{"fontSize":14,"fontWeight":400,"lineHeight":"49px","margin":0}
0:["IBUjUjp6SZGio4yaIwNH4",[[["",{"children":["(page)",{"children":["quiz",{"children":["__PAGE__",{}]}]}]},"$undefined","$undefined",true],["",{"children":["(page)",{"children":["quiz",{"children":["__PAGE__",{},[["$L1","$L2",null],null],null]},[[null,["$","$L3",null,{"children":["$","section",null,{"className":"flex flex-col justify-center items-center \n p-[20px] bg-white border-[1px] border-[#E0E0E0] \n min-w-[700px] max-w-[1000px] min-h-[700px] max-h-[800px]\n rounded-primary \n shadow-sm \n overflow-y-scroll\n !justify-start","children":["$","$L4",null,{"parallelRouterKey":"children","segmentPath":["children","(page)","children","quiz","children"],"error":"$undefined","errorStyles":"$undefined","errorScripts":"$undefined","template":["$","$L5",null,{}],"templateStyles":"$undefined","templateScripts":"$undefined","notFound":"$undefined","notFoundStyles":"$undefined"}]}]}]],null],null]},[null,["$","$L4",null,{"parallelRouterKey":"children","segmentPath":["children","(page)","children"],"error":"$undefined","errorStyles":"$undefined","errorScripts":"$undefined","template":["$","$L5",null,{}],"templateStyles":"$undefined","templateScripts":"$undefined","notFound":[["$","title",null,{"children":"404: This page could not be found."}],["$","div",null,{"style":{"fontFamily":"system-ui,\"Segoe UI\",Roboto,Helvetica,Arial,sans-serif,\"Apple Color Emoji\",\"Segoe UI Emoji\"","height":"100vh","textAlign":"center","display":"flex","flexDirection":"column","alignItems":"center","justifyContent":"center"},"children":["$","div",null,{"children":[["$","style",null,{"dangerouslySetInnerHTML":{"__html":"body{color:#000;background:#fff;margin:0}.next-error-h1{border-right:1px solid rgba(0,0,0,.3)}@media (prefers-color-scheme:dark){body{color:#fff;background:#000}.next-error-h1{border-right:1px solid rgba(255,255,255,.3)}}"}}],["$","h1",null,{"className":"next-error-h1","style":{"display":"inline-block","margin":"0 20px 0 0","padding":"0 23px 0 0","fontSize":24,"fontWeight":500,"verticalAlign":"top","lineHeight":"49px"},"children":"404"}],["$","div",null,{"style":{"display":"inline-block"},"children":["$","h2",null,{"style":{"fontSize":14,"fontWeight":400,"lineHeight":"49px","margin":0},"children":"This page could not be found."}]}]]}]}]],"notFoundStyles":[]}]],null]},[[[["$","link","0",{"rel":"stylesheet","href":"/_next/static/css/2d7a7b6971cb1693.css","precedence":"next","crossOrigin":"$undefined"}]],["$","html",null,{"lang":"en","children":["$","body",null,{"className":"__variable_1e4310 __variable_c3aa02 antialiased","children":[["$","$L6",null,{}],["$","main",null,{"className":"w-full h-[calc(100vh-80px)] bg-background flex justify-center items-center","children":["$","$L7",null,{"children":["$","$L4",null,{"parallelRouterKey":"children","segmentPath":["children"],"error":"$undefined","errorStyles":"$undefined","errorScripts":"$undefined","template":["$","$L5",null,{}],"templateStyles":"$undefined","templateScripts":"$undefined","notFound":[["$","title",null,{"children":"404: This page could not be found."}],["$","div",null,{"style":"$8","children":["$","div",null,{"children":[["$","style",null,{"dangerouslySetInnerHTML":{"__html":"body{color:#000;background:#fff;margin:0}.next-error-h1{border-right:1px solid rgba(0,0,0,.3)}@media (prefers-color-scheme:dark){body{color:#fff;background:#000}.next-error-h1{border-right:1px solid rgba(255,255,255,.3)}}"}}],["$","h1",null,{"className":"next-error-h1","style":"$9","children":"404"}],["$","div",null,{"style":"$a","children":["$","h2",null,{"style":"$b","children":"This page could not be found."}]}]]}]}]],"notFoundStyles":[]}]}]}]]}]}]],null],null],["$Lc",null]]]]
d:I[870,["513","static/chunks/app/(page)/quiz/page-c334575102ab2fe2.js"],"default"]
2:["$","div",null,{"className":"w-full","children":[["$","div",null,{"className":"flex flex-col gap-2 mt-24","children":[["$","h1",null,{"className":"text-title1 text-center","children":"개발 퀴즈"}],["$","p",null,{"className":"mb-10 text-title2Normal text-center","children":"퀴즈를 통해 개발 지ì‹ì„ 테스트해 보세요!"}]]}],["$","$Ld",null,{}]]}]
c:[["$","meta","0",{"name":"viewport","content":"width=device-width, initial-scale=1"}],["$","meta","1",{"charSet":"utf-8"}],["$","title","2",{"children":"퀴즈 시작하기"}],["$","meta","3",{"name":"description","content":"퀴즈를 통해 개발 지ì‹ì„ 테스트해 보세요.í”„ë¡ íŠ¸ 엔드, 백엔드, ë°ì´í„°ë² ì´ìŠ¤, 네트워í¬, ì•Œê³ ë¦¬ì¦˜ 등 다양한 ì£¼ì œì˜ í€´ì¦ˆë¥¼ 풀어보세요."}],["$","meta","4",{"name":"application-name","content":"개발 퀴즈 앱"}],["$","link","5",{"rel":"author","href":"https://github.com/BrightJun96"}],["$","meta","6",{"name":"author","content":"jjalseu"}],["$","meta","7",{"name":"generator","content":"Next.js"}],["$","meta","8",{"name":"keywords","content":"Next.js,퀴즈,quiz,코아,개발ìž,개발지ì‹,개발ìžë“¤ì„ 위한 퀴즈,개발 퀴즈"}],["$","meta","9",{"name":"referrer","content":"origin-when-cross-origin"}],["$","meta","10",{"name":"creator","content":"jjalseu"}],["$","meta","11",{"name":"publisher","content":"jjalseu"}],["$","link","12",{"rel":"icon","href":"/favicon.ico","type":"image/x-icon","sizes":"16x16"}],["$","meta","13",{"name":"next-size-adjust"}]]
1:null
I 타입 데이터:
위 응답 데이터를 확인해보면 다음과 같은 데이터가 있는데요.
3:I[1994,["946","static/chunks/app/(page)/quiz/layout-baaf889432faea58.js"],"default"]
6:I[3606,["231","static/chunks/231-8f68daddfd42f71b.js","185","static/chunks/app/layout-fe03fd3fabb6c2db.js"],"default"]
여기서 I는 클라이언트 컴포넌트를 나타내며, 배열 안에 위치 정보와 관련된 참조 데이터가 포함됩니다.
첫 번째 숫자 (1994, 3606)는 고유 ID로 보이며, 컴포넌트의 위치를 추적하거나 식별하는 데 사용됩니다.
"static/chunks/..."는 해당 컴포넌트를 로드하기 위한 JavaScript 파일 경로로, 클라이언트 컴포넌트의 구체적인 위치와 관련이 있습니다.
segmentPath 필드:
위 응답 데이터를 확인해보면 다음과 같은 데이터가 있는데요.
segmentPath는 서버 컴포넌트 트리에서 클라이언트 컴포넌트가 위치한 경로를 정의합니다.
경로는 부모-자식 관계를 나타내며, 클라이언트 컴포넌트를 어디에서 렌더링해야 하는지를 명확히 합니다.
클라이언트 컴포넌트 관련 props 데이터:
{"parallelRouterKey":"children","template":["$","$L5",null,{}]}
parallelRouterKey는 클라이언트 컴포넌트를 렌더링할 특정 영역(슬롯)을 지정하며, 어떤 props나 템플릿 데이터가 적용되어야 하는지도 정의됩니다.
이벤트 참조 정보
위 응답값 기준으로 다음과 같은 데이터가 있습니다.
["$","div",null,{"className":"flex flex-col","children":[["$","h1",null,{"className":"text-title1 text-center","children":"개발 퀴즈"}],["$","button",null,{"onClick":["$","$handleClick"],"children":"시작하기"}]]}]
onClick: "handleClick"과 같은 이벤트 핸들러를 클라이언트에서 실행할 것을 정의하고 React는 이 핸들러를 클라이언트에서 활성화하고 바인딩합니다.
그 다음으로 문서에서는 다음과 같이 설명을 하는데요.
그런 다음 클라이언트에서는:
HTML을 사용하여 경로의 빠른 비인터랙티브 미리보기를 즉시 표시합니다 - 이는 초기 페이지 로드에만 해당됩니다.
React Server Components Payload를 사용하여 Client와 Server Component 트리를 조정하고 DOM을 업데이트합니다.
JavaScript 지침을 사용하여 Client Components를 하이드레이션(opens in a new tab)하고 애플리케이션을 인터랙티브하게 만듭니다.
(Then, on the client:
The HTML is used to immediately show a fast non-interactive preview of the route - this is for the initial page load only.
The React Server Components Payload is used to reconcile the Client and Server Component trees, and update the DOM.
The JavaScript instructions are used to hydrate Client Components and make the application interactive.)
위에서는 reconcile the Client and Server Component trees
이에 대한 문구가 잘 이해가 되지 않았어요.
자세히 살펴볼게요.
reconcile the Client and Server Component trees
라는 문장은 클라이언트와 서버 컴포넌트 트리의 상태를 동기화하고 일치시키는 과정을 의미합니다.
Reconciliation(재조정)은 React의 핵심 알고리즘입니다. 컴포넌트의 변경 사항을 감지하고, 필요한 부분만 업데이트하여 효율적으로 DOM을 관리합니다.
Server Component는 서버에서 렌더링된 상태로 클라이언트에 전달됩니다. 이후 클라이언트는 서버에서 받은 데이터와 로컬 상태(또는 UI)를 비교하여 최적의 DOM 업데이트를 수행합니다.
서버에서 렌더링된 UI 상태를 나타냅니다.
React의 "React Server Component Payload (RSC Payload)"로 클라이언트에 전달됩니다. Payload는 아까 무엇인지 위에서 자세히 알아봤었죠?
클라이언트에서 JavaScript를 통해 상호작용 가능한 상태를 나타냅니다.
서버에서 내려온 HTML과 RSC Payload를 기반으로 상호작용 가능한 React 트리를 형성합니다.
클라이언트와 서버 트리는 서로 다른 작업을 담당합니다.
서버 트리는 HTML과 초기 상태를 전달합니다.클라이언트 트리는 상호작용 및 상태 업데이트를 처리합니다.
클라이언트가 서버에서 제공된 UI를 이어받아 일관성을 유지하고, UI 업데이트를 원활히 하기 위해 둘의 트리가 정확히 동기화되어야 합니다.
초기 HTML 렌더링
클라이언트는 먼저 서버에서 전달된 HTML을 "비상호작용적인" 형태로 렌더링합니다.
이는 사용자가 빠르게 초기 화면을 볼 수 있도록 합니다.
Tree 동기화
클라이언트는 서버에서 받은 RSC Payload를 사용해, 서버 트리와 현재 클라이언트 트리를 비교(Reconcile)합니다.
React는 차이를 계산하고, 필요한 부분만 DOM에 반영하여 효율적인 업데이트를 수행합니다.
Hydration (하이드레이션)
클라이언트에서 JavaScript를 통해 상호작용 가능한 컴포넌트를 활성화합니다.
이 과정에서 서버에서 렌더링된 HTML에 클릭, 이벤트 등의 기능이 연결됩니다.