Fluent React 6 - React Server Component
2024/03/06
n°50
category : React
☼
2024/03/06
n°50
category : React
Fluent React에서 서버 컴포넌트에 대해 깊이있게 다루고 있어 학습한 내용을 정리. 책과 공식문서, 예시 코드를 작성해가며 학습했고, 유익한 내용이 많았다. 사이드 프로젝트를 RSC를 사용하여 개발했는데, 이 챕터를 학습하고 이해가 더 깊어진 것 같다.
(italic체는 Fluent React의 인용입니다)
React Server Component
리액트 컴포넌트는 서버에서 실행되고 클라이언트측의 자바스크립트 번들에서는 제외되는 새로운 타입의 컴포넌트를 도입한다. RSCs introduce a new type of component that “runs” on the server and is otherwise excluded from the client-side JavaScript bundle.
리액트 서버 컴포넌트는 말그대로 서버에서만 실행되는 컴포넌트이다. 도입된 이유는 성능, 코드의 효율성, 사용자 경험을 향상시키기 위한 목적이라고 한다. 결과적으로 서버 컴포넌트를 통해 SPA의 장점과 서버 렌더링된 MPA의 장점을 동시에 취할 수 있다고 한다.
Benefits
기본적으로 훅을 사용하는 오늘날의 리액트 컴포넌트를 떠올리면 서버 컴포넌트를 '서버에서 실행되는 함수’로 이해하면 낯설 것이 없다. 하지만 이 컴포넌트가 어떻게 브라우저에서 변환되고, 이 컴포넌트를 실행하는 서버에서는 어떤 일이 일어나는 것일까?
How It works?
서버 컴포넌트는 기본적으로 리액트 엘리먼트를 반환하는 함수이다. 서버 컴포넌트가 어떻게 서버에서 브라우저에서 렌더링 가능한 컴포넌트로 변환되는지 순서대로 살펴보자.
JSX의 트리:
{
$$typeOf: Symbol("react.element"),
type: "div",
props: {
children: [
{
$$typeOf: Symbol("react.element"),
type: "h1",
props: {
children: "hi!"
}
},
{
$$typeOf: Symbol("react.element"),
type: "p",
props: {
children: "I like React!"
}
},
],
},
}
다음은 서버 컴포넌트가 클라이언트에 전달되는 간략화한 과정이다.
이 과정을 코드와 함께 살펴보면 다음과 같다. 아래 예시 코드는 서버사이드에서 서버 컴포넌트를 변환하는 과정을 모사한 코드다. (*인용된 예시는 이해를 위한 code snippet이며, 실제 구현과 차이가 있다)
// server.js
const express = require("express");
const path = require("path");
const React = require("react");
const ReactDOMServer = require("react-dom/server");
const App = require("./src/App");
app.use(express.static(path.join(__dirname, "build")));
app.get("*", async (req, res) => {
// 1 - 서버컴포넌트가 리액트 엘리먼트 트리로 변환된다
const rscTree = await turnServerComponentsIntoTreeOfElements(<App
/>);
/* turnServerComponentsIntoTreeOfElements
* 함수 내부에서 서버 컴포넌트를 리액트 앨리먼트로 변환 */
// 2 - await가 완료된 컴포넌트 트리를 HTML로 변환한다
const html = ReactDOMServer.renderToString(rscTree);
// renderToPipeableStream로 사용하는 것을 권장한다.
-> 이부분에서 rscTree의 리액트 엘리먼트가 직렬화된다.
// Send it
res.send(`
<!DOCTYPE html>
<html>
<head>
<title>My React App</title>
</head>
<body>
<div id="root">${html}</div>
<script src="/static/js/main.js"></script>
</body>
</html>
`);
});
app.listen(3000, () => {
console.log("Server listening on port 3000");
});
1번의 turnServerComponentsInto... 함수는 인자로 컴포넌트를 받고 있다. 이 함수에서 서버 컴포넌트를 전송 가능하게 변환하기 때문에 예시가 이어진다. (*마찬가지로 인용된 예시는 code snippet이다. RSC renderer를 이해하기 위한 코드이다.)
async function turnServerComponentsIntoTreeOfElements(jsx) {
if (
typeof jsx === "string" ||
typeof jsx === "number" ||
typeof jsx === "boolean" ||
jsx == null
) {
// A - Don't need to do anything special with these types.
return jsx;
}
if (Array.isArray(jsx)) {
// B - Process each item in an array.
return await Promise.all(jsx.map(renderJSXToClientJSX(child)));
}
// If we're dealing with an object
if (jsx != null && typeof jsx === "object") {
// C If the object is a React element,
if (jsx.$$typeof === Symbol.for("react.element")) {
// C-1 `{ type } is a string for built-in components.
if (typeof jsx.type === "string") {
// This is a built-in component like <div />.
// Go over its props to make sure they can be turned into JSON.
return {
...jsx,
props: await renderJSXToClientJSX(jsx.props),
};
}
if (typeof jsx.type === "function") {
// C-2 This is a custom React component (like <Footer />).
// Call its function, and repeat the procedure for the JSX it returns.
const Component = jsx.type;
const props = jsx.props;
const returnedJsx = await Component(props);
return await renderJSXToClientJSX(returnedJsx);
}
throw new Error("Not implemented.");
} else {
// D - This is an arbitrary object (props, or something inside them).
// It's an object, but not a React element (we handled that case above).
// Go over every value and process any JSX in it.
return Object.fromEntries(
await Promise.all(
Object.entries(jsx).map(async ([propName, value]) => [
propName,
await renderJSXToClientJSX(value),
])
)
);
}
}
throw new Error("Not implemented");
}
이 복잡한 함수의 역할은 매개변수로 받은 안의 모든 서버 컴포넌트를 리액트 엘리먼트 트리로 변환하는 것이다. 조금 더 단순화하자면, 내의 모든 컴포넌트를 분기처리하여 그에 맞는 리엑트 엘리먼트를 반환하는 함수이다.
주석의 A, B, C-1, C-2, D로 나누어 설명하면 다음과 같다.
A - 기본 타입 처리
B - 배열 처리
[
<div>hi</div>,
<h1>hello</h1>,
<span>love u</span>,
(props) => <p id={props.id}>lorem ipsum</p>,
];
C-1 - 리액트 엘리먼트 / 내장 컴포넌트
C-2 - 사용자 정의 컴포넌트
D - 임의 객체
앞선 과정을 통해 직렬화serialization가 가능한 리액트 트리로 서버 컴포넌트를 변환하였다. 이제 이 엘리먼트 트리를 HTML 마크업으로 변환하여 서버에 보내주기 위해 직렬화를 해야한다. 직렬화serialization란 리액트 엘리먼트를 문자열로 바꾸는 과정이다. 이는 RSC 뿐만 아니라 일반적인 SSR에서도 필요한 과정이다. 서버에서 페이지를 HTML로 전달하는 것이 SSR의 핵심이기 때문이다. 직렬화는 ReactDOMServer의 다음 메서드로 구현한다.