React Cheat Sheet (advanced)

1. RSCs / React Server Components (RSCs)란 무엇인가?

  • React Server Components (RSCs) 개념

    • RSCs는 서버에서만 실행되는 React 컴포넌트로, 클라이언트 쪽 JavaScript 번들에 포함되지 않습니다.
    • 서버에서 실행되기 때문에 파일 시스템 접근, 정적 콘텐츠 읽기, 데이터 레이어 접근 등이 가능합니다.
    • RSCs는 서버-렌더링된 멀티페이지 앱(MPAs)과 클라이언트 렌더링된 SPA의 장점을 결합하여 성능, 효율성, 유지보수성을 향상시킵니다.
    • 서버에서 RSC가 생성한 데이터는 JSON 형태로 클라이언트에 전달되고, 클라이언트는 이 데이터를 기반으로 화면을 렌더링합니다.
  • RSCs의 동작 원리

    1. 서버에서 컴포넌트를 실행해 React 요소(React element) 트리를 생성.
    2. React 요소 트리를 직렬화하여 클라이언트에 전달.
    3. 클라이언트는 전달받은 JSON 데이터를 파싱하여 화면을 렌더링.
  • RSCs의 장점

    • 예측 가능한 성능: 서버에서 실행되므로 클라이언트 디바이스 성능에 의존하지 않음.
    • 보안 강화: 서버 환경에서 민감한 데이터 처리 가능.
    • 비동기 처리 가능: 서버에서 실행 결과를 기다린 뒤 데이터를 클라이언트로 전달.
  • RSCs와 서버 렌더링의 상호작용

    1. RSC 렌더러: RSC를 React 요소 트리로 변환.
    2. 서버 렌더러: React 요소 트리를 HTML 스트림이나 문자열로 변환.
  • RSC 렌더링 로직

    • React 요소를 재귀적으로 순회하며, 각 요소를 처리.
    • React 요소는 기본적으로 다음 두 가지 타입으로 구분:
      1. DOM 요소 (e.g., <div>, <span>): 문자열로 변환.
      2. React 컴포넌트 (e.g., <Footer />): 컴포넌트 함수 실행 후 반환값 재귀 처리.
    • props나 children 등 내부 값도 재귀적으로 처리.
  • 데이터 흐름 도식화

    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는 서버에서만 실행되어 클라이언트에 전달되는 React 요소 데이터를 생성합니다. 이를 통해 클라이언트 쪽에서 처리 부담을 줄이고, 서버의 계산 성능과 보안을 활용할 수 있습니다.
  • 데이터 흐름

    1. 서버에서 JSX를 React 요소로 변환합니다.
    2. 변환된 React 요소를 JSON으로 직렬화하여 클라이언트로 전송합니다.
    3. 클라이언트는 전달받은 JSON을 React 요소로 복원하고, 화면을 렌더링합니다.
  • 장점 요약

    • 클라이언트 디바이스의 성능 한계를 극복.
    • 서버에서 민감한 데이터 처리 가능.
    • 비동기 처리로 효율적인 데이터 전송.
  • RSC와 서버 렌더링의 차이점

    • RSC는 React 요소 트리를 생성하는 역할만 하고, 서버 렌더링은 이를 HTML 스트림으로 변환하여 전송하는 추가 작업을 수행합니다.
  1. 서버 측 코드: 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}`);
    });
  2. 클라이언트 측 코드 (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 />);
    }
  3. 주요 동작

    • 서버는 React.ReactElement를 생성하고 이를 JSON으로 직렬화하여 클라이언트로 전달합니다.
    • 클라이언트는 JSON 데이터를 복원하여 React 요소로 변환하고 렌더링합니다.
  4. 결과

    • 서버: JSX를 JSON으로 변환해 클라이언트로 전달.
    • 클라이언트: 전달받은 JSON 데이터를 기반으로 화면 렌더링.

2. RSCs / React 요소 직렬화(Serialization)

  • 직렬화(Serialization)는 React 요소(React element)를 HTML 문자열로 변환하는 과정입니다.
  • 서버에서 생성된 React 요소는 HTML 문자열로 변환되어 클라이언트로 전송됩니다.
  • 이 과정은 ReactDOMServer.renderToString을 사용하여 이루어집니다.
const element = <h1>Hello, world</h1>;
const htmlString = ReactDOMServer.renderToString(element);
// 결과: '<h1>Hello, world</h1>'
  • 이 HTML 문자열은 클라이언트에서 초기 렌더링에 사용되며, 이후 React가 이를 "hydrate(재수화)"하여 DOM과 이벤트 핸들러를 연결합니다.

  • 직렬화의 중요성

    1. 빠른 초기 렌더링: 클라이언트가 서버에서 완성된 HTML을 받아 즉시 페이지를 표시할 수 있습니다.
    2. 일관성 보장: 서버와 클라이언트가 동일한 HTML을 사용하므로 초기 렌더링과 최종 렌더링 간에 차이가 없습니다.
    3. 재수화(hydration) 지원: React는 HTML 문자열을 기반으로 이벤트 핸들러를 연결하고 동적 콘텐츠를 채울 수 있습니다.
  • React 요소의 JSON 직렬화

    1. React 요소는 일반 JavaScript 객체와 다르며 $$typeof라는 특수한 속성을 가집니다.
    2. $$typeof 속성은 Symbol로 되어 있어 기본적인 JSON.stringify로는 직렬화할 수 없습니다.
    3. 이를 해결하기 위해 JSON.stringifyJSON.parsereplacer 함수를 사용하여 직렬화/역직렬화합니다.
  • 직렬화 (서버 측)

    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 요소 트리를 HTML 문자열로 변환하여 클라이언트로 전송.
    2. 클라이언트에서 JSON 역직렬화: HTML 문자열을 기반으로 React 요소 복원.
    3. 재수화(hydration): 클라이언트에서 이벤트 핸들러와 동적 콘텐츠 연결.
  • 데이터 흐름 도식화

    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 요소 직렬화의 필요성

    • React 요소는 서버에서 HTML 문자열로 직렬화되어 클라이언트로 전달됩니다. 이 과정은 초기 페이지 로딩 속도를 개선하고 서버와 클라이언트 간의 일관성을 보장합니다.
    • 데이터 처리 흐름
      • 서버: React 요소를 생성하고 JSON 형태로 직렬화.
      • 클라이언트: JSON 데이터를 역직렬화하여 React 요소를 복원.
      • 렌더링 및 재수화: 복원된 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;
    });

3. RSCs / 서버 사이드 렌더링 vs. 리액트 서버 컴포넌트

  • 서버사이드 렌더링(SSR)

    • 동작 방식:
      1. 서버에서 React 컴포넌트를 실행하여 완전한 HTML 문자열을 생성.
      2. 이 HTML 문자열을 클라이언트로 전송.
      3. 클라이언트에서 HTML을 받아 화면에 렌더링 후, React가 이를 "hydrate(재수화)"하여 이벤트 핸들러를 연결.
    • 특징:
      • 서버가 모든 렌더링 작업(HTML 생성)을 처리.
      • 클라이언트는 초기 HTML을 빠르게 표시하지만, 재수화 과정에서 추가적인 작업(React 이벤트 연결)이 필요.
      • HTML 생성 비용이 클라이언트로 오기 전에 서버에서 발생.
  • React Server Components(RSCs)

    • 동작 방식:
      1. 서버에서 React Server Components를 실행하여 React 요소의 JSON 표현을 생성.
      2. 이 JSON 데이터를 클라이언트로 전송.
      3. 클라이언트에서 JSON 데이터를 React 요소로 복원한 뒤 HTML로 렌더링.
    • 특징:
      • 서버는 React 컴포넌트 실행 결과를 React 요소(데이터 트리)로 직렬화하여 전송.
      • 클라이언트는 데이터를 받아서 React가 HTML을 렌더링.
      • 클라이언트와 서버 간 작업을 분리하여 렌더링 최적화.
  • 차이점 비교

    특징 서버사이드 렌더링(SSR) React Server Components(RSCs)
    서버 작업 HTML 생성 (ReactDOMServer.renderToString) React 요소 트리 생성 및 직렬화
    클라이언트 작업 HTML 재수화 (이벤트 연결, 동적 렌더링) JSON 데이터를 React 요소로 복원 후 렌더링(재수화 - 정확히말하면 재수화는 아니고 그냥 랜더링임)
    전송 데이터 완전한 HTML 문자열 직렬화된 React 요소(JSON 형태)
    주요 이점 초기 로딩 속도 개선 (완전한 HTML 제공) 데이터 처리 비용 감소 (React 컴포넌트 실행은 서버에서만 수행)
    적합한 상황 모든 HTML을 클라이언트에 즉시 보여줘야 하는 경우 데이터 중심 렌더링에서 성능 최적화가 중요한 경우
  • RSCs의 이점

    • RSCs의 주요 이점은 클라이언트의 렌더링 비용 감소에 있습니다.
      • 서버에서 컴포넌트 로직 실행: 서버에서 데이터를 가져오고, 컴포넌트를 실행하여 React 요소 트리를 생성.
      • 클라이언트의 부하 감소: 클라이언트는 데이터를 받아 HTML 생성만 수행하므로 더 빠르게 반응.
      • 네트워크 데이터 효율화: JSON 데이터로 직렬화되어 전송되므로 HTML보다 가볍고 유연.
  • 정리

  • RSC는 JSON 형태로 직렬화된 React 요소 트리를 클라이언트로 전달하고, 클라이언트는 이를 사용해 렌더링합니다.

  • RSC의 이점은 클라이언트에서 React 컴포넌트 로직을 실행할 필요 없이 이미 계산된 데이터를 받아 HTML을 생성하므로 성능이 더 최적화된다는 점입니다.

결론적으로, RSC는 클라이언트와 서버 간 작업을 분리하여 성능을 최적화하고, 특히 데이터 중심의 애플리케이션에서 유리합니다.

4. RSCs / 리액트 서버 컴포넌트와 hydrate

  • RSC와 재수화(hydration)의 차이

    • RSCs의 동작
      • RSC는 클라이언트에서 hydrate(재수화) 대신, 서버에서 직렬화된 React 요소(JSON 데이터)를 받아 이를 React의 렌더링 로직을 통해 DOM으로 생성합니다.
      • 클라이언트는 서버에서 전송된 JSON 데이터를 React 요소로 복원하여 새롭게 렌더링합니다.
      • 재수화는 기존 DOM을 기반으로 React가 이벤트 핸들러를 연결하는 과정인데, RSC는 HTML을 새로 생성하는 과정에 더 가깝습니다.
  • SSR과 재수화(hydration)

    • SSR에서 클라이언트는 서버가 전달한 HTML을 그대로 DOM으로 사용하고, React는 hydrate(재수화)를 통해 해당 DOM에 이벤트 핸들러와 동적 콘텐츠를 연결합니다.
    • 여기서 재수화는 이미 렌더링된 HTML과 React의 가상 DOM을 동기화하는 과정입니다.
  • 비교: RSC와 SSR의 클라이언트 처리

    특징 React Server Components (RSCs) 서버사이드 렌더링 (SSR)
    초기 클라이언트 작업 JSON 데이터를 기반으로 React 요소를 복원하여 DOM 생성. 서버에서 전송된 HTML을 사용해 DOM 생성 및 재수화.
    hydrate 과정 필요 없음 (JSON 데이터를 새로 렌더링). 기존 HTML과 React 가상 DOM을 동기화.
    결과물 클라이언트에서 새롭게 HTML을 생성. 서버가 생성한 HTML을 그대로 사용.
  • RSC에서 hydrate가 필요 없는 이유

    • RSC는 다음과 같은 특성 때문에 hydrate 과정을 필요로 하지 않습니다:
      1. React 요소 트리를 새로 생성:
        • 서버에서 직렬화된 React 요소를 받아 클라이언트에서 DOM을 새로 생성합니다.
        • 클라이언트가 DOM을 새로 생성하므로 기존 DOM과 동기화할 필요가 없습니다.
      2. JSON 데이터 기반 렌더링:
        • RSC는 직렬화된 데이터(JSON)를 기반으로 렌더링하며, 클라이언트는 이 데이터를 사용해 React 요소를 복원한 뒤 DOM을 직접 렌더링합니다.
  • RSC가 hydrate를 대체하는 방식

    • RSC는 재수화를 대체하는 방식으로 React 요소 트리의 데이터를 클라이언트에서 다시 렌더링합니다. 이를 통해:
      • 재수화의 복잡성을 제거합니다.
      • 더 나은 성능을 제공합니다(클라이언트는 이미 계산된 데이터를 렌더링만 수행).
  • RSC는 hydrate 과정을 거치지 않습니다.

    • SSR은 서버가 생성한 HTML과 React 가상 DOM을 동기화(hydration)합니다.
    • 반면, RSC는 서버가 생성한 React 요소 트리를 클라이언트에서 새롭게 렌더링합니다.
    • 따라서 RSC는 hydrate와는 완전히 다른 렌더링 접근 방식을 사용합니다.

이 두 방식의 차이로 인해 RSC는 더 간단한 구조로 동작하며, 클라이언트 부하를 줄일 수 있습니다.

5. RSCs / 리액트 서버 컴포넌트(RSCs)에서 Navigation(네비게이션) 처리

기존 HTML 링크(<a href="/blog">)를 클릭하면 전체 페이지 새로고침(Full-Page Navigation)이 발생합니다. 이는 사용자 경험을 저하시킬 수 있으므로, RSCs에서는 Soft Navigation을 구현합니다.

  • Soft Navigation이란?

    • Full-Page Navigation 없이, 링크를 클릭하면 서버에 요청하여 필요한 JSX 데이터만 받아오고, React가 페이지를 재렌더링하는 방식.
    • 페이지 상태(state)가 보존되며, 사용자 경험이 더 부드러워집니다.
  • 구현 방법

    1. 이벤트 위임(Event Delegation):
      • window에 이벤트 리스너를 추가하여 클릭 이벤트를 감지.
      • 클릭 대상이 <a> 태그일 경우 기본 동작(페이지 새로고침)을 막고, 대신 Soft Navigation 처리.
    2. 클라이언트에서 navigate 함수 구현:
      • 서버에 새로운 페이지 URL 요청.
      • 서버에서 반환된 직렬화된 JSX 데이터를 역직렬화(JSON.parse)하여 React 요소로 변환.
      • React의 root.render로 새로운 페이지 렌더링.
    3. 서버의 응답 처리:
      • 요청 헤더에 jsx-only가 있으면, JSX 트리(JSON 형태)를 직렬화하여 응답.
      • 일반 요청의 경우 완전한 HTML 문자열로 응답.
  • 주요 코드 설명

    1. 클라이언트: 이벤트 위임 및 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); // 새 페이지 렌더링
      }
    2. 서버: 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. 페이지 새로고침 없음:
      • 새로운 페이지로 이동하면서 기존 상태를 보존.
      • 빠르고 부드러운 사용자 경험 제공.
    2. 데이터 효율성:
      • 전체 HTML 대신 필요한 JSX 데이터만 전송하여 네트워크 비용 감소.
    3. React와 완벽한 통합:
      • React의 root.render를 활용하여 동적으로 렌더링.
  • 데이터 흐름 도식화

    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 구현

    • Soft Navigation은 페이지 새로고침 없이 사용자 경험을 부드럽게 만드는 네비게이션 방식입니다. 이를 위해, <a> 태그 클릭 시 서버로부터 JSX 데이터(JSON 형태)만 받아와서 페이지를 렌더링합니다.
    • 작동 과정
      • 사용자가 <a> 태그를 클릭하면, 기본 동작(페이지 새로고침)을 막고, 새로운 페이지 데이터를 요청합니다.
      • 서버는 해당 페이지에 필요한 React 컴포넌트를 실행하여 JSX 데이터를 반환합니다.
      • 클라이언트는 JSX 데이터를 React 요소로 변환한 뒤, 기존 페이지를 새 페이지로 대체합니다.
  • Soft Navigation의 핵심

    • 페이지 상태 유지: 기존 SPA처럼 상태를 잃지 않고 페이지 이동 가능.
    • HTML 대신 데이터 전송: 네트워크 효율성을 극대화.
    • React의 렌더링 최적화: React API(root.render)를 활용해 간결하고 효율적인 코드 작성.

6. RSCs / 리액트 서버 컴포넌트(RSCs)의 한계

RSCs는 성능 최적화를 제공하지만, 모든 React 컴포넌트가 서버 컴포넌트로 동작할 수는 없습니다. 아래 이유를 중심으로 RSCs의 한계를 살펴봅니다:

  1. 상태(State) 관련 제한

    • useState는 클라이언트 전용 API입니다.
      • 서버는 상태를 저장하거나 관리할 수 없습니다. 상태는 클라이언트별로 로컬에 존재해야 하며, 이를 서버에서 관리하면 보안 문제(다른 클라이언트와 상태가 공유되는 위험)가 발생할 수 있습니다.
      • useState의 상태 변경 함수(setState)는 함수로 직렬화할 수 없기 때문에 서버-클라이언트 간 전송이 불가능합니다.
  2. 이벤트 핸들러의 제한

    • onClick 같은 이벤트 핸들러는 클라이언트 전용입니다.
      • 서버는 클릭 등의 UI 이벤트를 처리할 수 없습니다. 서버는 인터랙션이 없는 환경이기 때문에 이런 핸들러를 사용할 수 없습니다.
      • 모든 RSC의 props는 직렬화 가능(serializable)해야 하므로, 함수 형태의 이벤트 핸들러를 서버 컴포넌트의 props로 전달할 수 없습니다.
  3. 해결책: 서버와 클라이언트 컴포넌트 분리

    • 위 제한을 극복하기 위해, 서버 렌더링 가능한 부분과 클라이언트에서만 실행되는 인터랙션 부분을 분리해야 합니다.
    • 예제:
      • 서버 컴포넌트: 정적인 텍스트와 레이아웃 렌더링.
      • 클라이언트 컴포넌트: useState와 이벤트 핸들러를 통한 인터랙션 처리.
    // 서버 컴포넌트
    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>
      );
    }
  4. RSCs의 성능 이점

    • JavaScript 번들 크기 감소:
      • 인터랙션이 필요한 작은 클라이언트 컴포넌트만 번들에 포함됩니다.
      • 나머지 부분은 서버에서 렌더링되어 클라이언트 번들에서 제외됩니다.
    • 네트워크 효율성:
      • 클라이언트로 전송되는 데이터 양이 줄어들어 로딩 시간이 빨라집니다.
    • CPU 사용량 감소:
      • 클라이언트에서 JavaScript 파싱 및 실행 작업이 줄어들어 더 나은 성능을 제공합니다.
  • 데이터 흐름 도식화

    1. 사용자가 페이지 요청
       ➡️ 서버에서 ServerCounter 컴포넌트 렌더링
       ➡️ HTML과 클라이언트 컴포넌트의 초기 상태 데이터 전송
    
    2. 클라이언트에서 InteractiveClientPart 컴포넌트 렌더링
       ➡️ useState로 상태 관리 및 onClick 이벤트 핸들러 연결
       ➡️ 화면에 인터랙티브 UI 렌더링
  • 정리

  1. 리액트 서버 컴포넌트의 제한
    • RSC는 서버와 클라이언트 역할을 구분합니다. 서버에서는 상태 관리(useState)나 이벤트 핸들러(onClick)를 사용할 수 없습니다.
    • 이유:
      1. 서버 상태는 클라이언트별로 독립적이어야 하며, 공유되면 보안 문제가 발생할 수 있습니다.
      2. 함수 형태의 데이터(setState, onClick)는 직렬화할 수 없기 때문입니다.
  2. 해결 방법
    • 컴포넌트를 서버 컴포넌트와 클라이언트 컴포넌트로 분리합니다:
      • 서버 컴포넌트는 정적인 UI 렌더링.
      • 클라이언트 컴포넌트는 상태 관리와 이벤트 핸들러 처리.
  3. RSC의 주요 이점
    • 클라이언트로 전송되는 JavaScript 번들을 최소화하여 빠른 로딩 속도를 제공합니다.
    • CPU 및 네트워크 효율성이 개선되어 사용자 경험이 향상됩니다.

RSC를 도입하면 컴포넌트 분리 작업이 필요해지지만, 이를 통해 성능과 사용자 경험을 크게 개선할 수 있습니다.

7. RSCs / 함수는 직렬화가 불가능함

함수는 직렬화가 불가능합니다. 이는 자바스크립트의 기본적인 동작 방식 및 JSON 표준에 따른 것입니다. 아래에서 이유와 직렬화와 관련된 제한 사항을 자세히 설명하겠습니다.

  1. 직렬화란?

    • 직렬화(Serialization)는 데이터 구조나 객체를 네트워크로 전송하거나 파일로 저장할 수 있도록 문자열로 변환하는 과정입니다.
    • JavaScript에서는 주로 JSON.stringifyJSON.parse를 사용하여 객체를 직렬화하고 복원합니다.
  2. 함수가 직렬화되지 않는 이유

    • JSON 표준의 제한:

      • JSON은 문자열, 숫자, 불리언, 객체, 배열과 같은 데이터 유형을 표현할 수 있지만, 함수는 표현하지 않습니다.

      • 함수는 실행 코드와 관련된 상태(클로저)를 포함하고 있는데, 이 상태를 문자열로 변환할 방법이 표준 JSON에는 없습니다.

        const obj = {
          name: "John",
          greet: function () {
            return "Hello " + this.name;
          },
        };
        
        console.log(JSON.stringify(obj));
        // 결과: {"name":"John"} (greet 함수는 포함되지 않음)
    • 함수는 실행 환경에 의존:

      • 함수는 코드 뿐 아니라 해당 함수가 생성된 컨텍스트(클로저) 정보를 가지고 있습니다.
      • 이러한 실행 환경은 메모리와 관련된 복잡한 정보를 포함하므로 문자열로 변환할 수 없습니다.
  3. 예제: 함수 직렬화 실패

    • 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
      • func 함수는 직렬화 단계에서 제거되었으므로 복원할 수 없습니다.
  4. 함수 직렬화가 필요한 경우의 대안

    1. 함수 코드를 문자열로 저장:

      • 함수를 문자열로 변환해 저장하거나 전송한 뒤, 클라이언트에서 eval이나 new Function을 사용해 다시 함수로 변환할 수 있습니다.

      • 그러나 이는 보안상 위험하며, 일반적으로 권장되지 않습니다.

        const obj = {
          func: "console.log('Hello!')",
        };
        
        const deserializedFunc = new Function(obj.func);
        deserializedFunc(); // "Hello!"
    2. 직렬화 가능한 데이터로 대체:

      • 함수 대신 데이터를 기반으로 동작을 정의.

      • 예: 함수 대신 콜백 이름을 문자열로 저장하고, 클라이언트에서 해당 이름에 맞는 함수를 실행.

        const obj = {
          callback: "sayHello",
        };
        
        const callbacks = {
          sayHello: () => console.log("Hello!"),
        };
        
        callbacks[obj.callback](); // "Hello!"
  5. 결론

    • 함수는 직렬화가 불가능합니다, JSON 표준이 이를 지원하지 않기 때문입니다.
    • 직렬화가 필요한 경우, 데이터로 표현 가능한 방식으로 설계하거나, 함수 문자열화 등의 대체 방법을 사용해야 합니다.
    • 하지만 보안 및 유지보수성을 고려하여 함수 직렬화는 되도록 피하는 것이 좋습니다.

RSC에서 함수가 직렬화되지 않는 이유도 이와 같습니다. 서버는 함수(onClick, setState)를 클라이언트로 전달할 수 없으므로, 클라이언트 전용 컴포넌트로 분리해야 합니다.

8. RSCs / React 서버 컴포넌트(RSCs)의 내부 동작 원리

  1. 서버 컴포넌트와 클라이언트 컴포넌트의 구분

    • 클라이언트 컴포넌트는 파일 상단에 "use client" 지시어를 추가하여 명시합니다.
    • 빌드 시, 서버 그래프와 클라이언트 그래프를 분리:
      • 서버 그래프: 서버에서만 실행되며 클라이언트 번들에 포함되지 않음.
      • 클라이언트 그래프: 필요한 컴포넌트만 클라이언트 번들로 묶이고, 필요 시 로드(lazy load) 가능.
  2. RSC의 렌더링 과정

    • 서버:
      1. 서버는 컴포넌트 트리를 생성하면서, 클라이언트 컴포넌트를 만날 경우 모듈 참조를 포함한 플레이스홀더를 생성.
      2. 이 트리를 JSON 형태로 직렬화해 클라이언트에 전송.
    • 클라이언트:
      1. 클라이언트는 서버로부터 받은 JSON 데이터를 React 트리로 복원.
      2. 플레이스홀더를 참조하여 필요한 클라이언트 모듈을 불러와 렌더링.
      3. 클라이언트 컴포넌트는 상태 관리(useState)나 이벤트 핸들러(onClick)를 포함하여 상호작용 가능.
  3. 모듈 참조 처리

    • 서버는 클라이언트 컴포넌트를 직접 렌더링하지 않고, 모듈 참조(module reference)를 통해 다음을 포함:
      • 컴포넌트 파일 경로 (filename).
      • 컴포넌트 ID (moduleId).
    • 클라이언트는 이 참조를 사용해 번들에서 적절한 모듈을 찾아와 렌더링.
  4. 서버 컴포넌트의 Suspense 처리

    • 서버 컴포넌트는 Suspense 경계로 감싸질 수 있습니다.
    • 서버에서 데이터가 준비될 때까지 비동기로 데이터를 스트리밍하여 클라이언트에 제공.
  5. 이점

    • 클라이언트 번들 크기 최소화: 클라이언트 컴포넌트만 번들에 포함.
    • 성능 최적화: 서버는 데이터를 빠르게 렌더링하며 클라이언트는 필요한 모듈만 동적으로 로드.
    • 클라이언트 컴포넌트는 상태 및 이벤트를 지원.
1. 서버에서 React 트리 생성
   ➡️ ServerCounter 컴포넌트 렌더링
   ➡️ 클라이언트 컴포넌트(ClientPart)에 모듈 참조를 포함한 플레이스홀더 생성
   ➡️ React 요소 트리를 JSON 형태로 직렬화 후 클라이언트 전송

2. 클라이언트에서 JSON 데이터 복원
   ➡️ React 트리 재구성
   ➡️ 모듈 참조를 기반으로 번들에서 클라이언트 컴포넌트를 가져와 렌더링
   ➡️ 페이지 완성
  • 정리
    • React 서버 컴포넌트 내부 동작

      1. 서버-클라이언트 컴포넌트 분리:
        • 서버는 정적인 데이터를 렌더링.
        • 클라이언트는 상호작용이 필요한 부분을 처리.
      2. 클라이언트 컴포넌트의 처리:
        • 클라이언트 컴포넌트는 모듈 참조를 기반으로 번들에서 로드되어 클라이언트에서 렌더링됩니다.
      3. 서버 컴포넌트와 Suspense:
        • 서버 컴포넌트는 데이터를 비동기로 처리하여 준비되는 즉시 클라이언트로 스트리밍합니다.
    • 이점

      • 서버와 클라이언트의 역할을 분리하여 성능 최적화.
      • 클라이언트 번들을 최소화하여 네트워크 및 CPU 부담 감소.
      • React의 트리 렌더링 구조를 활용한 유연한 렌더링.

RSC를 활용하면 필요한 부분만 클라이언트에 부담을 주며, 나머지는 서버에서 효율적으로 처리할 수 있습니다.

9. RSCs / 리액트 서버 컴포넌트(RSCs)와 클라이언트 컴포넌트의 실행 경계

  1. 오해:

    • 서버 컴포넌트는 서버에서만 실행되고, 클라이언트 컴포넌트는 클라이언트에서만 실행된다고 생각하는 것은 잘못된 이해입니다.
    • 클라이언트 컴포넌트도 서버에서 실행됩니다, 단, 이는 초기 렌더링 시에 한정됩니다.
  2. 컴포넌트의 실행이란?

    • 컴포넌트의 실행은 해당 컴포넌트의 함수가 호출되는 것을 의미합니다.
    • 호출 결과는 React 요소(React element) 객체로 반환됩니다.
    function MyComponent() {
      return <div>hello world</div>;
    }
    
    // MyComponent 실행 결과:
    {
      $$typeof: Symbol(react.element),
      type: "div",
      props: {
        children: "hello world"
      }
    }
  3. 실행 경계의 정확한 이해

    • 서버에서 실행:
      • 서버 컴포넌트는 서버에서만 실행되어 React 요소를 반환.
      • 클라이언트 컴포넌트도 서버에서 실행되며, React 요소를 반환.
      • 이 결과로 서버에서 전체 React 요소 트리를 생성.
    • 클라이언트에서 실행:
      • 서버에서 클라이언트로 전송된 React 트리를 기반으로 클라이언트 컴포넌트가 실행.
      • 서버 컴포넌트는 클라이언트에서 실행되지 않음.
  4. 핵심 흐름 요약

    • 서버:
      1. 서버 컴포넌트와 클라이언트 컴포넌트를 모두 실행하여 React 요소 트리를 생성.
      2. 이 트리를 HTML 문자열로 직렬화하여 클라이언트로 전송.
    • 클라이언트:
      1. HTML을 기반으로 클라이언트 컴포넌트를 실행하여 동적 상호작용을 활성화.
      2. 이후 서버 컴포넌트는 클라이언트에서 실행되지 않음.
1. 서버에서 컴포넌트 실행
   ➡️ 서버 컴포넌트 실행: React 요소 반환
   ➡️ 클라이언트 컴포넌트 실행: React 요소 반환
   ➡️ 전체 React 요소 트리 생성

2. 서버에서 HTML로 직렬화
   ➡️ React 요소 트리를 HTML 문자열로 변환
   ➡️ HTML 문자열을 클라이언트로 전송

3. 클라이언트에서 렌더링
   ➡️ HTML을 DOM으로 렌더링
   ➡️ 클라이언트 컴포넌트만 실행 (상호작용 처리)
  1. 실행 경계의 정확한 이해
    • 서버와 클라이언트 컴포넌트 모두 초기 렌더링 시 서버에서 실행됩니다.
    • 서버는 전체 React 요소 트리를 생성하고 이를 HTML로 직렬화하여 클라이언트로 보냅니다.
    • 클라이언트는 이후 클라이언트 컴포넌트만 실행하며, 동적 상호작용을 처리합니다.
  2. 실행 흐름 요약
    1. 서버:
      • 서버 컴포넌트와 클라이언트 컴포넌트를 실행하여 React 요소 트리를 생성.
      • React 요소 트리를 HTML로 직렬화하여 전송.
    2. 클라이언트:
      • 클라이언트 컴포넌트만 실행하며, 이벤트 처리 및 상태 관리를 수행.
  3. 오해와 올바른 이해
    • 오해: 서버 컴포넌트와 클라이언트 컴포넌트가 각각 서버와 클라이언트에서만 실행된다.
    • 올바른 이해:
      • 서버에서 초기 렌더링 시 클라이언트 컴포넌트도 실행.
      • 클라이언트는 이후 동적 상호작용을 위해 클라이언트 컴포넌트만 실행.

이로 인해 서버와 클라이언트의 역할이 명확히 분리되며, 성능과 상호작용 효율성을 최적화할 수 있습니다.

10. RSCs Rules / 서버 컴포넌트의 규칙

이제 서버 컴포넌트가 내부적으로 어떻게 작동하는지 이해했으므로 서버 컴포넌트로 작업할 때 따라야 할 몇 가지 규칙, 또는 더 광범위하게 서버 컴포넌트로 작업할 때 염두에 두어야 할 사항에 대해 논의해 보겠습니다.

11. RSCs Rules / 리액트 서버 컴포넌트(RSCs)와 직렬화 규칙

  1. 직렬화 가능한 props만 허용:

    • RSCs에서 모든 props는 반드시 직렬화 가능한 값이어야 합니다.
    • 서버는 props를 직렬화하여 클라이언트로 전송하기 때문에 함수나 비직렬화 가능한 값은 허용되지 않습니다.
  2. render props 패턴의 비호환성:

    • render props처럼 함수를 props로 전달하는 패턴은 사용할 수 없습니다.
    • 이유:
      • 함수는 직렬화할 수 없기 때문에 서버에서 클라이언트로 전송이 불가능합니다.
      • 이를 사용하려고 하면 에러가 발생합니다.
  3. 예제: 직렬화 불가능한 props로 인한 문제

    function ServerComponent() {
      return <ClientComponent onClick={() => alert("hi")} />;
    }
    // ❌ 에러 발생: onClick은 함수이므로 직렬화 불가능
  4. 해결 방법:

    • 클라이언트 컴포넌트로 캡슐화:
      • 직렬화가 필요한 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 같은 동작 처리 포함)
  1. 직렬화 가능한 props만 허용

    • RSCs에서는 모든 props는 직렬화 가능해야 합니다. 함수와 같은 비직렬화 값은 서버에서 클라이언트로 전송할 수 없으므로 사용이 제한됩니다.
  2. render props 패턴의 비호환성

    • 기존에 React에서 사용하던 render props 패턴(props로 함수 전달)은 RSCs에서 동작하지 않습니다.
    • 이는 RSCs가 props를 클라이언트로 전송하기 위해 JSON으로 직렬화해야 하는데, 함수는 이를 지원하지 않기 때문입니다.
  3. 해결 방법

    • 서버와 클라이언트의 역할 분리:
      • 서버 컴포넌트는 직렬화 가능한 정적 데이터만 처리.
      • 클라이언트 컴포넌트에서 동적 상호작용(onClick 등)을 처리.
  4. 주요 이점

    • props 직렬화 규칙을 통해 서버와 클라이언트 간 데이터 전송의 일관성과 안전성을 보장합니다.
    • 서버와 클라이언트 컴포넌트의 명확한 역할 분리를 통해 성능 최적화를 지원합니다.

이로 인해 RSCs의 동작 원리가 더 명확해지며, 개발자는 직렬화 가능한 데이터 구조로 설계해야 하는 추가적인 이해가 필요합니다.

12. RSCs Rules / 리액트 서버 컴포넌트(RSCs)에서의 Hook 사용 제한

  1. 서버 환경의 특성

    • 서버는 DOM, window 객체, 인터랙션 등이 없는 환경입니다.
    • 클라이언트와 달리, 서버는 상호작용이 필요 없는 정적인 렌더링만 처리합니다.
  2. 효과적인(Effecful) Hook 금지

    • useEffect, useLayoutEffect와 같은 효과를 가지는 Hook은 서버 컴포넌트에서 사용할 수 없습니다.
      • 이유: 서버에서는 브라우저 기반 API나 상태를 처리하지 않기 때문입니다.
    • 상태 기반의 useState나, 클라이언트와 연관된 useEffect도 지원되지 않습니다.
  3. 허용되는 Hook

    • 서버에서 사용할 수 있는 Hook은 상태나 브라우저 API에 의존하지 않는 Hook에 한정됩니다.
    • 예:
      • 허용: useRef (상태나 DOM 의존성이 없음)
      • 금지: useState, useEffect
  4. 프레임워크 규칙

    • Next.js와 같은 프레임워크는 서버 컴포넌트에서 모든 Hook 사용을 금지하는 규칙을 강제하기도 합니다.
    • 이는 개발자가 안전하게 설계된 컴포넌트를 작성하도록 유도합니다.
1. 서버 컴포넌트에서 Hook 호출
   ➡️ 상태와 효과 기반 Hook(`useState`, `useEffect`) 호출 시 에러 발생
   ➡️ 서버에서 상태와 DOM 처리가 불가능하기 때문

2. 허용 가능한 Hook
   ➡️ 서버 컴포넌트에서는 DOM 및 상태 의존성이 없는 Hook(`useRef`)만 호출 가능
   ➡️ 정적 데이터를 처리하고 React 요소를 생성
  1. 서버 컴포넌트에서의 Hook 제한
    • 서버는 클라이언트와 달리 상호작용 및 상태 관리를 처리하지 않으므로, 효과적인 Hook(useEffect, useState)은 금지됩니다.
    • 대신, 상태와 DOM 의존성이 없는 useRef와 같은 일부 Hook은 사용할 수 있습니다.
  2. 주요 이유
    • 서버는 상호작용 없이 정적인 데이터를 처리하기 때문에, 클라이언트 전용 기능을 지원할 필요가 없습니다.
    • 이는 컴포넌트를 더 안전하고 예측 가능하게 만듭니다.
  3. 프레임워크에서의 제한
    • Next.js와 같은 프레임워크는 서버 컴포넌트에서 모든 Hook 사용을 금지하는 린트 규칙을 제공해 개발자의 실수를 방지합니다.
  4. 장점
    • 서버 컴포넌트를 더 안정적이고 예측 가능하게 설계할 수 있습니다.
    • 개발자는 상태 관리와 동작 처리를 클라이언트 컴포넌트로 분리하게 되어 명확한 역할 구분을 유지합니다.

결론: 서버 컴포넌트는 정적 렌더링을 처리하며, 상태나 효과를 기반으로 하는 Hook은 금지되어야 합니다. 대신, DOM과 상태에 의존하지 않는 useRef와 같은 Hook은 안전하게 사용할 수 있습니다.

13. RSCs Rules / 서버 컴포넌트와 클라이언트 컴포넌트의 상태 차이

  • 서버 컴포넌트에서의 상태:

    • 서버는 여러 클라이언트로부터 요청을 받을 수 있으므로 상태가 공유될 위험이 있습니다.
    • 서버-클라이언트 관계는 브로드캐스트(broadcast) 형식으로 동작하기 때문에, 하나의 상태가 여러 클라이언트 사이에서 공유될 수 있습니다.
    • 이는 민감한 데이터 유출 위험을 초래할 수 있습니다.
  • 클라이언트 컴포넌트에서의 상태:

    • 클라이언트 컴포넌트는 독립적인 상태 관리가 가능합니다.
    • 상태(useState, useReducer 등)를 클라이언트에서만 처리함으로써 상태가 클라이언트별로 고유하게 유지됩니다.
  • RSC의 상태와 Hook 규칙

    • 서버 컴포넌트는 상태와 관련된 Hook(useState, useReducer)을 사용할 수 없습니다.
    • 상태 관리가 필요한 컴포넌트는 클라이언트 컴포넌트로 분리해야 합니다.
  • 주요 이유

    1. 상태 공유 문제:
      • 서버에서 상태를 관리하면 여러 클라이언트가 같은 상태를 공유하게 되어 데이터 누출 가능성이 높습니다.
    2. 예측 가능성:
      • 클라이언트 컴포넌트에서만 상태를 처리하면 각 클라이언트의 상태가 독립적으로 관리되므로 예측 가능한 동작이 가능합니다.
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의 성능과 안정성을 유지하는 데 필수적입니다.

14. RSCs Rules / 클라이언트 컴포넌트와 서버 컴포넌트의 제한

  1. 클라이언트 컴포넌트는 서버 컴포넌트를 import할 수 없음

    • 서버 컴포넌트는 서버에서만 실행되며, 클라이언트 컴포넌트는 브라우저에서도 실행되기 때문에 클라이언트 환경에서 서버 컴포넌트를 실행하려 하면 에러가 발생합니다.
  2. 제한의 이유

    • 서버 컴포넌트는 브라우저에서 지원하지 않는 Node.js API나 서버 전용 기능(readFile, 데이터베이스 호출 등)을 사용할 수 있습니다.
    • 서버 컴포넌트를 클라이언트 컴포넌트가 import하면 브라우저에서 실행 시 해당 서버 기능이 실행되지 않아 런타임 에러가 발생합니다.
  • 해결 방법: Props로 서버 컴포넌트 전달

    • 클라이언트 컴포넌트는 서버 컴포넌트를 props로 전달받아 조합(composition)할 수 있습니다.
    • 서버 컴포넌트를 import하는 대신, 서버 컴포넌트를 렌더링하는 부모 컴포넌트에서 클라이언트 컴포넌트의 props로 전달하면 문제가 해결됩니다.
    • 이유: 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로 렌더링
  • 요약
    1. 클라이언트 컴포넌트는 서버 컴포넌트를 직접 import할 수 없습니다.
      • 서버 컴포넌트는 서버 전용 기능을 사용하므로 클라이언트 환경에서 실행할 수 없습니다.
    2. 해결 방법:
      • 클라이언트 컴포넌트는 props를 통해 서버 컴포넌트를 전달받아 조합합니다.
      • 부모 컴포넌트가 서버 컴포넌트를 렌더링하고, 클라이언트 컴포넌트의 children으로 전달합니다.

이 규칙은 번들러가 클라이언트와 서버 간의 컴포넌트를 정확히 분리하여 실행 환경에 맞게 동작하도록 보장합니다.

15. RSCs Rules / 클라이언트 컴포넌트는 여전히 중요한 역할을 한다

  • React에서 서버 컴포넌트가 도입되기 전까지는 클라이언트 컴포넌트만 존재했으며, 이 방식으로 모든 React 애플리케이션이 작성되었습니다.

  • 클라이언트 컴포넌트는 나쁜 것이 아닙니다.

    • 서버 컴포넌트는 기존 클라이언트 컴포넌트를 대체하는 것이 아니라, 보완하고 함께 사용할 수 있는 추가적인 도구입니다.
  • 클라이언트 컴포넌트의 역할

    • React 애플리케이션의 중심 요소:
      • 상호작용 및 이벤트 핸들링을 처리(useState, useEffect, onClick 등).
      • 사용자 경험(UX)을 완성하는 핵심적인 부분.
    • 여전히 가장 많이 작성되는 컴포넌트 유형:
      • 서버 컴포넌트는 정적인 데이터 렌더링에 적합하지만, 대부분의 애플리케이션은 여전히 클라이언트 컴포넌트를 중심으로 작성됩니다.
  • 서버 컴포넌트와의 관계

    • 서버 컴포넌트는 클라이언트 컴포넌트를 대체하지 않습니다.
    • 서버 컴포넌트는 클라이언트 컴포넌트를 보완하여 성능 최적화를 돕는 역할을 합니다.
      • 예: 초기 데이터 렌더링은 서버 컴포넌트, 사용자와의 상호작용은 클라이언트 컴포넌트.
  • 주요 메시지

    • 클라이언트 컴포넌트는 여전히 React의 기본 구성 요소입니다.
    • 서버 컴포넌트는 새로운 선택지일 뿐, 기존 클라이언트 컴포넌트를 대체하지 않습니다.
    • 서버와 클라이언트 컴포넌트를 함께 사용하여 성능과 사용자 경험을 최적화할 수 있습니다.
1. 클라이언트 컴포넌트
   ➡️ 사용자 상호작용 처리
   ➡️ 상태 관리 및 이벤트 핸들링
   ➡️ React 애플리케이션의 핵심 역할 수행

2. 서버 컴포넌트
   ➡️ 초기 데이터 렌더링 최적화
   ➡️ 클라이언트로 직렬화된 데이터를 전달
   ➡️ 클라이언트 컴포넌트를 보완

3. 서버와 클라이언트 협력
   ➡️ 서버에서 정적 데이터 제공
   ➡️ 클라이언트에서 동적 상호작용 활성화
  • 요약
    • 클라이언트 컴포넌트는 React의 기본 구성 요소로, 없어지지 않습니다.
      • 서버 컴포넌트는 보완적인 역할을 하며, 클라이언트 컴포넌트를 대체하지 않습니다.
    • 클라이언트 컴포넌트의 중요성:
      • 상호작용과 상태 관리의 중심.
      • 여전히 가장 많이 작성되고 활용되는 컴포넌트 유형.
    • 최적의 조합:
      • 서버 컴포넌트를 통해 초기 데이터 렌더링 최적화.
      • 클라이언트 컴포넌트로 동적 UI와 상호작용 처리.

React는 서버와 클라이언트 컴포넌트를 함께 사용하여 최적의 성능과 경험을 제공하도록 설계되었습니다.

16. RSCs / 서버 액션(Server Action)

React의 새로운 기능인 서버 컴포넌트(Server Components, RSC)와 함께 도입된 주요 기능 중 하나로 서버 액션(Server Action)이 있다. 서버 액션은 클라이언트 코드에서 서버 함수를 직접 호출할 수 있게 해주는 기술이다. 이를 위해 use server라는 새로운 디렉티브(directive)를 사용한다.

  • 핵심 개념:
    • use server 디렉티브:
      • 함수의 첫 번째 줄에 "use server"라고 명시하면, 해당 함수는 서버에서만 실행되어야 하는 서버 액션으로 지정된다. 이 함수는 클라이언트 코드에서 호출할 수 있으나, 실제 로직은 서버에서 실행된다.
    • 클라이언트 → 서버 호출 흐름:
      • 클라이언트에서 서버 액션 함수 호출 시, 인자(Arguments)는 직렬화되어(serialize) 네트워크 요청을 통해 서버로 전송된다. 서버는 해당 함수 로직을 실행한 뒤 결과 값을 직렬화하여 다시 클라이언트로 응답하게 된다. 클라이언트는 이 직렬화된 값을 받아서 사용한다.
    • 파일 단위 use server:
      • 개별 함수마다 "use server"를 선언하는 대신, 파일 최상단에 "use server"를 명시하면 그 파일에서 내보내는(export) 모든 함수들이 자동으로 서버 액션으로 간주된다. 이로써 파일 단위로 서버 액션을 일괄 관리할 수 있다.

요약하면, 서버 액션은 클라이언트 측에서 서버 함수를 호출할 수 있는 인터페이스를 제공하여, 클라이언트-서버 간 데이터 교환을 더욱 직관적으로 만들어 준다.

[클라이언트 코드] --(함수 호출 & 인자 직렬화)--> [서버 액션 함수 실행: 서버]
             <--(결과 직렬화 & 응답)-------
  1. 클라이언트에서 서버 액션 함수를 호출한다.
  2. 호출 시 전달된 인자가 직렬화되어 서버로 전송된다.
  3. 서버는 해당 서버 액션 함수를 실행한다.
  4. 함수의 반환값을 직렬화하여 클라이언트로 되돌려준다.
  5. 클라이언트는 응답 받은 직렬화 데이터를 역직렬화하여 사용한다.
  • 타입스크립트 기반 예시 코드

    • 예시 시나리오:
      • actions.ts 파일 최상단에 "use server"를 선언해서 해당 파일의 모든 함수가 서버 액션으로 동작한다.
      • actions.ts에서 addNumbers라는 서버 액션 함수를 작성한다. 이 함수는 두 숫자를 더한 뒤 결과를 반환한다.
      • 클라이언트 컴포넌트(ClientComponent.tsx)에서 addNumbers 함수를 호출하고 결과를 출력한다.
  • actions.ts (서버 액션 파일)

"use server";

export async function addNumbers(a: number, b: number): Promise<number> {
  // 이 함수는 서버에서만 실행된다.
  // 클라이언트에서는 이 함수를 호출만 할 수 있고, 실제 계산 로직은 서버에서 이루어짐.
  return a + b;
}
  • ClientComponent.tsx (클라이언트 컴포넌트)
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를 호출하고, 서버에서 계산된 결과를 받아 화면에 표시한다. 이 과정에서 클라이언트에서는 단순히 함수를 호출하는 듯 하지만, 실제 로직은 서버에서 처리되고, 그 결과만 클라이언트로 전달된다.

17. RSCs / Forms and Mutations

이 예제는 리액트(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>
      );
    }
    • 위 코드에서 requestUsername 함수는 'use server'를 선언하여 서버 액션임을 명시하고 있습니다.
    • <form action={requestUsername}>를 통해 폼 제출 시 해당 서버 액션 함수가 호출되며, 폼의 데이터가 서버로 전달됩니다.
  • git commit 1