최근 리액트 서버 컴포넌트에 대한 좋은 글을 보게되어 중요한 부분을 요약 정리
컴포넌트란?
React Server Component, 그 전에 React에서 말하는 컴포넌트란 무엇인가?
컴포넌트란? 데이타를 인자로 받고 JSX 를 리턴하는 JS 함수 이다.
그렇다면 React 컴포넌트는 어떻게 브라우저에 렌더링 할까?
1.
React 문법으로 컴포넌트 작성
import React, { useState } from 'react';
function FormComponent() {
const [formData, setFormData] = useState({ name: '', email: '' });
const handleChange = (e) => {};
const handleSubmit = (e) => {};
return (
<form onSubmit={handleSubmit}>
<div>
<label>Name:</label>
<input
type="text"
value={formData.name}
onChange={handleChange}
/>
</div>
<div>
<label>Email:</label>
<input
type="email"
value={formData.email}
onChange={handleChange}
/>
</div>
<button type="submit">Submit</button>
</form>
);
}
export default FormComponent;
Plain Text
복사
2. babel을 만나 vanilla js 로 변환
import React, { useState } from 'react';
function FormComponent() {
const [formData, setFormData] = useState({ name: '', email: '' });
const handleChange = (e) => {};
const handleSubmit = (e) => {};
return React.createElement(
'form',
{ onSubmit: handleSubmit },
React.createElement(
'div',
null,
React.createElement(
'label',
null,
'Name:'
),
React.createElement('input', {
type: 'text',
name: 'name',
value: formData.name,
onChange: handleChange
})
),
React.createElement(
'div',
null,
React.createElement(
'label',
null,
'Email:'
),
React.createElement('input', {
type: 'email',
name: 'email',
value: formData.email,
onChange: handleChange
})
),
React.createElement(
'button',
{ type: 'submit' },
'Submit'
)
);
}
export default FormComponent;
Plain Text
복사
3. js를 기반으로 React Element 생성 (구 Virtual DOM)
절대 코드를 자세히 보지마
// 가상의 Virtual DOM 생성 (개념적으로 이해하기 위해 단순화)
const reactElement = {
type: 'form',
props: {
onSubmit: handleSubmit,
children: [
{
type: 'div',
props: {
children: [
{ type: 'label', props: { children: 'Name:' }, key: null, ref: null },
{
type: 'input',
props: { type: 'text', name: 'name', value: formData.name, onChange: handleChange },
key: null,
ref: null
}
]
},
key: null,
ref: null
},
{
type: 'div',
props: {
children: [
{ type: 'label', props: { children: 'Email:' }, key: null, ref: null },
{
type: 'input',
props: { type: 'email', name: 'email', value: formData.email, onChange: handleChange },
key: null,
ref: null
}
]
},
key: null,
ref: null
},
{ type: 'button', props: { type: 'submit', children: 'Submit' }, key: null, ref: null }
]
},
key: null,
ref: null
};
Plain Text
복사
4. React Element 를 기반으로 Fiber Node 생성
5. Reconciliation
React Element 와 Fiber Node 를 통해 Reconcile을 수행 DOM으로 렌더링을 시킴
서버 컴포넌트
그렇다면 서버 컴포넌트라는것은 무엇일까?
그 전에 서버 컴포넌트가 왜 탄생했는지에 대한 설명을 먼저 필요하다.
모든 기술은 기존 기술이 해결하지 못한 문제들을 개선하기 위해 발전한다. 기존의 Page Router 방식에서는 서버와 클라이언트의 경계가 명확히 구분되어 있긴 하지만, 모든 것이 페이지 단위로 렌더링되다 보니, 모든 데이터 페칭이 완료된 후에야 클라이언트로 HTML이 전달되는 구조였다. 이는 사용자에게 초기 콘텐츠를 보여주기까지의 시간이 길어지는 문제를 초래했다.
이를 해결하기 위해 [HTML Streaming]이 도입되었고, 이를 가능하게 하는 기술이 바로 서버 컴포넌트이다.
그렇다면 서버컴포넌트는 어떻게 작동 하는가?
결론: 서버에서는 크게 두개의 정보를 생성하여 클라이언트로 전송한다.
ON THE SERVER
1.
RSC payload 생성 (렌더링에 필요한 binary 데이타)
•
서버 컴포넌트의 렌더링된 결과물
•
placeholder (클라이언트 컴포넌트 가 렌더링 될 위치의 빈자리)
•
클라이언트 컴포넌트에서 사용될 JS 파일들의 위치
•
서버 컴포넌트가 클라이언트 컴포넌트에게 전달 할 인자 (props)
2. Render Tree HTML 뼈대를 만든다
•
RSC payload를 활용하여 클라이언트 컴포넌트의 정적 데이타까지 서버에서 만듬 (useState 등과 같이 각종 hooks 를 실제로 서버에서 실행은 못하고 초기 값만 가져올수 있음)
•
즉, 서버 컴포넌트 + 클라이언트 컴포넌트가 합쳐진 전체 HTML이 만들어짐
서버에서 전달하는 RSC payload와 Render Tree HTML 모두 서버내에서 React 를 실행할때 변환되는 React Element, Fiber Node 정보를 기반으로 RSC payload 와 Render Tree HTML 을 생성
상호작용 handler (onClick), broswer 의 기능 사용은 서버컴포넌트에선 불가능. 오직 클라이언트 컴포넌트에서만 구현이 가능
때문에 FE 개발자는 최대한 많은 컴포넌트를 서버 컴포넌트로 제작하여 서버에 캐시시켜야 하고 클라이언트 컴포넌트는 꼭 필요한 부분만 사용해야 사용자 경험과 서버 부하를 줄일 수 있음
아주 간단한 형태의 RSC Payload 예시
{
"0": [
"style",
"/_next/static/css/main.css"
],
"1": [
"script",
"/_next/static/js/main.js"
],
"2": [
"div",
{
"children": [
["h1", {"children": "Hello, Server Component!"}],
["p", {"children": "This is rendered on the server."}]
]
}
],
"3": [
"ClientComponent",
{
"id": "client-component-1",
"props": {"message": "Hello from the Client Component!"}
}
],
"4": [
"script",
"/_next/static/js/chunk-client-component.js"
],
"$type": "payload",
"$version": 1
}
Plain Text
복사
Render Tree HTML 뼈대
Press enter or click to view image in full size
서버에서 생성하는 뼈대 HTML tree
ON THE CLIENT
1.
만들어진 HTML을 “즉시” 보여준다. (initial first render)
2.
서버로부터 온 Render Tree HTML 뼈대에 있는 placeholder 부분을 RSC payload를 이용하여 채워준다.
3.
클라이언트 컴포넌트를 실행하기 위해 필요한 JavaScript 번들을 로드한 후, 컴포넌트를 실행한다.
4.
서버에서 온 서버 컴포넌트 트리와 클라이언트에서 실행한 컴포넌트까지 결합하여 최종 컴포넌트 트리를 완성한다.
5.
Reconcile
6.
2에서 생성된 서버 컴포넌트와 클라이언트 컴포넌트가 결합된 트리와 서버에서 이미 렌더링된 HTML을 비교하여, 변경된 렌더 트리만 다시 렌더링한다 (Reconcile의 핵심).
7.
Hydrate (click등의 상호작용 인터렉션)
8.
클라이언트 컴포넌트에서 필요한 상호작용이나 브라우저의 기능을 사용할 수 있도록, React가 기존의 서버 렌더링된 HTML에 이벤트 핸들러를 연결하여 클라이언트 측에서 인터랙션을 활성화한다.
전통적인 서버 사이드 렌더링
Press enter or click to view image in full size기존 Server Side Render 방식 예시
Press enter or click to view image in full size5개의 호출중 1개가 완료된 모습 빨리 브라우저로 가고 싶어하는 모양이다.Press enter or click to view image in full size하지만 어림없다. 5개가 모두 끝나야한다.Press enter or click to view image in full size지각자 한명 때문에 연대책임을 물었다.Press enter or click to view image in full size그렇게 아무잘못 없는 브라우저는 10초뒤에 렌더링 되고 말았다.
서버 컴포넌트는 어떻게 동작할까?
그 데~단하다던 스트리밍 HTML 어디한번 보자. 이번엔 서버컴포넌트가 스트리밍 HTML 되는 과정을 살펴보자
Press enter or click to view image in full size
RSC: ok 렌더 트리 접수 완료
Press enter or click to view image in full size
RSC 특: 바로 클라이언트로 돌진함
Press enter or click to view image in full size선요청 선응답 선렌더
RSC도 물론 SSR과 같은 동기적 렌더링 또한 가능하지만 Streaming을 활용하여 사용하는 것이 서버컴포넌트의 목적에 가장 부합
여기까지보면
엥? 그냥 우리가 보던 전통적인 호출 아닌가? 클라이언트에서 호출하고, 호출하는 도중에 Skeleton 보여주는.. 이게 RSC랑 무슨 상관인데? App Router, RSC 뭐 쌩 난리 부르스만 쳐놓고 별것도 아니구만, 호들갑은 ㅉㅉ.
놀랍게도 저 호출은 서버에서 이루어지고 있고, 응답이 오더라도 브라우저에서 HTTP 통신으로 호출한 것이 아니기 때문에 모든 서버 로직이나 엔드포인트 호출 기록 등이 은닉됨. 서버에서 딸랑 데이터 response가 오는 것이 아니라, 데이터가 React를 통해 이미 렌더링이 다 된 HTML 그 자체가 클라이언트로 도착하여 그대로 HTML이 브라우저에 꼽히게 됨.
아직 이해가 완벽히 안 되셨을 분들을 위해.. 한 번 더 뇌절 예시 시전.
Server Side Rendering
전체 물탱크를 채우고 한 번에 붓는 것
Press enter or click to view image in full size
서버에서 모든 데이타를 기다린뒤 브라우저에 전달한다. (느리지만 정직한놈..)
Streaming Server Side Rendering
기본적인 뼈대를 먼저 브라우저로 던지고 수도꼭지를 열어둔 뒤, 데이타가 응답이 오는대로 수도꼭지로 데이타(chunk)를 내려줌. 다 내리면 수도꼭지는 닫힘
Press enter or click to view image in full size
한번 Streaming 되어 브라우저에게 오는 html 을 분석해보자
React의 모든 노드를 읽고 지금 당장 렌더링 할수 있는 기본 뼈대를 만든다. (클라이언트 컴포넌트도 예외 없음)
그리고 data pipe 를 열어둔 상태로(수도꼭지) 브라우저에게 document response 를 던짐
Press enter or click to view image in full size
처음 서버에서 전달 받은 document response
response 를 보면 이상한 점이 보인다. html 이 완성되지 않은 상태로 내려왔다. 말도 안된다고요?
진짜 network 탭
Press enter or click to view image in full size
브라우저의 HTML 파싱에 자동 Closing 기능이 있어 일단 태그를 닫고 구조를 완성시는 기능이 내장되어있음 (위에 서술했던 Transfer-Encoding: chunked)
:손가락을_ 들어_깜짝_놀라는_붉은반팔티의_어린아이
모든 태그가 닫히지 않았다 → 오! 아직 스트리밍 상태 구나! 라고 브라우저는 인지할수 있음
그리고 모든 데이타가 Streaming 되고 모든 태그 또한 닫히면 Streaming 은 종료가 된다.
위에서 부터 HTML 코드를 보며 이상한 태그들을 봤을텐데, 애써 외면했던 것들을 이제 파헤쳐보자.
<template id=”B:0”><div id=”S:0”><div id=”P:0”>
이것들은 대체 뭘까..?
이것이 바로 맨 처음에 설명했던 Placeholder 즉 자리표시자이다.
즉, 서버에서 완전히 생성 가능한 정적(static)한 컴포넌트는 이런 요상한 것들이 붙지 않는다. Suspense로 스트리밍되어 오는 비동기 컴포넌트와 클라이언트 컴포넌트의 경우, 이런 placeholder가 붙게 된다. 즉, 서버에서 당장 확정적으로 줄 수 없는 부분들을 placeholder로 비워둔 뒤 (id로 고유의 값을 매김), 추후 스트리밍으로 도착한 데이터를 해당 id와 동일한 id로 치환해주는 것이 바로 스트리밍의 원리이다.
•
B:0, B:1, B:2 …: 특정 블록의 자리를 나타낸다. 스트리밍 되어 도착하는 데이타는 이 블록과 치환됨
•
S:0, S:1, S:2 …: 서버에서 스트리밍된 데이터. S는 B와 치환되게 됨
•
P:1, P:3, P:4 …: S로 부터 도착한 데이타 내부에 클라이언트 컴포넌트를 동적으로 로드 해야할 콘텐츠
아직도 모르겠다고요? 더 자세히 파해쳐 보겠습니다.
Press enter or click to view image in full size
사실 저도 잘 모릅니다.
실제로 어떻게 작동하는지 단계별로 살펴 보겠습니다.
예시는 최대한 간략화 시킴
Press enter or click to view image in full size
첫 HTML response 를 받은 HTML의 형태
Press enter or click to view image in full size1차 사이드 메뉴 Streaming 데이타 도착
실제로는 $RC 라는 함수 정의까지 함께 옴. $RC를 통해 B:0와 S:0 의 위치를 교체 하고 클라이언트 컴포넌트인 P:3 와 S3 의 위치까지 교체되게 됨 (예시엔 실수로 못적음 지송)
Press enter or click to view image in full size2차 사이드 메뉴 Streaming 데이타 도착
1차 Streaming 된 결과를 보면 template이나 div hidden 같은건 모두 제거 된것을 볼수 있습니다. 물론 fallback인 로딩중도 사라졌죠 (치환 함수 내부에서 바로 아래 Element fallback 도 같이 삭제시킴)
그리고 2차 스트리밍 데이타도 도착합니다. 1차때와 마찬가지입니다.
모든 Streaming 이 끝나고 최종 렌더링 된 결과
깔끔한 데이타만 남았습니다. 끗~
이런 식으로 2차 데이타 로딩까지 끝나면 </html> 으로 모든 태그가 닫히게 되고, streaming 도 종료가 됩니다. (수도꼭지 off)
진짜 쉽죠?
Press enter or click to view image in full size