React Server Components (RSCs) 개념
MPAs
)과 클라이언트 렌더링된 SPA
의 장점을 결합하여 성능, 효율성, 유지보수성을 향상시킵니다.RSCs의 동작 원리
RSCs의 장점
RSCs와 서버 렌더링의 상호작용
RSC 렌더링 로직
<div>
, <span>
): 문자열로 변환.<Footer />
): 컴포넌트 함수 실행 후 반환값 재귀 처리.데이터 흐름 도식화
1. 서버에서 JSX를 React 요소 트리로 변환
JSX:
<App>
<div>
<h1>hi!</h1>
<p>I like React!</p>
</div>
</App>
➡️ React 요소 트리:
{
type: "div",
props: {
children: [
{ type: "h1", props: { children: "hi!" } },
{ type: "p", props: { children: "I like React!" } }
]
}
}
2. React 요소 트리를 JSON으로 직렬화
➡️ 직렬화된 JSON:
{
"type": "div",
"props": {
"children": [
{ "type": "h1", "props": { "children": "hi!" } },
{ "type": "p", "props": { "children": "I like React!" } }
]
}
}
3. 클라이언트에서 JSON 데이터를 React 요소로 복원
➡️ 복원된 React 요소:
{
type: "div",
props: {
children: [
{ type: "h1", props: { children: "hi!" } },
{ type: "p", props: { children: "I like React!" } }
]
}
}
4. 클라이언트에서 복원된 React 요소를 렌더링
➡️ 화면 출력:
<div>
<h1>hi!</h1>
<p>I like React!</p>
</div>
React Server Components의 본질
데이터 흐름
장점 요약
RSC와 서버 렌더링의 차이점
서버 측 코드: Express 기반
import express from "express";
import path from "path";
import React from "react";
import ReactDOMServer from "react-dom/server";
// React 컴포넌트 타입 정의
type ComponentProps = {
title: string;
content: string;
};
// 간단한 React 서버 컴포넌트
const ServerComponent: React.FC<ComponentProps> = ({ title, content }) => {
return (
<div>
<h1>{title}</h1>
<p>{content}</p>
</div>
);
};
// React 요소를 JSON으로 직렬화
const serializeReactElement = (element: React.ReactElement): string => {
return JSON.stringify(element, null, 2);
};
// 서버 설정
const app = express();
const PORT = 3000;
// 정적 파일 경로 설정
app.use(express.static(path.join(__dirname, "build")));
// 모든 요청 처리
app.get("*", (req, res) => {
// React 요소 생성
const reactElement = <ServerComponent title="Hello RSC" content="This is a Server Component!" />;
// React 요소를 JSON으로 직렬화
const serializedJSON = serializeReactElement(reactElement);
// HTML로 감싸서 전송
res.send(`
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>React Server Components</title>
</head>
<body>
<div id="root"></div>
<script>
// 서버에서 받은 JSON 데이터
const serverData = ${serializedJSON};
// 클라이언트에서 React 복원 및 렌더링 (ReactDOM을 통한 가정)
console.log("Server Data:", serverData);
</script>
</body>
</html>
`);
});
// 서버 시작
app.listen(PORT, () => {
console.log(`Server is running on http://localhost:${PORT}`);
});
클라이언트 측 코드 (TypeScript 포함)
import React from "react";
import ReactDOM from "react-dom/client";
// 서버에서 전달받은 JSON 데이터
declare const serverData: string;
const App = () => {
const data = JSON.parse(serverData);
// React 요소 복원
const ReactElement = React.createElement(data.type, data.props);
return <>{ReactElement}</>;
};
// 클라이언트 측 렌더링
const rootElement = document.getElementById("root");
if (rootElement) {
const root = ReactDOM.createRoot(rootElement);
root.render(<App />);
}
주요 동작
결과
Serialization
)는 React 요소(React element)를 HTML 문자열로 변환하는 과정입니다.ReactDOMServer.renderToString
을 사용하여 이루어집니다.const element = <h1>Hello, world</h1>;
const htmlString = ReactDOMServer.renderToString(element);
// 결과: '<h1>Hello, world</h1>'
이 HTML 문자열은 클라이언트에서 초기 렌더링에 사용되며, 이후 React가 이를 "hydrate(재수화)"하여 DOM과 이벤트 핸들러를 연결합니다.
직렬화의 중요성
React 요소의 JSON 직렬화
$$typeof
라는 특수한 속성을 가집니다.$$typeof
속성은 Symbol
로 되어 있어 기본적인 JSON.stringify
로는 직렬화할 수 없습니다.JSON.stringify
와 JSON.parse
에 replacer
함수를 사용하여 직렬화/역직렬화합니다.직렬화 (서버 측)
JSON.stringify(jsxTree, (key, value) => {
if (key === "$$typeof") {
return "react.element"; // Symbol 대신 문자열로 대체
}
return value;
});
역직렬화 (클라이언트 측)
JSON.parse(serializedJsxTree, (key, value) => {
if (key === "$$typeof") {
return Symbol.for("react.element"); // 문자열을 다시 Symbol로 변환
}
return value;
});
데이터 흐름 정리
데이터 흐름 도식화
1. 서버에서 React 요소를 생성
➡️ React 요소 트리 생성
{
$$typeof: Symbol("react.element"),
type: "h1",
props: { children: "Hello, world" }
}
2. JSON.stringify로 직렬화 (replacer 사용)
➡️ JSON 데이터로 변환
{
"$$typeof": "react.element",
"type": "h1",
"props": { "children": "Hello, world" }
}
3. 클라이언트에서 JSON 데이터 수신
➡️ JSON.parse로 역직렬화 (replacer 사용)
{
$$typeof: Symbol("react.element"),
type: "h1",
props: { children: "Hello, world" }
}
4. React 요소를 DOM으로 렌더링
➡️ <h1>Hello, world</h1>
React 요소 직렬화의 필요성
서버 측 직렬화 예제
const serializedData = JSON.stringify(jsxTree, (key, value) => {
if (key === "$$typeof") {
return "react.element";
}
return value;
});
클라이언트 측 역직렬화 예제
const reactElement = JSON.parse(serializedData, (key, value) => {
if (key === "$$typeof") {
return Symbol.for("react.element");
}
return value;
});
서버사이드 렌더링(SSR)
React Server Components(RSCs)
차이점 비교
특징 | 서버사이드 렌더링(SSR) | React Server Components(RSCs) |
---|---|---|
서버 작업 | HTML 생성 (ReactDOMServer.renderToString) | React 요소 트리 생성 및 직렬화 |
클라이언트 작업 | HTML 재수화 (이벤트 연결, 동적 렌더링) | JSON 데이터를 React 요소로 복원 후 렌더링(재수화 - 정확히말하면 재수화는 아니고 그냥 랜더링임) |
전송 데이터 | 완전한 HTML 문자열 | 직렬화된 React 요소(JSON 형태) |
주요 이점 | 초기 로딩 속도 개선 (완전한 HTML 제공) | 데이터 처리 비용 감소 (React 컴포넌트 실행은 서버에서만 수행) |
적합한 상황 | 모든 HTML을 클라이언트에 즉시 보여줘야 하는 경우 | 데이터 중심 렌더링에서 성능 최적화가 중요한 경우 |
RSCs의 이점
정리
RSC는 JSON 형태로 직렬화된 React 요소 트리를 클라이언트로 전달하고, 클라이언트는 이를 사용해 렌더링합니다.
RSC의 이점은 클라이언트에서 React 컴포넌트 로직을 실행할 필요 없이 이미 계산된 데이터를 받아 HTML을 생성하므로 성능이 더 최적화된다는 점입니다.
결론적으로, RSC는 클라이언트와 서버 간 작업을 분리하여 성능을 최적화하고, 특히 데이터 중심의 애플리케이션에서 유리합니다.
RSC와 재수화(hydration)의 차이
SSR과 재수화(hydration)
재수화
)를 통해 해당 DOM에 이벤트 핸들러와 동적 콘텐츠를 연결합니다.비교: RSC와 SSR의 클라이언트 처리
특징 | React Server Components (RSCs) | 서버사이드 렌더링 (SSR) |
---|---|---|
초기 클라이언트 작업 | JSON 데이터를 기반으로 React 요소를 복원하여 DOM 생성. | 서버에서 전송된 HTML을 사용해 DOM 생성 및 재수화. |
hydrate 과정 | 필요 없음 (JSON 데이터를 새로 렌더링). | 기존 HTML과 React 가상 DOM을 동기화. |
결과물 | 클라이언트에서 새롭게 HTML을 생성. | 서버가 생성한 HTML을 그대로 사용. |
RSC에서 hydrate가 필요 없는 이유
RSC가 hydrate를 대체하는 방식
RSC는 hydrate 과정을 거치지 않습니다.
이 두 방식의 차이로 인해 RSC는 더 간단한 구조로 동작하며, 클라이언트 부하를 줄일 수 있습니다.
기존 HTML 링크(<a href="/blog">
)를 클릭하면 전체 페이지 새로고침(Full-Page Navigation
)이 발생합니다.
이는 사용자 경험을 저하시킬 수 있으므로, RSCs에서는 Soft Navigation
을 구현합니다.
Soft Navigation이란?
구현 방법
<a>
태그일 경우 기본 동작(페이지 새로고침)을 막고, 대신 Soft Navigation 처리.주요 코드 설명
클라이언트: 이벤트 위임 및 navigate 함수
window.addEventListener("click", (event) => {
if (event.target.tagName !== "A") {
return;
}
event.preventDefault(); // 기본 링크 동작 막기
navigate(event.target.href); // Soft Navigation
});
async function navigate(url) {
const response = await fetch(url, { headers: { "jsx-only": true } });
const jsxTree = await response.json();
const element = JSON.parse(jsxTree, (key, value) => {
if (key === "$$typeof") {
return Symbol.for("react.element");
}
return value;
});
root.render(element); // 새 페이지 렌더링
}
서버: jsx-only 헤더 처리
app.get("*", async (req, res) => {
const jsxTree = await turnServerComponentsIntoTreeOfElements(<App />);
if (req.headers["jsx-only"]) {
// JSX 트리만 직렬화하여 반환
res.end(
JSON.stringify(jsxTree, (key, value) => {
if (key === "$$typeof") {
return "react.element";
}
return value;
})
);
} else {
// 일반 HTML 반환
const html = ReactDOMServer.renderToString(jsxTree);
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>
`);
}
});
Soft Navigation의 장점
데이터 흐름 도식화
1. 사용자가 <a> 태그 클릭
➡️ window 이벤트 리스너 감지
➡️ 기본 동작 취소 및 navigate 함수 호출
2. navigate 함수
➡️ 서버에 URL 요청 (Header: { "jsx-only": true })
➡️ 서버가 JSX 트리를 JSON 형태로 응답
➡️ 클라이언트가 JSON을 React 요소로 역직렬화
➡️ root.render로 새 페이지 렌더링
3. 서버
➡️ 요청 URL에 맞는 JSX 트리 생성
➡️ 직렬화(JSON.stringify) 후 응답
React Server Components에서 Soft Navigation 구현
<a>
태그 클릭 시 서버로부터 JSX 데이터(JSON 형태
)만 받아와서 페이지를 렌더링합니다.<a>
태그를 클릭하면, 기본 동작(페이지 새로고침)을 막고, 새로운 페이지 데이터를 요청합니다.Soft Navigation의 핵심
RSCs는 성능 최적화를 제공하지만, 모든 React 컴포넌트가 서버 컴포넌트로 동작할 수는 없습니다. 아래 이유를 중심으로 RSCs의 한계를 살펴봅니다:
상태(State) 관련 제한
이벤트 핸들러의 제한
serializable
)해야 하므로, 함수 형태의 이벤트 핸들러를 서버 컴포넌트의 props로 전달할 수 없습니다.해결책: 서버와 클라이언트 컴포넌트 분리
// 서버 컴포넌트
function ServerCounter() {
return (
<div>
<h1>Hello friends, look at my nice counter!</h1>
<p>About me: I like to count things!</p>
<InteractiveClientPart />
</div>
);
}
// 클라이언트 컴포넌트
"use client";
function InteractiveClientPart() {
const [count, setCount] = useState(0);
return (
<div>
<p>Count: {count}</p>
<button onClick={() => setCount(count + 1)}>+</button>
</div>
);
}
RSCs의 성능 이점
데이터 흐름 도식화
1. 사용자가 페이지 요청
➡️ 서버에서 ServerCounter 컴포넌트 렌더링
➡️ HTML과 클라이언트 컴포넌트의 초기 상태 데이터 전송
2. 클라이언트에서 InteractiveClientPart 컴포넌트 렌더링
➡️ useState로 상태 관리 및 onClick 이벤트 핸들러 연결
➡️ 화면에 인터랙티브 UI 렌더링
정리
RSC를 도입하면 컴포넌트 분리 작업이 필요해지지만, 이를 통해 성능과 사용자 경험을 크게 개선할 수 있습니다.
함수는 직렬화가 불가능합니다. 이는 자바스크립트의 기본적인 동작 방식 및 JSON 표준에 따른 것입니다. 아래에서 이유와 직렬화와 관련된 제한 사항을 자세히 설명하겠습니다.
직렬화란?
Serialization
)는 데이터 구조나 객체를 네트워크로 전송하거나 파일로 저장할 수 있도록 문자열로 변환하는 과정입니다. JSON.stringify
와 JSON.parse
를 사용하여 객체를 직렬화하고 복원합니다.함수가 직렬화되지 않는 이유
JSON 표준의 제한:
JSON은 문자열, 숫자, 불리언, 객체, 배열과 같은 데이터 유형을 표현할 수 있지만, 함수는 표현하지 않습니다.
함수는 실행 코드와 관련된 상태(클로저
)를 포함하고 있는데, 이 상태를 문자열로 변환할 방법이 표준 JSON에는 없습니다.
const obj = {
name: "John",
greet: function () {
return "Hello " + this.name;
},
};
console.log(JSON.stringify(obj));
// 결과: {"name":"John"} (greet 함수는 포함되지 않음)
함수는 실행 환경에 의존:
예제: 함수 직렬화 실패
JSON.stringify의 동작
const obj = {
num: 42,
func: () => console.log("Hello!"),
};
console.log(JSON.stringify(obj));
// 결과: {"num":42} (func은 제외됨)
직렬화된 데이터 전송 후 복원
const serialized = '{"num":42}'; // JSON으로 직렬화된 데이터
const deserialized = JSON.parse(serialized);
console.log(deserialized.num); // 42
console.log(deserialized.func); // undefined
함수 직렬화가 필요한 경우의 대안
함수 코드를 문자열로 저장:
함수를 문자열로 변환해 저장하거나 전송한 뒤, 클라이언트에서 eval이나 new Function을 사용해 다시 함수로 변환할 수 있습니다.
그러나 이는 보안상 위험하며, 일반적으로 권장되지 않습니다.
const obj = {
func: "console.log('Hello!')",
};
const deserializedFunc = new Function(obj.func);
deserializedFunc(); // "Hello!"
직렬화 가능한 데이터로 대체:
함수 대신 데이터를 기반으로 동작을 정의.
예: 함수 대신 콜백 이름을 문자열로 저장하고, 클라이언트에서 해당 이름에 맞는 함수를 실행.
const obj = {
callback: "sayHello",
};
const callbacks = {
sayHello: () => console.log("Hello!"),
};
callbacks[obj.callback](); // "Hello!"
결론
RSC에서 함수가 직렬화되지 않는 이유도 이와 같습니다. 서버는 함수(onClick, setState)를 클라이언트로 전달할 수 없으므로, 클라이언트 전용 컴포넌트로 분리해야 합니다.
서버 컴포넌트와 클라이언트 컴포넌트의 구분
RSC의 렌더링 과정
모듈 참조 처리
module reference
)를 통해 다음을 포함:서버 컴포넌트의 Suspense 처리
이점
1. 서버에서 React 트리 생성
➡️ ServerCounter 컴포넌트 렌더링
➡️ 클라이언트 컴포넌트(ClientPart)에 모듈 참조를 포함한 플레이스홀더 생성
➡️ React 요소 트리를 JSON 형태로 직렬화 후 클라이언트 전송
2. 클라이언트에서 JSON 데이터 복원
➡️ React 트리 재구성
➡️ 모듈 참조를 기반으로 번들에서 클라이언트 컴포넌트를 가져와 렌더링
➡️ 페이지 완성
React 서버 컴포넌트 내부 동작
이점
RSC를 활용하면 필요한 부분만 클라이언트에 부담을 주며, 나머지는 서버에서 효율적으로 처리할 수 있습니다.
오해:
컴포넌트의 실행이란?
function MyComponent() {
return <div>hello world</div>;
}
// MyComponent 실행 결과:
{
$$typeof: Symbol(react.element),
type: "div",
props: {
children: "hello world"
}
}
실행 경계의 정확한 이해
핵심 흐름 요약
1. 서버에서 컴포넌트 실행
➡️ 서버 컴포넌트 실행: React 요소 반환
➡️ 클라이언트 컴포넌트 실행: React 요소 반환
➡️ 전체 React 요소 트리 생성
2. 서버에서 HTML로 직렬화
➡️ React 요소 트리를 HTML 문자열로 변환
➡️ HTML 문자열을 클라이언트로 전송
3. 클라이언트에서 렌더링
➡️ HTML을 DOM으로 렌더링
➡️ 클라이언트 컴포넌트만 실행 (상호작용 처리)
이로 인해 서버와 클라이언트의 역할이 명확히 분리되며, 성능과 상호작용 효율성을 최적화할 수 있습니다.
이제 서버 컴포넌트가 내부적으로 어떻게 작동하는지 이해했으므로 서버 컴포넌트로 작업할 때 따라야 할 몇 가지 규칙, 또는 더 광범위하게 서버 컴포넌트로 작업할 때 염두에 두어야 할 사항에 대해 논의해 보겠습니다.
직렬화 가능한 props만 허용:
render props 패턴의 비호환성:
예제: 직렬화 불가능한 props로 인한 문제
function ServerComponent() {
return <ClientComponent onClick={() => alert("hi")} />;
}
// ❌ 에러 발생: onClick은 함수이므로 직렬화 불가능
해결 방법:
직렬화가 필요한 props는 서버 컴포넌트에서 처리하고, 상호작용 관련 로직은 클라이언트 컴포넌트로 분리합니다.
function ServerComponent() {
return <ClientComponent />;
}
"use client";
function ClientComponent() {
return <button onClick={() => alert("hi")}>Click me</button>;
}
1. 서버에서 React 트리 생성
➡️ ServerComponent 실행
➡️ ClientComponent를 포함한 React 요소 트리 생성 (onClick 같은 함수 props는 포함 불가)
2. 서버에서 JSON으로 직렬화
➡️ React 요소 트리를 직렬화 가능한 JSON으로 변환
➡️ JSON 데이터 전송
3. 클라이언트에서 렌더링
➡️ JSON 데이터를 기반으로 React 트리 복원
➡️ ClientComponent 실행 (onClick 같은 동작 처리 포함)
직렬화 가능한 props만 허용
render props 패턴의 비호환성
해결 방법
주요 이점
이로 인해 RSCs의 동작 원리가 더 명확해지며, 개발자는 직렬화 가능한 데이터 구조로 설계해야 하는 추가적인 이해가 필요합니다.
서버 환경의 특성
효과적인(Effecful) Hook 금지
허용되는 Hook
프레임워크 규칙
1. 서버 컴포넌트에서 Hook 호출
➡️ 상태와 효과 기반 Hook(`useState`, `useEffect`) 호출 시 에러 발생
➡️ 서버에서 상태와 DOM 처리가 불가능하기 때문
2. 허용 가능한 Hook
➡️ 서버 컴포넌트에서는 DOM 및 상태 의존성이 없는 Hook(`useRef`)만 호출 가능
➡️ 정적 데이터를 처리하고 React 요소를 생성
결론: 서버 컴포넌트는 정적 렌더링을 처리하며, 상태나 효과를 기반으로 하는 Hook은 금지되어야 합니다. 대신, DOM과 상태에 의존하지 않는 useRef와 같은 Hook은 안전하게 사용할 수 있습니다.
서버 컴포넌트에서의 상태:
클라이언트 컴포넌트에서의 상태:
RSC의 상태와 Hook 규칙
주요 이유
1. 서버에서 상태 관리
➡️ 상태가 여러 클라이언트 간에 공유될 위험 존재
➡️ 민감한 데이터 유출 가능성
2. 상태 관리 분리
➡️ 서버 컴포넌트는 정적 데이터를 렌더링
➡️ 클라이언트 컴포넌트는 상태(`useState`, `useReducer`)를 관리
상태 관리 분리 예시
서버 컴포넌트는 정적인 데이터를 처리.
클라이언트 컴포넌트에서 상태 관리와 동적 동작 처리.
// ServerComponent.tsx
function ServerComponent() {
return (
<div>
<h1>Hello, world!</h1>
<ClientCounter />
</div>
);
}
export default ServerComponent;
// ClientCounter.tsx
"use client";
import React, { useState } from "react";
function ClientCounter() {
const [count, setCount] = useState(0);
return (
<div>
<p>Count: {count}</p>
<button onClick={() => setCount(count + 1)}>Increase</button>
</div>
);
}
export default ClientCounter;
요약
이러한 구조는 React Server Components의 성능과 안정성을 유지하는 데 필수적입니다.
클라이언트 컴포넌트는 서버 컴포넌트를 import할 수 없음
제한의 이유
해결 방법: Props로 서버 컴포넌트 전달
예제 코드
문제가 되는 코드: 클라이언트 컴포넌트가 서버 컴포넌트를 import
"use client";
import { ServerComponent } from "./ServerComponent";
function ClientComponent() {
return (
<div>
<h1>Hey everyone, check out my great server component!</h1>
<ServerComponent /> {/* ❌ 에러 발생 */}
</div>
);
}
올바른 코드: 서버 컴포넌트를 props로 전달
// ServerComponent.tsx
import { readFile } from "node:fs/promises";
export async function ServerComponent() {
const content = await readFile("./some-file.txt", "utf-8");
return <div>{content}</div>;
}
// ClientComponent.tsx
"use client";
function ClientComponent({ children }: { children: React.ReactNode }) {
return (
<div>
<h1>Hey everyone, check out my great server component!</h1>
{children}
</div>
);
}
export default ClientComponent;
// TheParentOfBothComponents.tsx
import { ServerComponent } from "./ServerComponent";
import ClientComponent from "./ClientComponent";
async function TheParentOfBothComponents() {
return (
<ClientComponent>
<ServerComponent />
</ClientComponent>
);
}
export default TheParentOfBothComponents;
1. 서버 컴포넌트 정의
➡️ Node.js API(`readFile`) 사용 가능
➡️ 클라이언트 환경에 포함되지 않음
2. 클라이언트 컴포넌트 정의
➡️ 서버 컴포넌트를 직접 import하지 않음
➡️ props를 통해 서버 컴포넌트 조합 가능
3. 부모 컴포넌트에서 처리
➡️ 서버 컴포넌트를 생성하여 클라이언트 컴포넌트의 children으로 전달
➡️ 클라이언트에서 서버 컴포넌트를 props로 렌더링
이 규칙은 번들러가 클라이언트와 서버 간의 컴포넌트를 정확히 분리하여 실행 환경에 맞게 동작하도록 보장합니다.
React에서 서버 컴포넌트가 도입되기 전까지는 클라이언트 컴포넌트만 존재했으며, 이 방식으로 모든 React 애플리케이션이 작성되었습니다.
클라이언트 컴포넌트는 나쁜 것이 아닙니다.
클라이언트 컴포넌트의 역할
서버 컴포넌트와의 관계
주요 메시지
1. 클라이언트 컴포넌트
➡️ 사용자 상호작용 처리
➡️ 상태 관리 및 이벤트 핸들링
➡️ React 애플리케이션의 핵심 역할 수행
2. 서버 컴포넌트
➡️ 초기 데이터 렌더링 최적화
➡️ 클라이언트로 직렬화된 데이터를 전달
➡️ 클라이언트 컴포넌트를 보완
3. 서버와 클라이언트 협력
➡️ 서버에서 정적 데이터 제공
➡️ 클라이언트에서 동적 상호작용 활성화
React는 서버와 클라이언트 컴포넌트를 함께 사용하여 최적의 성능과 경험을 제공하도록 설계되었습니다.
React의 새로운 기능인 서버 컴포넌트(Server Components
, RSC
)와 함께 도입된 주요 기능 중 하나로 서버 액션(Server Action
)이 있다.
서버 액션은 클라이언트 코드에서 서버 함수를 직접 호출할 수 있게 해주는 기술이다.
이를 위해 use server
라는 새로운 디렉티브(directive
)를 사용한다.
요약하면, 서버 액션은 클라이언트 측에서 서버 함수를 호출할 수 있는 인터페이스를 제공하여, 클라이언트-서버 간 데이터 교환을 더욱 직관적으로 만들어 준다.
[클라이언트 코드] --(함수 호출 & 인자 직렬화)--> [서버 액션 함수 실행: 서버]
<--(결과 직렬화 & 응답)-------
타입스크립트 기반 예시 코드
actions.ts (서버 액션 파일)
"use server";
export async function addNumbers(a: number, b: number): Promise<number> {
// 이 함수는 서버에서만 실행된다.
// 클라이언트에서는 이 함수를 호출만 할 수 있고, 실제 계산 로직은 서버에서 이루어짐.
return a + b;
}
import React, { useState } from 'react';
import { addNumbers } from './actions'; // 서버 액션 import
export default function ClientComponent() {
const [result, setResult] = useState<number | null>(null);
const handleClick = async () => {
// 클라이언트 코드에서 서버 액션 호출
const sum = await addNumbers(5, 10);
setResult(sum);
};
return (
<div>
<button onClick={handleClick}>서버 액션 호출</button>
{result !== null && <p>계산 결과: {result}</p>}
</div>
);
}
위 예제에서 ClientComponent는 사용자가 버튼을 클릭할 때 서버 액션 addNumbers를 호출하고, 서버에서 계산된 결과를 받아 화면에 표시한다. 이 과정에서 클라이언트에서는 단순히 함수를 호출하는 듯 하지만, 실제 로직은 서버에서 처리되고, 그 결과만 클라이언트로 전달된다.
이 예제는 리액트(React)의 새로운 기능인 "서버 액션(Server Action)"을 활용하는 방식입니다. Next.js나 Remix와 유사하게, 리액트는 폼(form) 처리와 뮤테이션(mutation)을 위한 1급(First-Class) 프리미티브를 제공하고 있습니다. 즉, 폼을 제출할 때 자바스크립트 번들이 로드되기 전에도 서버 함수를 호출할 수 있습니다.
작동 방식:
<form>
태그의 action
속성에 "서버 액션" 함수를 전달합니다.FormData
형태로 서버 액션 함수의 첫 번째 인자로 전달합니다.Progressive Enhancement
)가 가능합니다. 즉, 폼 제출 시 점진적으로 앱이 더 나은 경험을 제공할 수 있습니다.정리하자면, 서버 액션을 사용하면 다음과 같은 이점이 있습니다.
[사용자] --- (폼 제출) ---> [<form action={requestUsername}>]
│
│ (브라우저)
↓
[서버 액션 requestUsername 함수 호출]
│
FormData 전달
↓
[서버에서 데이터 처리 로직 실행]
│
↓
[응답 반환 → UI 업데이트]
코드 예시 (원문 예시 코드 그대로)
// App.js
async function requestUsername(formData) {
'use server';
const username = formData.get('username');
// ...
}
export default function App() {
return (
<form action={requestUsername}>
<input type="text" name="username" />
<button type="submit">Request</button>
</form>
);
}
<form action={requestUsername}>
를 통해 폼 제출 시 해당 서버 액션 함수가 호출되며, 폼의 데이터가 서버로 전달됩니다.