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의 다음 메서드로 구현한다.
이 두 메서드에 대한 내용은 다음 글에서 더 자세히 살펴보도록 하자. 그렇다면 이같은 과정을 통해 서버에서 제공함으로써 얻는 이점은 무엇일까?
사실 이는 리액트의 일반적인 SSR버사이드 렌더링의 이점과도 동일하다. 여기서 한가지 의문점이 들었다.
이러한 의문점을 해소해보자.
"차세대 번들러"라는 조금 낯선 내용을 포함하지만, 이어지는 챕터를 통해 더 명확히 이해할 수 있다.
서버 / 클라이언트 컴포넌트의 구분
앞서 서버 컴포넌트와 클라이언트의 구분을 간략히 살폈다. 서버 컴포넌트를 도입하면서 이러한 구분이 왜 필요한게 됐는지 살펴보자.
function Counter() {
const [count, setCount] = useState(0);
return (
<div>
<h1>Hello friends, look at my nice counter!</h1>
<p>About me: I like pie! Sign my guest book!</p>
<p>Count: {count}</p>
<button onClick={() => setCount(count + 1)}>+</button>
</div>
);
}
버튼을 클릭하면 count의 숫자에 1이 더해지는 컴포넌트이다. 이 컴포넌트는 다음 이유에서 서버 컴포넌트가 될 수 없고, 클라이언트 컴포넌트여야만 한다.
로컬 상태의 사용
브라우저 API 사용
여러 이유가 있지만, 근본적으로 분리할 수 밖에 없는 이유가 충분히 설명된다. 위의 내용을 요약하자면 다음과 같다.
이같은 이유를 살펴보니 어느정도 납득이 된다. 그럼 이같은 구분이 가져오는 이점에 대해서도 살펴보자.
// Server Component
function ServerCounter() {
return (
<div>
<h1>Hello friends, look at my nice counter!</h1>
<p>
About me: I like to count things and I'm a counter and sometimes I count
things but other times I enjoy playing the Cello and one time at band
camp I counted to 1000 and a pirate appeared
</p>
<InteractiveClientPart />
</div>
);
}
// Client Component
"use client";
function InteractiveClientPart() {
const [count, setCount] = useState(0);
return (
<div>
<p>Count: {count}</p>
<button onClick={() => setCount(count + 1)}>+</button>
</div>
);
}
이 예시는 서버 컴포넌트와 클라이언트를 분리한다.
... 카운터 애플리케이션()의 작은 부분을 분리해내어 인터랙티브하도록 했습니다. 앱의 이 부분만이 실제로 사용자에게 JavaScript 번들의 일부로 전달될 것입니다. 나머지 부분은 전달되지 않습니다. 그 결과로, 우리는 네트워크를 통해 훨씬 작은 JavaScript 번들을 전송하게 되며, 이는 더 빠른 로딩 시간과 사용자에게 더 나은 성능을 의미합니다. 이는 CPU 측면에서, JavaScript를 파싱하고 실행하는 데 필요한 작업이 줄어들며, 네트워크 측면에서, 다운로드해야 하는 데이터가 줄어드는 측면 모두에서 해당됩니다. 이같은 이점에서 클라이언트 사이드 번들에서 코드를 제외하기 위해 서버에서 안전하게 렌더링할 수 있는 만큼 많이 렌더링하고자 하는 이유입니다.
서버/클라이언트 컴포넌트의 분리 장점을 요약하면 다음과 같다.
서버 컴포넌트의 간략한 작동 원리, 도입 배경과 함께 살펴보니 더 납득이 간다. 이제 본격적으로 서버 컴포넌트를 사용하기 위한 방법과 지켜야할 규칙을 간략히 살펴보자.
하지만 다음 예제에서 ServerCounter 컴포넌트는 이미 클라이언트 컴포넌트를 포함하고 있다. InteractiveClientPart 클라이언트 컴포넌트가 서버사이드에서 이뤄지는 과정은 길지만 흥미롭다.
TLDR;
...React는 언제 클라이언트 컴포넌트를 가져오고 실행해야 하는지 어떻게 알까요? 이를 이해하기 위해, 전형적인 React 트리를 고려해야 합니다. 우리의 카운터 예제를 사용하여 이를 해보겠습니다.
오렌지 컴포넌트는 서버에서 렌더링되고, 초록색 컴포넌트는 클라이언트에서 렌더링됩니다. 트리의 루트가 서버 컴포넌트인 경우, 전체 트리는 서버에서 렌더링됩니다. 그러나 InteractiveClientPart 컴포넌트는 클라이언트 컴포넌트이므로 서버에서 렌더링되지 않습니다. 대신, 서버는 클라이언트 컴포넌트를 위한 플레이스홀더를 렌더링하는데, **이는 클라이언트 번들러가 생성한 특정 모듈에 대한 참조입니다. 이 모듈 참조는 본질적으로 “트리의 이 지점에 도달하면, 이 특정 모듈을 사용할 시간이다”라고 말합니다.
RSC와 클라이언트 컴포넌트 그림 클라이언트와 서버 컴포넌트를 보여주는 컴포넌트 트리 모듈은 반드시 항상 지연 로딩되는 것만은 아니지만, 번들러가 사용자에게 전달하는 번들에 많은 모듈을 추가하기 때문에, 초기 번들에서도 로드될 수 있습니다. 실제로는 getModuleFromBundleAtPosition([0,4]) 또는 비슷한 것일 수 있습니다. 포인트는 서버가 올바른 클라이언트 모듈에 대한 참조를 보내고, 클라이언트 측 React가 이 공백을 채운다는 것입니다.
이 일이 발생하면, React는 모듈 참조를 실제 클라이언트 번들의 모듈로 대체합니다. 이것은 약간의 단순화이지만, 메커니즘을 충분히 이해하는 데 도움이 될 것입니다. 그런 다음 클라이언트 컴포넌트는 클라이언트에서 렌더링되며, 클라이언트 컴포넌트는 평소처럼 상호 작용할 수 있습니다. 이것이 RSC가 차세대 번들러를 요구하는 이유입니다: 서버와 클라이언트 컴포넌트를 위한 별도의 모듈 그래프를 생성할 수 있어야 합니다.
Serializability Is King 직렬화 가능성이 킹이다
서버 컴포넌트에서는 모든 props가 직렬화 가능해야 한다. 앞서 dispatch, 이벤트 핸들러가 서버 컴포넌트에 작성될 수 없는 이유와 동일하다. 서버에서 props를 직렬화하여 클라이언트에게 전송헤야하기 때문이다. 서버 컴포넌트에서 props는 함수나 다른 비직렬화 가능한 값일 수 없다.
// error!
function ServerComponent() {
return <ClientComponent onClick={() => alert("hi")} />;
}
이 컴포넌트는 오류를 발생시킨다. 이제 onClick prop을 ClientComponent 내부에 캡슐화하여 서버와 클라이언트 컴포넌트를 분리해야 한다.
+추가 내용
RSC에서 사용할 수 없는 컴포넌트 패턴들
*이 규칙은 5장에서 논의한 렌더 props 패턴을 사실상 구식으로 만듭니다.*
<ParentComponent>
{props => <ChildComponent {...props} />}
</ParentComponent>
서버에서 클라이언트로 컴포넌트의 props를 전송하기 위해 모든 props가 직렬화 가능해야 하기 때문에 render props 패턴은 RSC에서 사용이 불가능하다. 함수는 직렬화할 수 없는 객체이고, render props 패턴에서는 함수를 prop으로 전달하기 때문이다.
물론 클라이언트 컴포넌트 사이에선 여전히 사용 가능할 것이다. 개발하려는 앱의 구조에 따라 서버, 클라이언트 컴포넌트의 구성에 따라 더 적절한 패턴이 있을 것이기에, 단정짓기는 어렵다. 다만 앞으로 RSC가 훅이 처음 도입되어 오늘날 자리잡은 것만큼 일반적으로 사용된다면, 여러 컴포넌트 설계 패턴에서도 변화가 있을 것이 예상된다.
부수효과를 일으키는 훅 금지 No Effectful Hooks
앞서 살펴본 것처럼 컴포넌트가 실행되는 서버와 클라이언트는 환경이 다르다. 서버에는 window 객체, DOM, 그에 기반한 이벤트와 사용자의 인터렉션이 없다. 따라서 이를 처리하는 훅(useState, useEffect) 또한 서버 컴포넌트에서 사용할 수 없다.
Next.js와 같은 일부 프레임워크는 서버 컴포넌트에서 모든 hooks의 사용을 완전히 금지하는 린트 규칙을 가지고 있지만, 이것이 완전히 필요한 것은 아닙니다. RSC는 상태useState, 효과useEffect 또는 브라우저 전용 API에 의존하지 않는 hooks를 사용할 수 있습니다. 예를 들어, useRef hook는 상태, 효과 또는 브라우저 전용 API에 의존하지 않기 때문에 서버 컴포넌트에서 사용할 수 있습니다. 그러나 이러한 제한이 모두 나쁜 것은 아니며, 이는 우리가 컴포넌트를 더 안전하게 다루게끔 유도합니다.
useRef는 서버 컴포넌트에서 사용할 수 있다고는 한다! 몰랐지만.. 사용할 일이 있을까 싶다.
(서버) 상태가 (클라이언트)상태는 아니다 State is Not State
앞서 살펴봤듯이 서버 컴포넌트의 상태는 클라이언트 컴포넌트의 상태와 동일하지 않다. 그 이유 또한 살펴봤다. 각 컴포넌트가 렌더링되는 환경이 다르기 때문이다. 또한 서버 상의 상태가 여러 클라이언트에 공유될 수 있기 때문에, 보안 상의 이유로도 두 컴포넌트는 분리되야 한다고 했다.
서버 컴포넌트는 서버에서 렌더링되고, 클라이언트 컴포넌트는 클라이언트(브라우저)에서 렌더링됩니다. 이는 서버 컴포넌트의 상태가 여러 클라이언트에 공유될 수 있음을 뜻합니다. 서버와 클라이언트의 관계는 유니캐스트(하나의 클라이언트, 하나의 상태) 대신 브로드캐스트 관계이기 때문에, 클라이언트 간에 상태가 유출될 위험이 높습니다.
여러 클라이언트가 결국 동일한 리소스로 같은 서버 컴포넌트를 요청하기 때문에 상태가 공유될 위험성이 있다는 말이다. 따라서 상태와 관련된 useState 또는 useReducer 또는 상태state가 필요한 모든 컴포넌트는 클라이언트 컴포넌트가 적합하다.
클라이언트 컴포넌트는 서버 컴포넌트를 import 할 수 없다.
// error!
"use client";
import { ServerComponent } from "./ServerComponent";
function ClientComponent() {
return (
<div>
<h1>Hey everyone, check out my great server component!</h1>
<ServerComponent />
</div>
);
}
import { readFile } from "node:fs/promises";
export async function ServerComponent() {
const content = await readFile("./some-file.txt", "utf-8");
return <div>{content}</div>;
}
- 서버 컴포넌트는 Node.js API `"node:fs/promises"` 모듈에서 `readFile` 함수를 import한다.
- 클라이언트 컴포넌트는 서버 컴포넌트를 import하여 브라우저에서 사용할 수 없는 서버 컴포넌트의 import가 불가능하다. 서버에서 브라우저 API를 사용할 수 없는 것과 마찬가지다.
- Node.js API 사용만이 아니더라도, 서버에서 먼저 실행되는 서버 컴포넌트를 클라이언트 컴포넌트에서 import 하는 것은 서버/클라 분리의 원칙상으로도 어긋난다. (서버 컴포넌트를 클라이언트 번들에 포함하려는 것이기 때문에)
Workaround
"use client";
function ClientComponent({ children }) {
return (
<div>
<h1>Hey everyone, check out my great server component!</h1>
{children}
</div>
);
}
import { ServerComponent } from "./ServerComponent";
async function TheParentOfBothComponents() {
return (
<ClientComponent>
<ServerComponent />
</ClientComponent>
);
}
이는 클라이언트 컴포넌트에서 서버 컴포넌트를 명시적으로 import하지 않기 때문에 작동한다.
- 부모 서버 컴포넌트가 서버 컴포넌트를 클라이언트 컴포넌트에 props.children으로 전달.
- (클라이언트) 번들러는 import 문에만 주의를 기울이고 prop 구성에는 주의를 기울이지 않기 때문에 가능하다
클라이언트 컴포넌트 나쁘지만은 않다
서버 컴포넌트가 도입되기 전까지, 클라이언트 컴포넌트는 우리가 React에서 가지고 있던 유일한 유형의 컴포넌트였습니다. 이는 우리의 기존 컴포넌트들이 모두 클라이언트 컴포넌트라는 것을 의미하며, 그것은 괜찮습니다. 클라이언트 컴포넌트는 나쁘지 않으며 사라지지 않을 것입니다. 그들은 여전히 React 애플리케이션의 핵심이며, 우리가 작성할 가장 일반적인 유형의 컴포넌트입니다. 이 주제에 대해 어느 정도 혼란이 있었고, 서버 컴포넌트가 클라이언트 컴포넌트의 우월한 대체물로 일부에 의해 인식되었기 때문에 여기서 이를 언급합니다. 이것은 사실이 아닙니다. 서버 컴포넌트는 클라이언트 컴포넌트에 추가하여 사용할 수 있는 새로운 유형의 컴포넌트이지만, 클라이언트 컴포넌트의 대체물은 아닙니다.
이번 챕터에서 서버 컴포넌트 만을 깊게 다루었지만, 사실 아직 과도기의 느낌도 받는다. 뒤에 이어지는 서버 액션의 경우 이를 구현하는 새로운 훅 api가 (비공식적으로) 공개되었고, 리액트 컴파일러, use 훅 등등.. 변경 사항은 많아 보인다. 하지만 리액트의 강력한 점은 이전 버전의 코드와 호환이 가능한게 아닌가. 안정화가 될 때까지는 여전히 SPA, SSR 등을 사용하고 있을 것으로 예상된다.
서버 액션과 use server는 아직 Canary이다. (관련한 PR도 계속 추가되고 있고, 많은 예측과 논의가 오가고 있다.) 아직은 실험적인 내용이기에, 먼저 기본적인 개념을 간략히 이해하는데 집중해보자.
RSC는 클라이언트 측 코드에서 호출할 수 있는 서버 측 함수를 표시하는 새로운 지시어 "use server"와 함께 작동합니다. 우리는 이러한 함수들을 서버 액션이라고 부릅니다. ...어떤 비동기 함수든 그 몸체의 첫 줄에 "use server"를 가질 수 있으며, 이는 React와 번들러에게 이 함수가 클라이언트 측 코드에서 호출될 수 있지만 서버에서만 실행되어야 한다는 것을 알립니다. 클라이언트에서 서버 액션을 호출할 때, 전달된 모든 인수의 직렬화된 복사본을 포함하는 네트워크 요청을 서버로 만듭니다. 서버 액션이 값을 반환하면, 그 값은 직렬화되어 클라이언트에 반환됩니다.
"use server"와 함께 사용된 함수를 Server Action이라고 한다.
"use server"
export const getSampleData = async () => ...
export const postFormSampleData = async () => ...
먼저 "use server" 디렉티브는 서버 컴포넌트를 만들기 위한 명령어가 아니다(Next.js의 "use client"의 반대가 아니다). "use server"는 함수에 적용하기 위해 사용한다. 이 함수는 클라이언트 컴포넌트에서 호출한다. 그러므로 Next.js라면, 한 컴포넌트 최상단에 "use client", 함수 안에는 "use server" 두 명령어가 모두 있을 수 있다.
목적 : 함수를 "서버에서만" 실행되게끔 한다. 클라이언트는 호출 -> (서버) -> 반환만 받는다.
클라이언트: 클라이언트에서 서버 액션 호출 (=서버로 요청) ->
서버: (서버에서 함수가 실행 -> 서버에서 결과를 반환 ) ->
클라이언트: 클라이언트 컴포넌트에서 응답을 받음 -> 함수가 반환한 값에만 접근할 수 있음.
앞서 살핀 것처럼 컴포넌트를 서버로 옮긴 것의 이점을, 클라이언트의 함수에 적용한다고 이해하면 되겠다.
// App.js
async function requestUsername(formData) {
'use server';
const username = formData.get('username');
const address = formData.get('address');
console.log(username)
// ...
return response
}
export default App() {
<form action={requestUsername}>
<input type="text" name="username" />
<input type="text" name="address" />
<button type="submit">Request</button>
</form>
}`
서버액션을 사용한 Form 컴포넌트의 예시이다.
클라이언트에서 기존대로 폼 데이터를 제출하는 것과는 다른 패턴이라 낯설다. form 제출에 서버 액션을 사용하여 다음과 같은 이점을 볼 수 있다고 한다.
앞으로도 변경사항이 있을 수 있는 내용인 것 같다. 기본적인 개념을 인지하고 넘어가자.
// LikeButton.jsx
"use client";
import incrementLike from "./actions";
import { useState, useTransition } from "react";
function LikeButton() {
const [isPending, startTransition] = useTransition();
const [likeCount, setLikeCount] = useState(0);
const incrementLink = async () => {
"use server";
return likeCount + 1;
};
const onClick = () => {
startTransition(async () => {
// 서버 액션 반환 값을 읽으려면, 반환된 프로미스를 await 해야함
const currentCount = await incrementLike(); // 서버 액션 호출
setLikeCount(currentCount);
});
};
return (
<>
<p>Total Likes: {likeCount}</p>
<button onClick={onClick} disabled={isPending}>
Like
</button>;
</>
);
}
// actions/incrementLike.js
// db를 호출하지 않는 간단한 예시.
let likeCount = 0;
export default async function incrementLike() {
likeCount++;
return likeCount;
}
// 데이터베이스로 직접 요청을 보낼 수도 있다
const pool = new Pool({ user: 'dbuser', host: 'database.server.com', database: 'mydb', password: 'secretpassword', port: 5432, });
async function incrementLike(postId) {
'use server'; // 서버 액션 지시어
try {
// 백엔드 api로 요청을 보낸다고 가정 시
const response = await fetch(`... `, {
method: 'POST', ...
});
// 데이터 베이스에 직접 접근을 가정할 시 (db는 PostgreSQL)
const client = await pool.connect();
const { rows } = await client.query('SELECT likes FROM posts WHERE id =
...
const { likeCount } = await response.json();
return likeCount; // 증가된 좋아요 수 반환
} catch (error) {
console.error('Error incrementing like:', error);
...
}
}
이 예시를 통해, form 태그 뿐만 아니라 다양한 클라이언트 컴포넌트의 이벤트에서도 사용가능하다.
리액트 공식문서에는 가장 간단한 예시를 제공하지만, 이론적으로 서버 액션으로 SQL을 작성하여 직접 db에 호출하는 것까지 가능하다. (얼마나 실용적인지는 모르겠다) 이 때문에 서버 액션은 처음 공개되었을 시 많은 반발을 샀던 걸로 기억한다. 그럼에도 가장 큰 변화 중 하나가 아닐까 싶다.
서버 액션을 통해 비동기 데이터를 관리하는 클라이언트 컴포넌트가 더 직관적이고 단순해졌다. 서버 액션으로 백엔드 api를 호출하는 방식이 BFF와 생각도 들기도 하여 이점이 없을 것 같진 않다. 하지만 아직은 예측만 해볼 뿐이다.
올해 말에 리액트 19가 나온다고 하니, 귀추가 주목된다!
관련한 흥미로운 글들
- 라우팅 기반으로 상태 관리가 바뀔 것이다
- 라우팅 기반의 상태 관리, 캐싱